From 92369af12fc2dbfc2a4f3ccb432341f41f8421e0 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 6 Nov 2025 18:34:32 +0100 Subject: [PATCH 1/2] make async strings work --- .../Sources/MySwiftLibrary/Async.swift | 4 ++ .../java/com/example/swift/AsyncTest.java | 6 ++ .../Convenience/JavaType+Extensions.swift | 9 +++ ...ISwift2JavaGenerator+JavaTranslation.swift | 6 +- ...wift2JavaGenerator+NativeTranslation.swift | 41 +++++++------ Sources/JExtractSwiftLib/JavaParameter.swift | 9 +++ .../JNI/JNIAsyncTests.swift | 61 +++++++++++++++++++ 7 files changed, 115 insertions(+), 21 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift index ebef1892..99f3a393 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -36,3 +36,7 @@ public func asyncOptional(i: Int64) async throws -> Int64? { public func asyncThrows() async throws { throw MySwiftError.swiftError } + +public func asyncString(input: String) async -> String { + return input +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index ae6e7cc7..5fe7c131 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -74,4 +74,10 @@ void asyncOptional() { CompletableFuture future = MySwiftLibrary.asyncOptional(42); assertEquals(OptionalLong.of(42), future.join()); } + + @Test + void asyncString() { + CompletableFuture future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.join()); + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index 3b29fcd3..645e5aa4 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -137,4 +137,13 @@ extension JavaType { return self } } + + var requiresBoxing: Bool { + switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double: + true + default: + false + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 56174e3e..a4e5ab45 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -514,14 +514,12 @@ extension JNISwift2JavaGenerator { ) // Update native function - nativeFunctionSignature.result.javaType = .void nativeFunctionSignature.result.conversion = .asyncCompleteFuture( - nativeFunctionSignature.result.conversion, swiftFunctionResultType: originalFunctionSignature.result.type, - nativeReturnType: nativeFunctionSignature.result.javaType, - outParameters: nativeFunctionSignature.result.outParameters, + nativeFunctionSignature: nativeFunctionSignature, isThrowing: originalFunctionSignature.isThrowing ) + nativeFunctionSignature.result.javaType = .void nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index d4ce9426..13e8c676 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -570,10 +570,8 @@ extension JNISwift2JavaGenerator { indirect case unwrapOptional(NativeSwiftConversionStep, name: String, fatalErrorMessage: String) indirect case asyncCompleteFuture( - NativeSwiftConversionStep, swiftFunctionResultType: SwiftType, - nativeReturnType: JavaType, - outParameters: [JavaParameter], + nativeFunctionSignature: NativeFunctionSignature, isThrowing: Bool ) @@ -806,15 +804,22 @@ extension JNISwift2JavaGenerator { return unwrappedName case .asyncCompleteFuture( - let inner, let swiftFunctionResultType, - let nativeReturnType, - let outParameters, + let nativeFunctionSignature, let isThrowing ): + var globalRefs: [String] = ["globalFuture"] + // Global ref all indirect returns - for outParameter in outParameters { + for outParameter in nativeFunctionSignature.result.outParameters { printer.print("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))") + globalRefs.append(parameter.name) } printer.print( @@ -826,16 +831,19 @@ extension JNISwift2JavaGenerator { func printDo(printer: inout CodePrinter) { printer.print("let swiftResult$ = await \(placeholder)") printer.print("environment = try! JavaVirtualMachine.shared().environment()") - let inner = inner.render(&printer, "swiftResult$") + let inner = nativeFunctionSignature.result.conversion.render(&printer, "swiftResult$") if swiftFunctionResultType.isVoid { printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)])") } else { - printer.print( - """ - let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(\(inner), in: environment) - environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: boxedResult$)]) - """ - ) + let result: String + if nativeFunctionSignature.result.javaType.requiresBoxing { + printer.print("let boxedResult$ = SwiftJavaRuntimeSupport._JNIBoxedConversions.box(\(inner), in: environment)") + result = "boxedResult$" + } else { + result = inner + } + + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: \(result))])") } } @@ -843,9 +851,8 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock("defer") { printer in // Defer might on any thread, so we need to attach environment. printer.print("let deferEnvironment = try! JavaVirtualMachine.shared().environment()") - printer.print("environment.interface.DeleteGlobalRef(deferEnvironment, globalFuture)") - for outParameter in outParameters { - printer.print("environment.interface.DeleteGlobalRef(deferEnvironment, \(outParameter.name))") + for globalRef in globalRefs { + printer.print("environment.interface.DeleteGlobalRef(deferEnvironment, \(globalRef))") } } if isThrowing { diff --git a/Sources/JExtractSwiftLib/JavaParameter.swift b/Sources/JExtractSwiftLib/JavaParameter.swift index 2670e6ee..0b243b3a 100644 --- a/Sources/JExtractSwiftLib/JavaParameter.swift +++ b/Sources/JExtractSwiftLib/JavaParameter.swift @@ -35,6 +35,15 @@ struct JavaParameter { } } + var isPrimitive: Bool { + switch self { + case .concrete(let javaType): + javaType.isPrimitive + case .generic(let name, let extends): + false + } + } + var jniTypeName: String { switch self { case .concrete(let type): type.jniTypeName diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index 82922c7d..81038797 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -353,4 +353,65 @@ struct JNIAsyncTests { ] ) } + + @Test("Import: (String) async -> String (Java, CompletableFuture)") + func completableFuture_asyncStringToString_java() throws { + try assertOutput( + input: """ + public func async(s: String) async -> String + """, + .jni, .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func async(s: String) async -> String + * } + */ + public static java.util.concurrent.CompletableFuture async(java.lang.String s) { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(s, $future); + return $future.thenApply((futureResult$) -> { + return futureResult$; + } + ); + } + """, + """ + private static native void $async(java.lang.String s, java.util.concurrent.CompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (String) async -> String (Swift, CompletableFuture)") + func completableFuture_asyncStringToString_swift() throws { + try assertOutput( + input: """ + public func async(s: String) async -> String + """, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__Ljava_lang_String_2Ljava_util_concurrent_CompletableFuture_2") + 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) + ... + defer { + let deferEnvironment = try! JavaVirtualMachine.shared().environment() + environment.interface.DeleteGlobalRef(deferEnvironment, globalFuture) + environment.interface.DeleteGlobalRef(deferEnvironment, s) + } + ... + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: swiftResult$.getJNIValue(in: environment))]) + ... + } + """ + ] + ) + } } From 802ae77d29db476a2aca5a1acadd3e270da2ef79 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 7 Nov 2025 13:43:43 +0900 Subject: [PATCH 2/2] Remove Swift downcall documentation comment from expected chunk --- Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index 81038797..975cccc6 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -364,12 +364,6 @@ struct JNIAsyncTests { detectChunkByInitialLines: 2, expectedChunks: [ """ - /** - * Downcall to Swift: - * {@snippet lang=swift : - * public func async(s: String) async -> String - * } - */ public static java.util.concurrent.CompletableFuture async(java.lang.String s) { java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); SwiftModule.$async(s, $future);