From 0f6140fb7eff66d358cb425ae3bd84873a34af6e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 28 Nov 2025 21:26:22 +0100 Subject: [PATCH 1/2] use thread environments --- Sources/SwiftJava/AnyJavaObject.swift | 2 +- .../SwiftJava/JavaObject+MethodCalls.swift | 50 ++++++++++++------- Sources/SwiftJava/JavaObjectHolder.swift | 2 + 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftJava/AnyJavaObject.swift b/Sources/SwiftJava/AnyJavaObject.swift index fe77bdbd..33a83159 100644 --- a/Sources/SwiftJava/AnyJavaObject.swift +++ b/Sources/SwiftJava/AnyJavaObject.swift @@ -66,7 +66,7 @@ extension AnyJavaObject { javaHolder.object } - /// Retrieve the environment in which this Java object resides. + /// Retrieve the environment in which this Java object was created. public var javaEnvironment: JNIEnvironment { javaHolder.environment } diff --git a/Sources/SwiftJava/JavaObject+MethodCalls.swift b/Sources/SwiftJava/JavaObject+MethodCalls.swift index 4880f750..0626be23 100644 --- a/Sources/SwiftJava/JavaObject+MethodCalls.swift +++ b/Sources/SwiftJava/JavaObject+MethodCalls.swift @@ -104,7 +104,8 @@ extension AnyJavaObject { resultType: Result.Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -115,7 +116,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: Result.javaType, - in: javaEnvironment + in: environment ) } } @@ -126,7 +127,8 @@ extension AnyJavaObject { parameterTypes: repeat (each Param).Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -137,7 +139,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: .void, - in: javaEnvironment + in: environment ) } } @@ -167,8 +169,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws -> Result { + let environment = try JavaVirtualMachine.shared().environment() + return try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -229,8 +233,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws { + let environment = try JavaVirtualMachine.shared().environment() + try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -276,7 +282,7 @@ extension AnyJavaObject { private func getJNIFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? where FieldType: ~Copyable { let this = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Retrieve the Java class instance from the object. let thisClass = environment.interface.GetObjectClass(environment, this)! @@ -289,15 +295,19 @@ extension AnyJavaObject { fieldType fieldType: FieldType.Type ) -> FieldType where FieldType: ~Copyable { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } nonmutating set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } @@ -311,7 +321,7 @@ extension JavaClass { resultType: Result.Type ) throws -> Result { let thisClass = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -345,7 +355,7 @@ extension JavaClass { arguments: repeat each Param ) throws { let thisClass = javaThis - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -372,7 +382,7 @@ extension JavaClass { /// Retrieve the JNI field ID for a field with the given name and type. private func getJNIStaticFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? { - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() return environment.interface.GetStaticFieldID(environment, javaThis, fieldName, FieldType.jniMangling) } @@ -382,15 +392,19 @@ extension JavaClass { fieldType fieldType: FieldType.Type ) -> FieldType { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniStaticFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniStaticFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } diff --git a/Sources/SwiftJava/JavaObjectHolder.swift b/Sources/SwiftJava/JavaObjectHolder.swift index 5930da59..b5e88835 100644 --- a/Sources/SwiftJava/JavaObjectHolder.swift +++ b/Sources/SwiftJava/JavaObjectHolder.swift @@ -32,6 +32,8 @@ public final class JavaObjectHolder { /// in Swift and the Java virtual machine is free to move or deallocate it. func forget() { if let object { + let environment = try! JavaVirtualMachine.shared().environment() + environment.interface.DeleteGlobalRef(environment, object) self.object = nil } From 9af73e40d5ab2a2191d1c2ca51a307719a6664db Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sat, 29 Nov 2025 08:15:39 +0100 Subject: [PATCH 2/2] add test --- Tests/SwiftJavaTests/BasicRuntimeTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/SwiftJavaTests/BasicRuntimeTests.swift b/Tests/SwiftJavaTests/BasicRuntimeTests.swift index b6c18bee..292c2a68 100644 --- a/Tests/SwiftJavaTests/BasicRuntimeTests.swift +++ b/Tests/SwiftJavaTests/BasicRuntimeTests.swift @@ -82,6 +82,17 @@ class BasicRuntimeTests: XCTestCase { let nullString = String(fromJNI: nil, in: environment) XCTAssertEqual(nullString, "") } + + func testCrossThreadAccess() async throws { + let environment = try jvm.environment() + let url = try URL("https://swift.org", environment: environment) + let string = await Task.detached { + // This should be called on a different thread + url.toString() + }.value + + XCTAssertEqual(string, "https://swift.org") + } } @JavaClass("org.swift.javakit.Nonexistent")