From ff986a105835de466d7a00ac84fa83159960dd4d Mon Sep 17 00:00:00 2001 From: Davide Sibilio Date: Mon, 13 Apr 2026 18:05:25 +0200 Subject: [PATCH 1/2] Fix JNI generation for Swift 6.2 sending closures Use fully qualified Java class references in generated calls and box JNI captures as unchecked Sendable before entering Task closures. Reattach the Java environment inside immediate tasks instead of capturing the caller environment directly. Fix JNI generation for Swift 6.2 sending closures Use fully qualified Java class references in generated calls and box JNI captures as unchecked Sendable before entering Task closures. Reattach the Java environment inside immediate tasks instead of capturing the caller environment directly. --- ...wift2JavaGenerator+NativeTranslation.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 954c865a..134b518a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1650,6 +1650,21 @@ extension JNISwift2JavaGenerator { """ ) + printer.print("struct _SwiftJavaUncheckedSendableBox: @unchecked Sendable { let value: T }") + for globalRef in globalRefs { + printer.print("let \(globalRef)_sendable$ = _SwiftJavaUncheckedSendableBox(value: \(globalRef))") + } + if let selfParameter = nativeFunctionSignature.selfParameter { + for parameter in selfParameter.parameters { + printer.print("let \(parameter.name)$sendable$ = _SwiftJavaUncheckedSendableBox(value: \(parameter.name)$)") + } + } + if let selfTypeParameter = nativeFunctionSignature.selfTypeParameter { + for parameter in selfTypeParameter.parameters { + printer.print("let \(parameter.name)$sendable$ = _SwiftJavaUncheckedSendableBox(value: \(parameter.name)$)") + } + } + func printDo(printer: inout CodePrinter) { // Make sure try/await are printed when necessary and avoid duplicate, or wrong-order, keywords (which would cause warnings) let placeholderWithoutTry = @@ -1692,6 +1707,19 @@ extension JNISwift2JavaGenerator { } func printTaskBody(printer: inout CodePrinter) { + for globalRef in globalRefs { + printer.print("let \(globalRef) = \(globalRef)_sendable$.value") + } + if let selfParameter = nativeFunctionSignature.selfParameter { + for parameter in selfParameter.parameters { + printer.print("let \(parameter.name)$ = \(parameter.name)$sendable$.value") + } + } + if let selfTypeParameter = nativeFunctionSignature.selfTypeParameter { + for parameter in selfTypeParameter.parameters { + printer.print("let \(parameter.name)$ = \(parameter.name)$sendable$.value") + } + } printer.printBraceBlock("defer") { printer in // Defer might on any thread, so we need to attach environment. printer.print("let deferEnvironment = try! JavaVirtualMachine.shared().environment()") @@ -1722,8 +1750,8 @@ extension JNISwift2JavaGenerator { printer.printHashIfBlock("swift(>=6.2)") { printer in printer.printBraceBlock("if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *)") { printer in printer.printBraceBlock("task = Task.immediate") { printer in - // Immediate runs on the caller thread, so we don't need to attach the environment again. - printer.print("var environment = environment!") // this is to ensure we always use the same environment name, even though we are rebinding it. + // Even immediate tasks are a sending closure in Swift 6.2+, so reattach instead of capturing the caller's environment directly. + printer.print("var environment = try! JavaVirtualMachine.shared().environment()") printTaskBody(printer: &printer) } } From 810ef95a1d27471d27cf1f83674c0fb1ee1cfe9a Mon Sep 17 00:00:00 2001 From: Davide Sibilio Date: Tue, 14 Apr 2026 06:25:50 +0200 Subject: [PATCH 2/2] Fix Swift 6.2 sending closure errors in generated JNI async thunks --- ...wift2JavaGenerator+NativeTranslation.swift | 21 +++++++------------ .../JNI/JNIAsyncTests.swift | 20 +++++++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 134b518a..dc565af5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1633,35 +1633,31 @@ extension JNISwift2JavaGenerator { // Global ref all indirect returns for outParameter in nativeFunctionSignature.result.outParameters { printer.print( - "let \(outParameter.name) = environment.interface.NewGlobalRef(environment, \(outParameter.name))" + "nonisolated(unsafe) let \(outParameter.name) = environment.interface.NewGlobalRef(environment, \(outParameter.name))" ) globalRefs.append(outParameter.name) } // We also need to global ref any objects passed in for parameter in nativeFunctionSignature.parameters.flatMap(\.parameters) where !parameter.type.isPrimitive { - printer.print("let \(parameter.name) = environment.interface.NewGlobalRef(environment, \(parameter.name))") + printer.print("nonisolated(unsafe) let \(parameter.name) = environment.interface.NewGlobalRef(environment, \(parameter.name))") globalRefs.append(parameter.name) } printer.print( """ - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) """ ) - printer.print("struct _SwiftJavaUncheckedSendableBox: @unchecked Sendable { let value: T }") - for globalRef in globalRefs { - printer.print("let \(globalRef)_sendable$ = _SwiftJavaUncheckedSendableBox(value: \(globalRef))") - } if let selfParameter = nativeFunctionSignature.selfParameter { for parameter in selfParameter.parameters { - printer.print("let \(parameter.name)$sendable$ = _SwiftJavaUncheckedSendableBox(value: \(parameter.name)$)") + printer.print("nonisolated(unsafe) let \(parameter.name)Sendable$ = \(parameter.name)$") } } if let selfTypeParameter = nativeFunctionSignature.selfTypeParameter { for parameter in selfTypeParameter.parameters { - printer.print("let \(parameter.name)$sendable$ = _SwiftJavaUncheckedSendableBox(value: \(parameter.name)$)") + printer.print("nonisolated(unsafe) let \(parameter.name)Sendable$ = \(parameter.name)$") } } @@ -1707,17 +1703,14 @@ extension JNISwift2JavaGenerator { } func printTaskBody(printer: inout CodePrinter) { - for globalRef in globalRefs { - printer.print("let \(globalRef) = \(globalRef)_sendable$.value") - } if let selfParameter = nativeFunctionSignature.selfParameter { for parameter in selfParameter.parameters { - printer.print("let \(parameter.name)$ = \(parameter.name)$sendable$.value") + printer.print("let \(parameter.name)$ = \(parameter.name)Sendable$") } } if let selfTypeParameter = nativeFunctionSignature.selfTypeParameter { for parameter in selfTypeParameter.parameters { - printer.print("let \(parameter.name)$ = \(parameter.name)$sendable$.value") + printer.print("let \(parameter.name)$ = \(parameter.name)Sendable$") } } printer.printBraceBlock("defer") { printer in diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index b7b4a41a..7c968e41 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -61,12 +61,12 @@ struct JNIAsyncTests { """ @_cdecl("Java_com_example_swift_SwiftModule__00024asyncVoid__Ljava_util_concurrent_CompletableFuture_2") public func Java_com_example_swift_SwiftModule__00024asyncVoid__Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, result_future: jobject?) { - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) var task: Task? = nil #if swift(>=6.2) if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { task = Task.immediate { - var environment = environment! + var environment = try! JavaVirtualMachine.shared().environment() defer { let deferEnvironment = try! JavaVirtualMachine.shared().environment() deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) @@ -137,12 +137,12 @@ struct JNIAsyncTests { """ @_cdecl("Java_com_example_swift_SwiftModule__00024async__Ljava_util_concurrent_CompletableFuture_2") public func Java_com_example_swift_SwiftModule__00024async__Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, result_future: jobject?) { - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) var task: Task? = nil #if swift(>=6.2) if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { task = Task.immediate { - var environment = environment! + var environment = try! JavaVirtualMachine.shared().environment() defer { let deferEnvironment = try! JavaVirtualMachine.shared().environment() deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) @@ -227,12 +227,12 @@ struct JNIAsyncTests { """ @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2") public func Java_com_example_swift_SwiftModule__00024async__JLjava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, i: jlong, result_future: jobject?) { - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) var task: Task? = nil #if swift(>=6.2) if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { task = Task.immediate { - var environment = environment! + var environment = try! JavaVirtualMachine.shared().environment() defer { let deferEnvironment = try! JavaVirtualMachine.shared().environment() deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) @@ -320,12 +320,12 @@ struct JNIAsyncTests { guard let c$ else { fatalError("c memory address was null in call to \\(#function)!") } - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) var task: Task? = nil #if swift(>=6.2) if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { task = Task.immediate { - var environment = environment! + var environment = try! JavaVirtualMachine.shared().environment() defer { let deferEnvironment = try! JavaVirtualMachine.shared().environment() deferEnvironment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) @@ -403,8 +403,8 @@ struct JNIAsyncTests { """ @_cdecl("Java_com_example_swift_SwiftModule__00024async__Ljava_lang_String_2Ljava_util_concurrent_CompletableFuture_2") public func Java_com_example_swift_SwiftModule__00024async__Ljava_lang_String_2Ljava_util_concurrent_CompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jstring?, result_future: jobject?) { - let s = environment.interface.NewGlobalRef(environment, s) - let globalFuture = environment.interface.NewGlobalRef(environment, result_future) + nonisolated(unsafe) let s = environment.interface.NewGlobalRef(environment, s) + nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future) ... defer { let deferEnvironment = try! JavaVirtualMachine.shared().environment()