Skip to content

Commit 4abf4fa

Browse files
committed
revamp testing infra for wrap-java, use classloader per test
1 parent d965693 commit 4abf4fa

File tree

12 files changed

+306
-163
lines changed

12 files changed

+306
-163
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ let package = Package(
403403
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
404404
"SwiftJava",
405405
"JavaUtilJar",
406+
"JavaNet",
406407
"JavaLangReflect",
407408
"JavaNet",
408409
"JavaTypes",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftJava
16+
import CSwiftJavaJNI
17+
18+
#if canImport(Darwin)
19+
import Foundation
20+
public typealias SwiftJavaFoundationURL = Foundation.URL
21+
#else
22+
import FoundationNetworking
23+
public typealias SwiftJavaFoundationURL = FoundationNetworking.URL
24+
#endif
25+
26+
extension SwiftJavaFoundationURL {
27+
public static func fromJava(_ url: URL) throws -> SwiftJavaFoundationURL {
28+
guard let converted = SwiftJavaFoundationURL(string: try url.toURI().toString()) else {
29+
throw SwiftJavaConversionError("Failed to convert \(URL.self) to \(SwiftJavaFoundationURL.self)")
30+
}
31+
return converted
32+
}
33+
}
34+
35+
extension URL {
36+
public static func fromSwift(_ url: SwiftJavaFoundationURL) throws -> URL {
37+
return try URL(url.absoluteString)
38+
}
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import CSwiftJavaJNI
16+
import SwiftJava
17+
18+
// FIXME: workaround until importing properly would make UCL inherit from CL https://github.com/swiftlang/swift-java/issues/423
19+
extension URLClassLoader /* workaround for missing inherits from ClassLoader */ {
20+
@JavaMethod
21+
public func loadClass(_ name: String) throws -> JavaClass<JavaObject>?
22+
}

Sources/SwiftJava/AnyJavaObject.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public protocol AnyJavaObject {
5252
/// Protocol that allows Swift types to specify a custom Java class loader on
5353
/// initialization. This is useful for platforms (e.g. Android) where the default
5454
/// class loader does not make all application classes visible.
55-
public protocol CustomJavaClassLoader: AnyJavaObject {
55+
public protocol AnyJavaObjectWithCustomClassLoader: AnyJavaObject {
5656
static func getJavaClassLoader(in environment: JNIEnvironment) throws -> JavaClassLoader!
5757
}
5858

@@ -118,8 +118,8 @@ extension AnyJavaObject {
118118
in environment: JNIEnvironment,
119119
_ body: (jclass) throws -> Result
120120
) throws -> Result {
121-
if let customJavaClassLoader = self as? CustomJavaClassLoader.Type,
122-
let customClassLoader = try customJavaClassLoader.getJavaClassLoader(in: environment) {
121+
if let AnyJavaObjectWithCustomClassLoader = self as? AnyJavaObjectWithCustomClassLoader.Type,
122+
let customClassLoader = try AnyJavaObjectWithCustomClassLoader.getJavaClassLoader(in: environment) {
123123
try _withJNIClassFromCustomClassLoader(customClassLoader, in: environment, body)
124124
} else {
125125
try _withJNIClassFromDefaultClassLoader(in: environment, body)

Sources/SwiftJava/JVM/JavaVirtualMachine.swift

Lines changed: 116 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,22 @@ public final class JavaVirtualMachine: @unchecked Sendable {
3939

4040
/// Thread-local storage to detach from thread on exit
4141
private static let destroyTLS = ThreadLocalStorage { _ in
42+
debug("Run destroyThreadLocalStorage; call JVM.shared() detach current thread")
4243
try? JavaVirtualMachine.shared().detachCurrentThread()
4344
}
4445

4546
/// The Java virtual machine instance.
4647
private let jvm: JavaVMPointer
4748

48-
let classpath: [String]
49+
let classpath: [String]?
4950

5051
/// Whether to destroy the JVM on deinit.
5152
private let destroyOnDeinit: LockedState<Bool> // FIXME: we should require macOS 15 and then use Synchronization
5253

5354
/// Adopt an existing JVM pointer.
54-
public init(adoptingJVM jvm: JavaVMPointer) {
55+
public init(adoptingJVM jvm: JavaVMPointer, classpath: [String]? = nil) {
5556
self.jvm = jvm
56-
self.classpath = [] // FIXME: bad...
57+
self.classpath = nil
5758
self.destroyOnDeinit = .init(initialState: false)
5859
}
5960

@@ -86,7 +87,7 @@ public final class JavaVirtualMachine: @unchecked Sendable {
8687
for path in classpath {
8788
if !fileManager.fileExists(atPath: path) {
8889
// FIXME: this should be configurable, a classpath missing a directory isn't reason to blow up
89-
print("[warning][swift-java][JavaVirtualMachine] Missing classpath element: \(URL(fileURLWithPath: path).absoluteString)") // TODO: stderr
90+
debug("[warning] Missing classpath element: \(URL(fileURLWithPath: path).absoluteString)") // TODO: stderr
9091
}
9192
}
9293
let pathSeparatedClassPath = classpath.joined(separator: FileManager.pathSeparator)
@@ -116,7 +117,10 @@ public final class JavaVirtualMachine: @unchecked Sendable {
116117
vmArgs.options = optionsBuffer.baseAddress
117118
vmArgs.nOptions = jint(optionsBuffer.count)
118119

119-
// Create the JVM instance.
120+
debug("Create JVM instance. Options:\(allVMOptions)")
121+
debug("Create JVM instance. jvm:\(jvm)")
122+
debug("Create JVM instance. environment:\(environment)")
123+
debug("Create JVM instance. vmArgs:\(vmArgs)")
120124
if let createError = VMError(fromJNIError: JNI_CreateJavaVM(&jvm, &environment, &vmArgs)) {
121125
throw createError
122126
}
@@ -126,8 +130,10 @@ public final class JavaVirtualMachine: @unchecked Sendable {
126130
}
127131

128132
public func destroyJVM() throws {
133+
debug("Destroy jvm (jvm:\(jvm))")
129134
try self.detachCurrentThread()
130-
if let error = VMError(fromJNIError: jvm.pointee!.pointee.DestroyJavaVM(jvm)) {
135+
let destroyResult = jvm.pointee!.pointee.DestroyJavaVM(jvm)
136+
if let error = VMError(fromJNIError: destroyResult) {
131137
throw error
132138
}
133139

@@ -151,9 +157,33 @@ extension JavaVirtualMachine: CustomStringConvertible {
151157
}
152158
}
153159

160+
let SwiftJavaVerboseLogging = {
161+
if let str = ProcessInfo.processInfo.environment["SWIFT_JAVA_VERBOSE"] {
162+
switch str.lowercased() {
163+
case "true", "yes", "1": true
164+
case "false", "no", "0": false
165+
default: false
166+
}
167+
} else {
168+
false
169+
}
170+
}()
171+
172+
fileprivate func debug(_ message: String, file: String = #fileID, line: Int = #line, function: String = #function) {
173+
if SwiftJavaVerboseLogging {
174+
print("[swift-java-jvm][t:\(getCurrentThreadID())][\(file):\(line)](\(function)) \(message)")
175+
}
176+
}
177+
154178
// ==== ------------------------------------------------------------------------
155179
// MARK: Java thread management.
156180

181+
fileprivate func getCurrentThreadID() -> UInt64 {
182+
var threadID: UInt64 = 0
183+
pthread_threadid_np(nil, &threadID)
184+
return threadID
185+
}
186+
157187
extension JavaVirtualMachine {
158188
/// Produce the JNI environment for the active thread, attaching this
159189
/// thread to the JVM if it isn't already.
@@ -162,6 +192,7 @@ extension JavaVirtualMachine {
162192
/// - asDaemon: Whether this thread should be treated as a daemon
163193
/// thread in the Java Virtual Machine.
164194
public func environment(asDaemon: Bool = false) throws -> JNIEnvironment {
195+
debug("Get JVM env, asDaemon:\(asDaemon)")
165196
// Check whether this thread is already attached. If so, return the
166197
// corresponding environment.
167198
var environment: UnsafeMutableRawPointer? = nil
@@ -205,6 +236,7 @@ extension JavaVirtualMachine {
205236
/// Detach the current thread from the Java Virtual Machine. All Java
206237
/// threads waiting for this thread to die are notified.
207238
func detachCurrentThread() throws {
239+
debug("Detach current thread, jvm:\(jvm)")
208240
if let resultError = VMError(fromJNIError: jvm.pointee!.pointee.DetachCurrentThread(jvm)) {
209241
throw resultError
210242
}
@@ -227,6 +259,17 @@ extension JavaVirtualMachine {
227259
/// simple.
228260
private static let sharedJVM: LockedState<JVMState> = .init(initialState: .init(jvm: nil, classpath: []))
229261

262+
public static func destroySharedJVM() throws {
263+
debug("Destroy shared JVM")
264+
return try sharedJVM.withLock { (sharedJVMPointer: inout JVMState) in
265+
if let jvm = sharedJVMPointer.jvm {
266+
try jvm.destroyJVM()
267+
}
268+
sharedJVMPointer.jvm = nil
269+
sharedJVMPointer.classpath = []
270+
}
271+
}
272+
230273
/// Access the shared Java Virtual Machine instance.
231274
///
232275
/// If there is no shared Java Virtual Machine, create one with the given
@@ -252,23 +295,26 @@ extension JavaVirtualMachine {
252295
file: String = #fileID, line: Int = #line
253296
) throws -> JavaVirtualMachine {
254297
precondition(!classpath.contains(where: { $0.contains(FileManager.pathSeparator) }), "Classpath element must not contain `\(FileManager.pathSeparator)`! Split the path into elements! Was: \(classpath)")
255-
print("[swift] Get shared JVM at \(file):\(line): Classpath = \(classpath.joined(separator: ","))")
298+
debug("Get shared JVM at \(file):\(line): Classpath = \(classpath.joined(separator: FileManager.pathSeparator))")
256299

257300
return try sharedJVM.withLock { (sharedJVMPointer: inout JVMState) in
258301
// If we already have a JavaVirtualMachine instance, return it.
259302
if replace {
260-
print("[swift] Replace JVM instance")
303+
debug("Replace JVM instance")
261304
if let jvm = sharedJVMPointer.jvm {
262-
print("[swift] destroyJVM instance!")
305+
debug("destroyJVM instance!")
263306
try jvm.destroyJVM()
264-
print("[swift] destroyJVM instance, done.")
307+
debug("destroyJVM instance, done.")
265308
}
266309
sharedJVMPointer.jvm = nil
267310
sharedJVMPointer.classpath = []
268311
} else {
269312
if let existingInstance = sharedJVMPointer.jvm {
270-
if classpath != sharedJVMPointer.classpath {
271-
print("[swift] Return existing JVM instance, same classpath classpath.")
313+
if classpath == [] {
314+
debug("Return existing JVM instance, no classpath requirement.")
315+
return existingInstance
316+
} else if classpath != sharedJVMPointer.classpath {
317+
debug("Return existing JVM instance, same classpath classpath.")
272318
return existingInstance
273319
} else {
274320
fatalError(
@@ -281,50 +327,88 @@ extension JavaVirtualMachine {
281327
}
282328
}
283329

330+
var remainingRetries = 8
284331
while true {
332+
remainingRetries -= 1
333+
guard remainingRetries > 0 else {
334+
fatalError("Unable to find or create JVM")
335+
}
336+
285337
var wasExistingVM: Bool = false
286338
while true {
339+
remainingRetries -= 1
340+
guard remainingRetries > 0 else {
341+
fatalError("Unable to find or create JVM")
342+
}
343+
287344
// Query the JVM itself to determine whether there is a JVM
288-
// instance that we don't yet know about.
289-
var jvm: UnsafeMutablePointer<JavaVM?>? = nil
345+
// instance that we don't yet know about.©
290346
var numJVMs: jsize = 0
291347
if JNI_GetCreatedJavaVMs(nil, 0, &numJVMs) == JNI_OK, numJVMs == 0 {
292-
print("[swift] Found JVMs: \(numJVMs), return existing one")
348+
debug("Found JVMs: \(numJVMs), create new one")
349+
} else {
350+
debug("Found JVMs: \(numJVMs), get existing one...")
293351
}
294352

295-
if JNI_GetCreatedJavaVMs(&jvm, 1, &numJVMs) == JNI_OK, numJVMs >= 1 {
296-
print("[swift] Found JVMs: \(numJVMs), return existing one")
297-
// Adopt this JVM into a new instance of the JavaVirtualMachine
298-
// wrapper.
299-
// FIXME: account for classpath
300-
let javaVirtualMachine = JavaVirtualMachine(adoptingJVM: jvm!)
301-
sharedJVMPointer.jvm = javaVirtualMachine
302-
sharedJVMPointer.classpath = classpath
303-
return javaVirtualMachine
353+
// Allocate buffer to retrieve existing JVM instances
354+
// Only allocate if we actually have JVMs to query
355+
if numJVMs > 0 {
356+
let bufferCapacity = Int(numJVMs)
357+
let jvmInstancesBuffer = UnsafeMutableBufferPointer<JavaVM?>.allocate(capacity: bufferCapacity)
358+
defer {
359+
jvmInstancesBuffer.deallocate()
360+
}
361+
362+
// Query existing JVM instances with proper error handling
363+
var jvmBufferPointer = jvmInstancesBuffer.baseAddress
364+
let jvmQueryResult = JNI_GetCreatedJavaVMs(&jvmBufferPointer, numJVMs, &numJVMs)
365+
366+
// Handle query result with comprehensive error checking
367+
guard jvmQueryResult == JNI_OK else {
368+
if let queryError = VMError(fromJNIError: jvmQueryResult) {
369+
debug("Failed to query existing JVMs: \(queryError)")
370+
throw queryError
371+
}
372+
fatalError("Unknown error querying JVMs, result code: \(jvmQueryResult)")
373+
}
374+
375+
if numJVMs >= 1 {
376+
debug("Found JVMs: \(numJVMs), try to adopt existing one")
377+
// Adopt this JVM into a new instance of the JavaVirtualMachine wrapper.
378+
let javaVirtualMachine = JavaVirtualMachine(
379+
adoptingJVM: jvmInstancesBuffer.baseAddress!,
380+
classpath: classpath
381+
)
382+
sharedJVMPointer.jvm = javaVirtualMachine
383+
sharedJVMPointer.classpath = classpath
384+
return javaVirtualMachine
385+
}
386+
387+
precondition(
388+
!wasExistingVM,
389+
"JVM reports that an instance of the JVM was already created, but we didn't see it."
390+
)
304391
}
305392

306-
precondition(
307-
!wasExistingVM,
308-
"JVM reports that an instance of the JVM was already created, but we didn't see it."
309-
)
310-
311393
// Create a new instance of the JVM.
312-
print("[swift] Create JVM")
394+
debug("Create JVM, classpath: \(classpath.joined(separator: FileManager.pathSeparator))")
313395
let javaVirtualMachine: JavaVirtualMachine
314396
do {
315397
javaVirtualMachine = try JavaVirtualMachine(
316398
classpath: classpath,
317-
vmOptions: vmOptions,
399+
vmOptions: vmOptions, // + ["-verbose:jni"],
318400
ignoreUnrecognized: ignoreUnrecognized
319401
)
320402
} catch VMError.existingVM {
321403
// We raced with code outside of this JavaVirtualMachine instance
322404
// that created a VM while we were trying to do the same. Go
323405
// through the loop again to pick up the underlying JVM pointer.
406+
debug("Failed to create JVM, Existing VM!")
324407
wasExistingVM = true
325408
continue
326409
}
327410

411+
debug("Created JVM: \(javaVirtualMachine)")
328412
sharedJVMPointer.jvm = javaVirtualMachine
329413
sharedJVMPointer.classpath = classpath
330414
return javaVirtualMachine
@@ -337,6 +421,7 @@ extension JavaVirtualMachine {
337421
///
338422
/// This will allow the shared JavaVirtualMachine instance to be deallocated.
339423
public static func forgetShared() {
424+
debug("forget shared JVM, without destroying it")
340425
sharedJVM.withLock { sharedJVMPointer in
341426
sharedJVMPointer.jvm = nil
342427
sharedJVMPointer.classpath = []
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Used to indicate Swift/Java conversion failures.
16+
public struct SwiftJavaConversionError: Error {
17+
public let message: String
18+
19+
public init(_ message: String) {
20+
self.message = message
21+
}
22+
}

Sources/SwiftJavaTool/Java/JavaClassLoader.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SwiftJavaShared
1717
import CSwiftJavaJNI
1818
import SwiftJava
1919

20+
// FIXME: do we need this here or can we rely on the generated one?
2021
@JavaClass("java.lang.ClassLoader")
2122
public struct ClassLoader {
2223
@JavaMethod

0 commit comments

Comments
 (0)