From 1c9c36c2246fce75fe701e002155bdf121008d20 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Sun, 6 Sep 2026 14:03:31 +0530 Subject: [PATCH 1/4] jextract/jni: Support async closures (#834) - Exclude async closures from matching synchronous KnownJavaFunctionalInterfaces - Generate CompletableFuture return type for async Java functional interfaces - Lower async closure invocation in Swift thunk to await future.get() - Add comprehensive unit tests in JNIClosureTests --- ...avaGenerator+EscapingClosureWrapJava.swift | 4 + ...ISwift2JavaGenerator+JavaTranslation.swift | 20 +- ...wift2JavaGenerator+NativeTranslation.swift | 52 +++++- .../KnownFunctionalInterfaces.swift | 6 +- .../JNI/JNIClosureTests.swift | 175 ++++++++++++++++++ 5 files changed, 246 insertions(+), 11 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index 6b6a4e729..9c12dec67 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -65,6 +65,10 @@ extension JNISwift2JavaGenerator { } let paramList = "(\(params.joined(separator: .comma)))" + if type.isAsync { + return "\(paramList) -> JavaObject?" + } + if type.resultType.isVoid { return paramList } else { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index f3e6e5189..4c856a24c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -265,7 +265,24 @@ extension JNISwift2JavaGenerator { ) } - let translatedResult = try translateResult(swiftType: swiftType.resultType, methodName: name) + var translatedResult = try translateResult(swiftType: swiftType.resultType, methodName: name) + + if swiftType.isAsync { + let futureType: JavaType = + switch self.config.effectiveAsyncFuncMode { + case .completableFuture: + .completableFuture(translatedResult.javaType) + case .legacyFuture: + .simpleCompletableFuture(translatedResult.javaType) + } + translatedResult = TranslatedResult( + javaType: futureType, + nativeJavaType: futureType, + annotations: translatedResult.annotations, + outParameters: translatedResult.outParameters, + conversion: translatedResult.conversion + ) + } return TranslatedFunctionType( name: name, @@ -1814,6 +1831,7 @@ extension JNISwift2JavaGenerator { var swiftType: SwiftFunctionType var isEscaping: Bool { swiftType.isEscaping } + var isAsync: Bool { swiftType.isAsync } /// Represents this `TranslatedFunctionType` if we need to create a synthetic protocol /// to handle the cross-language call to the function (closure). diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index a262e5fc9..f758702db 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -370,11 +370,11 @@ extension JNISwift2JavaGenerator { parentName: SwiftQualifiedTypeName ) throws -> NativeParameter { // @Sendable is not supported yet as "environment" is later captured inside the closure. - if functionType.isEscaping { - // For escaping closures we e need to create a Swift wrapper around + if functionType.isEscaping || functionType.isAsync { + // For escaping or async closures we need to create a Swift wrapper around // the passed down Java functional interface because we must keep it // alive with a global ref, that will remain around for as long as the - // escaping closure is. + // closure is. // // Prepare the name and shapes of the Java side functional interface // and Swift side @JavaInterface wrapper we'll use to implement that @@ -1746,12 +1746,8 @@ extension JNISwift2JavaGenerator { } // Build result conversion - // Note: The Java interface is synchronous even for async closures. - // The async nature is on the Swift side, inferred from the expected type. var resultPrinter = SwiftPrinter() let upcallExpr = "\(javaInterfaceVar).apply(\(upcallArguments.joined(separator: .comma)))" - let resultConverted = syntheticFunction.resultConversion.render(&resultPrinter, upcallExpr) - let resultPrefix = resultPrinter.finalize() // Note: async is part of the closure TYPE, not the closure literal syntax. // For closures without parameters, we can omit "in" entirely. @@ -1760,6 +1756,48 @@ extension JNISwift2JavaGenerator { ? "{" : "{ \(closureParameters) in" + if fn.isAsync { + let tryKeyword = fn.isThrowing ? "try " : "try? " + if isVoid { + printer.print( + """ + { + guard let \(placeholder) else { + fatalError("\(placeholder) is null") + } + let \(javaInterfaceVar) = \(syntheticFunction.javaInterfaceName)(javaThis: \(placeholder), environment: environment) + return \(closureHeader) + let future$ = \(upcallExpr) + _ = \(tryKeyword)future$?.dynamicJavaMethodCall(methodName: "get") + } + }() + """ + ) + } else { + printer.print( + """ + { + guard let \(placeholder) else { + fatalError("\(placeholder) is null") + } + let \(javaInterfaceVar) = \(syntheticFunction.javaInterfaceName)(javaThis: \(placeholder), environment: environment) + return \(closureHeader) + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = \(upcallExpr) + let result$ = \(tryKeyword)future$?.dynamicJavaMethodCall(methodName: "get", resultType: JavaObject?.self) + return \(fn.resultType.description).fromJavaObject(result$?.javaThis, in: environment$) + } + }() + """ + ) + } + + return printer.finalize() + } + + let resultConverted = syntheticFunction.resultConversion.render(&resultPrinter, upcallExpr) + let resultPrefix = resultPrinter.finalize() + // Construct the generated `@JavaInterface` wrap-java struct. // It will cause a new global ref on the javaThis, so no need for explicit global refs. // This object will be closed over by the closure we pass to Swift, and therefore keep alive the diff --git a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift index fe4b0b04e..06ea0732a 100644 --- a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift +++ b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift @@ -210,7 +210,7 @@ struct KnownJavaFunctionalInterface: Sendable { } static func find(_ functionType: SwiftFunctionType) -> KnownJavaFunctionalInterface? { - if functionType.isEscaping { + if functionType.isEscaping || functionType.isAsync { return nil } @@ -325,14 +325,14 @@ struct KnownJavaFunctionalInterface: Sendable { } static func find(_ functionType: JNISwift2JavaGenerator.TranslatedFunctionType) -> KnownJavaFunctionalInterface? { - if functionType.isEscaping { + if functionType.isEscaping || functionType.swiftType.isAsync { return nil } return find(parameters: functionType.parameters, result: functionType.result) } static func find(_ functionType: FFMSwift2JavaGenerator.TranslatedFunctionType) -> KnownJavaFunctionalInterface? { - if functionType.swiftType.isEscaping { + if functionType.swiftType.isEscaping || functionType.swiftType.isAsync { return nil } return find(parameters: functionType.parameters.map(\.parameter.type.javaType), result: functionType.result.javaResultType) diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 4ef90684c..9f36e69ed 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1159,4 +1159,179 @@ struct JNIClosureTests { ] ) } + + @Test + func asyncVoidClosure_javaBindings() throws { + let source = """ + public func schedule(op: () async -> Void) {} + """ + + try assertOutput( + input: source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class schedule { + /** Corresponds to the Swift closure parameter of type {@code () async -> Void}. */ + @FunctionalInterface + public interface op { + java.util.concurrent.CompletableFuture apply(); + } + } + """, + """ + public static void schedule(com.example.swift.SwiftModule.schedule.op op) { + SwiftModule.$schedule(op); + } + """, + ] + ) + } + + @Test + func escapingAsyncVoidClosure_javaBindings() throws { + let source = """ + public func schedule(op: @escaping () async -> Void) {} + """ + + try assertOutput( + input: source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class schedule { + /** Corresponds to the Swift closure parameter of type {@code @escaping () async -> Void}. */ + @FunctionalInterface + public interface op { + java.util.concurrent.CompletableFuture apply(); + } + } + """, + """ + public static void schedule(com.example.swift.SwiftModule.schedule.op op) { + SwiftModule.$schedule(op); + } + """, + ] + ) + } + + @Test + func asyncVoidClosure_swiftThunks() throws { + let source = """ + public func schedule(op: () async -> Void) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @JavaInterface("com.example.swift.SwiftModule$schedule$op") + public struct JavaSwiftModule_schedule_op { + @JavaMethod + public func apply() -> JavaObject? + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024schedule__Lcom_example_swift_SwiftModule_00024schedule_00024op_2") + public func Java_com_example_swift_SwiftModule__00024schedule__Lcom_example_swift_SwiftModule_00024schedule_00024op_2(environment: UnsafeMutablePointer!, thisClass: jclass, op: jobject?) { + SwiftModule.schedule(op: { + guard let op else { + fatalError("op is null") + } + let javaInterface_op$ = JavaSwiftModule_schedule_op(javaThis: op, environment: environment) + return { + let future$ = javaInterface_op$.apply() + _ = try? future$?.dynamicJavaMethodCall(methodName: "get") + } + }() + ) + } + """, + ] + ) + } + + @Test + func asyncThrowingClosure_swiftThunks() throws { + let source = """ + public func schedule(op: () async throws -> Void) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + let future$ = javaInterface_op$.apply() + _ = try future$?.dynamicJavaMethodCall(methodName: "get") + """ + ] + ) + } + + @Test + func asyncClosureWithParametersAndResult_javaBindings() throws { + let source = """ + public func compute(op: (Int64) async -> String) {} + """ + + try assertOutput( + input: source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class compute { + /** Corresponds to the Swift closure parameter of type {@code (Int64) async -> String}. */ + @FunctionalInterface + public interface op { + java.util.concurrent.CompletableFuture apply(long _0); + } + } + """ + ] + ) + } + + @Test + func asyncClosureWithParametersAndResult_swiftThunks() throws { + let source = """ + public func compute(op: (Int64) async -> String) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @JavaInterface("com.example.swift.SwiftModule$compute$op") + public struct JavaSwiftModule_compute_op { + @JavaMethod + public func apply(_ _0: Int64) -> JavaObject? + } + """, + """ + return { _0 in + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = javaInterface_op$.apply(_0) + let result$ = try? future$?.dynamicJavaMethodCall(methodName: "get", resultType: JavaObject?.self) + return String.fromJavaObject(result$?.javaThis, in: environment$) + } + """, + ] + ) + } } From cc84bf063a0132abb2888edb381396871794c69c Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Sun, 6 Sep 2026 18:53:23 +0530 Subject: [PATCH 2/4] jextract/jni: Add JavaCompletableFuture bindings and runtime tests for async closures - Add JavaCompletableFuture and JavaSimpleCompletableFuture bindings in SwiftJava with typed @JavaMethod get() matching JVM method descriptors - Generate JavaCompletableFuture return types on synthetic @JavaInterface closure wrappers - Lower async Swift closure invocation in native JNI thunk to futureTrue.get() and unbox the result via T.fromJavaObject - Add unit tests for async closures with primitive parameter and return types in JNIClosureTests - Add real JVM integration tests covering void, primitive (Long, Double), background-thread, and exceptional completions in SwiftJavaExtractJNISampleApp --- .../Sources/MySwiftLibrary/Async.swift | 29 ++++++-- .../java/com/example/swift/AsyncTest.java | 57 ++++++++++++++++ ...avaGenerator+EscapingClosureWrapJava.swift | 9 ++- ...wift2JavaGenerator+NativeTranslation.swift | 4 +- Sources/SwiftJava/JavaCompletableFuture.swift | 27 ++++++++ .../JNI/JNIClosureTests.swift | 66 +++++++++++++++++-- 6 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 Sources/SwiftJava/JavaCompletableFuture.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift index bb6e8aee6..5bfdcae6f 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -12,33 +12,48 @@ // //===----------------------------------------------------------------------===// -import SwiftJava // snippet.asyncDefinition +import SwiftJava + + +// snippet.end + + +// snippet.asyncClosureDefinition + + public func asyncSum(i1: Int64, i2: Int64) async -> Int64 { i1 + i2 } - public func asyncSleep() async throws { try await Task.sleep(for: .milliseconds(500)) } -// snippet.end - public func asyncCopy(myClass: MySwiftClass) async throws -> MySwiftClass { let new = MySwiftClass(x: myClass.x, y: myClass.y) try await Task.sleep(for: .milliseconds(500)) return new } - public func asyncOptional(i: Int64) async throws -> Int64? { try await Task.sleep(for: .milliseconds(100)) return i } - public func asyncThrows() async throws { throw MySwiftError.swiftError } - public func asyncString(input: String) async -> String { input } +public func asyncSchedule(op: @escaping () async -> Void) async { + await op() +} +public func asyncCompute(input: Int64, op: @escaping (Int64) async -> Int64) async -> Int64 { + await op(input) +} +public func asyncTransformDouble(input: Double, op: @escaping (Double) async -> Double) async -> Double { + await op(input) +} +public func asyncScheduleThrowing(op: @escaping () async throws -> Void) async throws { + try await op() +} +// snippet.end 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 1e9810954..ee7e64024 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.*; @@ -87,4 +88,60 @@ void asyncString() throws Exception { Future future = MySwiftLibrary.asyncString("hey"); assertEquals("hey", future.get()); } + + @Test + void asyncSchedule_voidClosure() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + Future future = MySwiftLibrary.asyncSchedule(() -> { + called.set(true); + return CompletableFuture.completedFuture(null); + }); + future.get(); + assertTrue(called.get(), "Async void closure should have been called"); + } + + @Test + void asyncCompute_primitiveLongClosure() throws Exception { + Future future = MySwiftLibrary.asyncCompute(21, (val) -> { + return CompletableFuture.completedFuture(val * 2); + }); + Long result = future.get(); + assertEquals(42L, result); + } + + @Test + void asyncCompute_backgroundThreadCompletion() throws Exception { + Future future = MySwiftLibrary.asyncCompute(10, (val) -> { + return CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return val + 15; + }); + }); + Long result = future.get(); + assertEquals(25L, result); + } + + @Test + void asyncTransformDouble_primitiveDoubleClosure() throws Exception { + Future future = MySwiftLibrary.asyncTransformDouble(3.5, (val) -> { + return CompletableFuture.completedFuture(val * 2.0); + }); + Double result = future.get(); + assertEquals(7.0, result, 0.001); + } + + @Test + void asyncScheduleThrowing_exceptionPropagates() { + Future future = MySwiftLibrary.asyncScheduleThrowing(() -> { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("Java async failed")); + return failed; + }); + + assertThrows(ExecutionException.class, future::get); + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index 9c12dec67..d7f2b38b5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -66,7 +66,14 @@ extension JNISwift2JavaGenerator { let paramList = "(\(params.joined(separator: .comma)))" if type.isAsync { - return "\(paramList) -> JavaObject?" + let futureType: String = + switch self.config.effectiveAsyncFuncMode { + case .completableFuture: + "JavaCompletableFuture?" + case .legacyFuture: + "JavaSimpleCompletableFuture?" + } + return "\(paramList) -> \(futureType)" } if type.resultType.isVoid { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index f758702db..86d473119 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1768,7 +1768,7 @@ extension JNISwift2JavaGenerator { let \(javaInterfaceVar) = \(syntheticFunction.javaInterfaceName)(javaThis: \(placeholder), environment: environment) return \(closureHeader) let future$ = \(upcallExpr) - _ = \(tryKeyword)future$?.dynamicJavaMethodCall(methodName: "get") + _ = \(tryKeyword)future$?.get() } }() """ @@ -1784,7 +1784,7 @@ extension JNISwift2JavaGenerator { return \(closureHeader) let environment$ = try! JavaVirtualMachine.shared().environment() let future$ = \(upcallExpr) - let result$ = \(tryKeyword)future$?.dynamicJavaMethodCall(methodName: "get", resultType: JavaObject?.self) + let result$ = \(tryKeyword)future$?.get() return \(fn.resultType.description).fromJavaObject(result$?.javaThis, in: environment$) } }() diff --git a/Sources/SwiftJava/JavaCompletableFuture.swift b/Sources/SwiftJava/JavaCompletableFuture.swift new file mode 100644 index 000000000..f975143a7 --- /dev/null +++ b/Sources/SwiftJava/JavaCompletableFuture.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaJNICore + +@JavaClass("java.util.concurrent.CompletableFuture") +open class JavaCompletableFuture: JavaObject { + @JavaMethod + public func get() throws -> JavaObject? +} + +@JavaClass("org.swift.swiftkit.core.SimpleCompletableFuture") +open class JavaSimpleCompletableFuture: JavaObject { + @JavaMethod + public func get() throws -> JavaObject? +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 9f36e69ed..19c0751af 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1236,7 +1236,7 @@ struct JNIClosureTests { @JavaInterface("com.example.swift.SwiftModule$schedule$op") public struct JavaSwiftModule_schedule_op { @JavaMethod - public func apply() -> JavaObject? + public func apply() -> JavaCompletableFuture? } """, """ @@ -1249,7 +1249,7 @@ struct JNIClosureTests { let javaInterface_op$ = JavaSwiftModule_schedule_op(javaThis: op, environment: environment) return { let future$ = javaInterface_op$.apply() - _ = try? future$?.dynamicJavaMethodCall(methodName: "get") + _ = try? future$?.get() } }() ) @@ -1273,7 +1273,7 @@ struct JNIClosureTests { expectedChunks: [ """ let future$ = javaInterface_op$.apply() - _ = try future$?.dynamicJavaMethodCall(methodName: "get") + _ = try future$?.get() """ ] ) @@ -1320,18 +1320,74 @@ struct JNIClosureTests { @JavaInterface("com.example.swift.SwiftModule$compute$op") public struct JavaSwiftModule_compute_op { @JavaMethod - public func apply(_ _0: Int64) -> JavaObject? + public func apply(_ _0: Int64) -> JavaCompletableFuture? } """, """ return { _0 in let environment$ = try! JavaVirtualMachine.shared().environment() let future$ = javaInterface_op$.apply(_0) - let result$ = try? future$?.dynamicJavaMethodCall(methodName: "get", resultType: JavaObject?.self) + let result$ = try? future$?.get() return String.fromJavaObject(result$?.javaThis, in: environment$) } """, ] ) } + + @Test + func asyncClosureWithPrimitiveParametersAndResult_javaBindings() throws { + let source = """ + public func computeInt(op: (Int64) async -> Int64) {} + """ + + try assertOutput( + input: source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class computeInt { + /** Corresponds to the Swift closure parameter of type {@code (Int64) async -> Int64}. */ + @FunctionalInterface + public interface op { + java.util.concurrent.CompletableFuture apply(long _0); + } + } + """ + ] + ) + } + + @Test + func asyncClosureWithPrimitiveParametersAndResult_swiftThunks() throws { + let source = """ + public func computeInt(op: (Int64) async -> Int64) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @JavaInterface("com.example.swift.SwiftModule$computeInt$op") + public struct JavaSwiftModule_computeInt_op { + @JavaMethod + public func apply(_ _0: Int64) -> JavaCompletableFuture? + } + """, + """ + return { _0 in + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = javaInterface_op$.apply(_0) + let result$ = try? future$?.get() + return Int64.fromJavaObject(result$?.javaThis, in: environment$) + } + """, + ] + ) + } } From c6c474b67c8c673943cb3a7088ab70e78eba7e95 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Mon, 7 Sep 2026 16:10:26 +0530 Subject: [PATCH 3/4] jextract/jni: Support optional return types and throwing in async closures - Translate async closure return types without leaking downcall out-parameter discriminators - Convert OptionalLong, OptionalInt, OptionalDouble, and Optional in Swift async closure thunks - Propagate exceptions from throwing async closures via try and fail-fast non-throwing closures with try! instead of swallowing errors with try? - Add unit tests for () async -> Int64? and (Int64) async throws -> String in JNIClosureTests - Add runtime integration tests in AsyncTest for optional returns and exceptional completion --- .../Sources/MySwiftLibrary/Async.swift | 18 +- .../java/com/example/swift/AsyncTest.java | 35 +++ .../com/example/swift/GenericTypeTest.java | 4 +- ...avaGenerator+EscapingClosureWrapJava.swift | 4 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 4 +- ...wift2JavaGenerator+NativeTranslation.swift | 39 +++- ...=> SwiftJavaSimpleCompletableFuture.swift} | 8 +- .../generated/JavaCompletableFuture.swift | 215 ++++++++++++++++++ Sources/SwiftJava/swift-java.config | 1 + .../JNI/JNIClosureTests.swift | 99 +++++++- 10 files changed, 398 insertions(+), 29 deletions(-) rename Sources/SwiftJava/{JavaCompletableFuture.swift => SwiftJavaSimpleCompletableFuture.swift} (75%) create mode 100644 Sources/SwiftJava/generated/JavaCompletableFuture.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift index 5bfdcae6f..c301a76a2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -13,22 +13,16 @@ //===----------------------------------------------------------------------===// -// snippet.asyncDefinition import SwiftJava - -// snippet.end - - -// snippet.asyncClosureDefinition - - +// snippet.asyncDefinition public func asyncSum(i1: Int64, i2: Int64) async -> Int64 { i1 + i2 } public func asyncSleep() async throws { try await Task.sleep(for: .milliseconds(500)) } +// snippet.end public func asyncCopy(myClass: MySwiftClass) async throws -> MySwiftClass { let new = MySwiftClass(x: myClass.x, y: myClass.y) try await Task.sleep(for: .milliseconds(500)) @@ -44,6 +38,8 @@ public func asyncThrows() async throws { public func asyncString(input: String) async -> String { input } + +// snippet.asyncClosureDefinition public func asyncSchedule(op: @escaping () async -> Void) async { await op() } @@ -56,4 +52,10 @@ public func asyncTransformDouble(input: Double, op: @escaping (Double) async -> public func asyncScheduleThrowing(op: @escaping () async throws -> Void) async throws { try await op() } +public func asyncFetchOptional(op: @escaping () async -> Int64?) async -> Int64? { + await op() +} +public func asyncComputeThrowing(input: Int64, op: @escaping (Int64) async throws -> Int64) async throws -> Int64 { + try await op(input) +} // snippet.end 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 ee7e64024..f7856072d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -144,4 +144,39 @@ void asyncScheduleThrowing_exceptionPropagates() { assertThrows(ExecutionException.class, future::get); } + + @Test + void asyncFetchOptional_present() throws Exception { + Future future = MySwiftLibrary.asyncFetchOptional(() -> { + return CompletableFuture.completedFuture(OptionalLong.of(99L)); + }); + assertEquals(OptionalLong.of(99L), future.get()); + } + + @Test + void asyncFetchOptional_empty() throws Exception { + Future future = MySwiftLibrary.asyncFetchOptional(() -> { + return CompletableFuture.completedFuture(OptionalLong.empty()); + }); + assertEquals(OptionalLong.empty(), future.get()); + } + + @Test + void asyncComputeThrowing_success() throws Exception { + Future future = MySwiftLibrary.asyncComputeThrowing(10, (val) -> { + return CompletableFuture.completedFuture(val + 5); + }); + assertEquals(15L, future.get()); + } + + @Test + void asyncComputeThrowing_exceptionPropagates() { + Future future = MySwiftLibrary.asyncComputeThrowing(10, (val) -> { + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("compute throwing failed")); + return failed; + }); + + assertThrows(ExecutionException.class, future::get); + } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java index e803c295b..8e1e5ef77 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/GenericTypeTest.java @@ -67,8 +67,8 @@ void genericEnum() { try (var arena = SwiftArena.ofConfined()) { GenericEnum value = MySwiftLibrary.makeIntGenericEnum(arena); switch (value.getCase()) { - case GenericEnum.Case.Foo _ -> assertTrue(value.getAsFoo().isPresent()); - case GenericEnum.Case.Bar _ -> assertTrue(value.getAsBar().isPresent()); + case GenericEnum.Case.Foo foo -> assertTrue(value.getAsFoo().isPresent()); + case GenericEnum.Case.Bar bar -> assertTrue(value.getAsBar().isPresent()); } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index d7f2b38b5..28d9850b5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -69,9 +69,9 @@ extension JNISwift2JavaGenerator { let futureType: String = switch self.config.effectiveAsyncFuncMode { case .completableFuture: - "JavaCompletableFuture?" + "JavaCompletableFuture?" case .legacyFuture: - "JavaSimpleCompletableFuture?" + "SwiftJavaSimpleCompletableFuture?" } return "\(paramList) -> \(futureType)" } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 4c856a24c..a7249dd89 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -279,8 +279,8 @@ extension JNISwift2JavaGenerator { javaType: futureType, nativeJavaType: futureType, annotations: translatedResult.annotations, - outParameters: translatedResult.outParameters, - conversion: translatedResult.conversion + outParameters: [], + conversion: .placeholder ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 86d473119..2ad391ca1 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1757,7 +1757,7 @@ extension JNISwift2JavaGenerator { : "{ \(closureParameters) in" if fn.isAsync { - let tryKeyword = fn.isThrowing ? "try " : "try? " + let tryKeyword = fn.isThrowing ? "try " : "try! " if isVoid { printer.print( """ @@ -1774,6 +1774,11 @@ extension JNISwift2JavaGenerator { """ ) } else { + let resultConversionExpr = Self.renderAsyncClosureResultConversion( + fn.resultType, + resultVar: "result$", + environmentVar: "environment$" + ) printer.print( """ { @@ -1785,7 +1790,7 @@ extension JNISwift2JavaGenerator { let environment$ = try! JavaVirtualMachine.shared().environment() let future$ = \(upcallExpr) let result$ = \(tryKeyword)future$?.get() - return \(fn.resultType.description).fromJavaObject(result$?.javaThis, in: environment$) + return \(resultConversionExpr) } }() """ @@ -2185,6 +2190,36 @@ extension JNISwift2JavaGenerator { return "" } } + + private static func renderAsyncClosureResultConversion( + _ resultType: SwiftType, + resultVar: String, + environmentVar: String + ) -> String { + switch resultType.asNominalType?.asKnownType { + case .optional(let wrapped): + switch wrapped.asNominalType?.asKnownType { + case .int64: + return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalLong.self))" + case .int32: + return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalInt.self))" + case .double: + return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalDouble.self))" + case .string: + return "Optional(javaOptional: \(resultVar)?.as(JavaOptional.self))" + default: + return "\(resultVar)?.as(\(wrapped.description).self)" + } + + case .int64, .int32, .int16, .int8, .int, + .uint64, .uint32, .uint16, .uint8, .uint, + .double, .float, .bool, .string: + return "\(resultType.description).fromJavaObject(\(resultVar)?.javaThis, in: \(environmentVar))" + + default: + return "\(resultVar)!.as(\(resultType.description).self)!" + } + } } enum NativeSwiftConversionCheck { diff --git a/Sources/SwiftJava/JavaCompletableFuture.swift b/Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift similarity index 75% rename from Sources/SwiftJava/JavaCompletableFuture.swift rename to Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift index f975143a7..233c42836 100644 --- a/Sources/SwiftJava/JavaCompletableFuture.swift +++ b/Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift @@ -14,14 +14,8 @@ import SwiftJavaJNICore -@JavaClass("java.util.concurrent.CompletableFuture") -open class JavaCompletableFuture: JavaObject { - @JavaMethod - public func get() throws -> JavaObject? -} - @JavaClass("org.swift.swiftkit.core.SimpleCompletableFuture") -open class JavaSimpleCompletableFuture: JavaObject { +open class SwiftJavaSimpleCompletableFuture: JavaObject { @JavaMethod public func get() throws -> JavaObject? } diff --git a/Sources/SwiftJava/generated/JavaCompletableFuture.swift b/Sources/SwiftJava/generated/JavaCompletableFuture.swift new file mode 100644 index 000000000..9653f197c --- /dev/null +++ b/Sources/SwiftJava/generated/JavaCompletableFuture.swift @@ -0,0 +1,215 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import SwiftJavaJNICore + +extension JavaClass { + /// Java method `allOf`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.allOf(java.util.concurrent.CompletableFuture...) + /// ``` + @JavaStaticMethod + public func allOf(_ arg0: [JavaCompletableFuture?]) -> JavaCompletableFuture! where ObjectType == JavaCompletableFuture + + /// Java method `anyOf`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.anyOf(java.util.concurrent.CompletableFuture...) + /// ``` + @JavaStaticMethod + public func anyOf(_ arg0: [JavaCompletableFuture?]) -> JavaCompletableFuture! where ObjectType == JavaCompletableFuture + + /// Java method `completedFuture`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.completedFuture(U) + /// ``` + @JavaStaticMethod + public func completedFuture(_ arg0: U?) -> JavaCompletableFuture! where ObjectType == JavaCompletableFuture + + /// Java method `failedFuture`. + /// + /// ### Java method signature + /// ```java + /// public static java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.failedFuture(java.lang.Throwable) + /// ``` + @JavaStaticMethod + public func failedFuture(_ arg0: Throwable?) -> JavaCompletableFuture! where ObjectType == JavaCompletableFuture +} +@JavaClass("java.util.concurrent.CompletableFuture") +open class JavaCompletableFuture: JavaObject { + public typealias T = JavaCompletableFuture_T + + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + + /// Java method `cancel`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.cancel(boolean) + /// ``` + @JavaMethod + open func cancel(_ arg0: Bool) -> Bool + + /// Java method `isCancelled`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.isCancelled() + /// ``` + @JavaMethod + open func isCancelled() -> Bool + + /// Java method `complete`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.complete(T) + /// ``` + @JavaMethod + open func complete(_ arg0: T?) -> Bool + + /// Java method `isCompletedExceptionally`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.isCompletedExceptionally() + /// ``` + @JavaMethod + open func isCompletedExceptionally() -> Bool + + /// Java method `completeExceptionally`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.completeExceptionally(java.lang.Throwable) + /// ``` + @JavaMethod + open func completeExceptionally(_ arg0: Throwable?) -> Bool + + /// Java method `copy`. + /// + /// ### Java method signature + /// ```java + /// public java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.copy() + /// ``` + @JavaMethod + open func copy() -> JavaCompletableFuture! + + /// Java method `isDone`. + /// + /// ### Java method signature + /// ```java + /// public boolean java.util.concurrent.CompletableFuture.isDone() + /// ``` + @JavaMethod + open func isDone() -> Bool + + /// Java method `exceptionNow`. + /// + /// ### Java method signature + /// ```java + /// public java.lang.Throwable java.util.concurrent.CompletableFuture.exceptionNow() + /// ``` + @JavaMethod + open func exceptionNow() -> Throwable! + + /// Java method `get`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.concurrent.CompletableFuture.get() throws java.lang.InterruptedException,java.util.concurrent.ExecutionException + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func get() throws -> T! + + /// Java method `join`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.concurrent.CompletableFuture.join() + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func join() -> T! + + /// Java method `newIncompleteFuture`. + /// + /// ### Java method signature + /// ```java + /// public java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.newIncompleteFuture() + /// ``` + @JavaMethod + open func newIncompleteFuture() -> JavaCompletableFuture! + + /// Java method `getNow`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.concurrent.CompletableFuture.getNow(T) + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func getNow(_ arg0: T?) -> T! + + /// Java method `getNumberOfDependents`. + /// + /// ### Java method signature + /// ```java + /// public int java.util.concurrent.CompletableFuture.getNumberOfDependents() + /// ``` + @JavaMethod + open func getNumberOfDependents() -> Int32 + + /// Java method `obtrudeException`. + /// + /// ### Java method signature + /// ```java + /// public void java.util.concurrent.CompletableFuture.obtrudeException(java.lang.Throwable) + /// ``` + @JavaMethod + open func obtrudeException(_ arg0: Throwable?) + + /// Java method `obtrudeValue`. + /// + /// ### Java method signature + /// ```java + /// public void java.util.concurrent.CompletableFuture.obtrudeValue(T) + /// ``` + @JavaMethod + open func obtrudeValue(_ arg0: T?) + + /// Java method `resultNow`. + /// + /// ### Java method signature + /// ```java + /// public T java.util.concurrent.CompletableFuture.resultNow() + /// ``` + @JavaMethod(typeErasedResult: "T!") + open func resultNow() -> T! + + /// Java method `toCompletableFuture`. + /// + /// ### Java method signature + /// ```java + /// public java.util.concurrent.CompletableFuture java.util.concurrent.CompletableFuture.toCompletableFuture() + /// ``` + @JavaMethod + open func toCompletableFuture() -> JavaCompletableFuture! + + /// Java method `toString`. + /// + /// ### Java method signature + /// ```java + /// public java.lang.String java.util.concurrent.CompletableFuture.toString() + /// ``` + @JavaMethod + open override func toString() -> String +} +extension JavaCompletableFuture { + @JavaInterface("java.util.concurrent.CompletableFuture$AsynchronousCompletionTask") + public struct AsynchronousCompletionTask { + + } +} diff --git a/Sources/SwiftJava/swift-java.config b/Sources/SwiftJava/swift-java.config index a38ad32fc..92128c88d 100644 --- a/Sources/SwiftJava/swift-java.config +++ b/Sources/SwiftJava/swift-java.config @@ -27,6 +27,7 @@ "java.util.OptionalDouble": "JavaOptionalDouble", "java.util.OptionalInt": "JavaOptionalInt", "java.util.OptionalLong": "JavaOptionalLong", + "java.util.concurrent.CompletableFuture": "JavaCompletableFuture", // Basic subset of collections API "java.util.Collection": "JavaCollection", diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 19c0751af..4630da29a 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1236,7 +1236,7 @@ struct JNIClosureTests { @JavaInterface("com.example.swift.SwiftModule$schedule$op") public struct JavaSwiftModule_schedule_op { @JavaMethod - public func apply() -> JavaCompletableFuture? + public func apply() -> JavaCompletableFuture? } """, """ @@ -1249,7 +1249,7 @@ struct JNIClosureTests { let javaInterface_op$ = JavaSwiftModule_schedule_op(javaThis: op, environment: environment) return { let future$ = javaInterface_op$.apply() - _ = try? future$?.get() + _ = try! future$?.get() } }() ) @@ -1320,14 +1320,45 @@ struct JNIClosureTests { @JavaInterface("com.example.swift.SwiftModule$compute$op") public struct JavaSwiftModule_compute_op { @JavaMethod - public func apply(_ _0: Int64) -> JavaCompletableFuture? + public func apply(_ _0: Int64) -> JavaCompletableFuture? } """, """ return { _0 in let environment$ = try! JavaVirtualMachine.shared().environment() let future$ = javaInterface_op$.apply(_0) - let result$ = try? future$?.get() + let result$ = try! future$?.get() + return String.fromJavaObject(result$?.javaThis, in: environment$) + } + """, + ] + ) + } + + @Test + func asyncThrowingClosureWithParametersAndResult_swiftThunks() throws { + let source = """ + public func computeThrowing(op: (Int64) async throws -> String) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @JavaInterface("com.example.swift.SwiftModule$computeThrowing$op") + public struct JavaSwiftModule_computeThrowing_op { + @JavaMethod + public func apply(_ _0: Int64) -> JavaCompletableFuture? + } + """, + """ + return { _0 in + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = javaInterface_op$.apply(_0) + let result$ = try future$?.get() return String.fromJavaObject(result$?.javaThis, in: environment$) } """, @@ -1376,18 +1407,74 @@ struct JNIClosureTests { @JavaInterface("com.example.swift.SwiftModule$computeInt$op") public struct JavaSwiftModule_computeInt_op { @JavaMethod - public func apply(_ _0: Int64) -> JavaCompletableFuture? + public func apply(_ _0: Int64) -> JavaCompletableFuture? } """, """ return { _0 in let environment$ = try! JavaVirtualMachine.shared().environment() let future$ = javaInterface_op$.apply(_0) - let result$ = try? future$?.get() + let result$ = try! future$?.get() return Int64.fromJavaObject(result$?.javaThis, in: environment$) } """, ] ) } + + @Test + func asyncClosureReturningOptional_javaBindings() throws { + let source = """ + public func fetchOptional(op: () async -> Int64?) {} + """ + + try assertOutput( + input: source, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static class fetchOptional { + /** Corresponds to the Swift closure parameter of type {@code () async -> Int64?}. */ + @FunctionalInterface + public interface op { + java.util.concurrent.CompletableFuture apply(); + } + } + """ + ] + ) + } + + @Test + func asyncClosureReturningOptional_swiftThunks() throws { + let source = """ + public func fetchOptional(op: () async -> Int64?) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @JavaInterface("com.example.swift.SwiftModule$fetchOptional$op") + public struct JavaSwiftModule_fetchOptional_op { + @JavaMethod + public func apply() -> JavaCompletableFuture? + } + """, + """ + return { + let environment$ = try! JavaVirtualMachine.shared().environment() + let future$ = javaInterface_op$.apply() + let result$ = try! future$?.get() + return Optional(javaOptional: result$?.as(JavaOptionalLong.self)) + } + """, + ] + ) + } } From 9f0486dc655c22c553fb3ffc5a3d86d2d2af5592 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Mon, 7 Sep 2026 16:51:08 +0530 Subject: [PATCH 4/4] jextract/jni: Introduce UpcallConversionStep.awaitFutureResult for async closures - Add awaitFutureResult case to UpcallConversionStep to unify sync and async closure result lowering - Guard that the returned future is non-nil and unbox primitive, optional, and object results - Unify escapingClosureLowering in native translation by delegating async closure awaiting to UpcallConversionStep - Update unit tests in JNIClosureTests to assert the non-nil future guard and awaiting logic --- ...Generator+InterfaceWrapperGeneration.swift | 60 ++++++++++++++- ...wift2JavaGenerator+NativeTranslation.swift | 75 ------------------- .../JNI/JNIClosureTests.swift | 36 ++++++--- 3 files changed, 83 insertions(+), 88 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..253f366b2 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -154,11 +154,19 @@ extension JNISwift2JavaGenerator { ) } - let resultConversion = try self.translateResult( + var resultConversion = try self.translateResult( type: functionType.resultType, methodName: "apply" ) + if functionType.isAsync { + resultConversion = .awaitFutureResult( + .placeholder, + resultType: functionType.resultType, + isThrowing: functionType.isThrowing + ) + } + return SyntheticEscapingClosureFunctionType( javaInterfaceName: javaInterfaceName, javaBinaryName: javaBinaryName, @@ -392,6 +400,12 @@ enum UpcallConversionStep { indirect case map(UpcallConversionStep, body: UpcallConversionStep) + indirect case awaitFutureResult( + UpcallConversionStep, + resultType: SwiftType, + isThrowing: Bool + ) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout SwiftPrinter, _ placeholder: String) -> String { switch self { @@ -455,6 +469,50 @@ enum UpcallConversionStep { printer.print("return \(body)") } return printer.finalize() + + case .awaitFutureResult(let inner, let resultType, let isThrowing): + let future = inner.render(&printer, placeholder) + let tryKeyword = isThrowing ? "try " : "try! " + printer.print( + """ + guard let future$ = \(future) else { + fatalError("Async closure upcall to apply returned a nil future") + } + """ + ) + if resultType.isVoid { + printer.print("_ = \(tryKeyword)future$.get()") + return "" + } + printer.print( + """ + let environment$ = try! JavaVirtualMachine.shared().environment() + let result$ = \(tryKeyword)future$.get() + """ + ) + switch resultType.asNominalType?.asKnownType { + case .optional(let wrapped): + switch wrapped.asNominalType?.asKnownType { + case .int64: + return "Optional(javaOptional: result$?.as(JavaOptionalLong.self))" + case .int32: + return "Optional(javaOptional: result$?.as(JavaOptionalInt.self))" + case .double: + return "Optional(javaOptional: result$?.as(JavaOptionalDouble.self))" + case .string: + return "Optional(javaOptional: result$?.as(JavaOptional.self))" + default: + return "result$?.as(\(wrapped.description).self)" + } + + case .int64, .int32, .int16, .int8, .int, + .uint64, .uint32, .uint16, .uint8, .uint, + .double, .float, .bool, .string: + return "\(resultType.description).fromJavaObject(result$?.javaThis, in: environment$)" + + default: + return "result$!.as(\(resultType.description).self)!" + } } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 2ad391ca1..2caaacd12 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1755,51 +1755,6 @@ extension JNISwift2JavaGenerator { fn.parameters.isEmpty ? "{" : "{ \(closureParameters) in" - - if fn.isAsync { - let tryKeyword = fn.isThrowing ? "try " : "try! " - if isVoid { - printer.print( - """ - { - guard let \(placeholder) else { - fatalError("\(placeholder) is null") - } - let \(javaInterfaceVar) = \(syntheticFunction.javaInterfaceName)(javaThis: \(placeholder), environment: environment) - return \(closureHeader) - let future$ = \(upcallExpr) - _ = \(tryKeyword)future$?.get() - } - }() - """ - ) - } else { - let resultConversionExpr = Self.renderAsyncClosureResultConversion( - fn.resultType, - resultVar: "result$", - environmentVar: "environment$" - ) - printer.print( - """ - { - guard let \(placeholder) else { - fatalError("\(placeholder) is null") - } - let \(javaInterfaceVar) = \(syntheticFunction.javaInterfaceName)(javaThis: \(placeholder), environment: environment) - return \(closureHeader) - let environment$ = try! JavaVirtualMachine.shared().environment() - let future$ = \(upcallExpr) - let result$ = \(tryKeyword)future$?.get() - return \(resultConversionExpr) - } - }() - """ - ) - } - - return printer.finalize() - } - let resultConverted = syntheticFunction.resultConversion.render(&resultPrinter, upcallExpr) let resultPrefix = resultPrinter.finalize() @@ -2190,36 +2145,6 @@ extension JNISwift2JavaGenerator { return "" } } - - private static func renderAsyncClosureResultConversion( - _ resultType: SwiftType, - resultVar: String, - environmentVar: String - ) -> String { - switch resultType.asNominalType?.asKnownType { - case .optional(let wrapped): - switch wrapped.asNominalType?.asKnownType { - case .int64: - return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalLong.self))" - case .int32: - return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalInt.self))" - case .double: - return "Optional(javaOptional: \(resultVar)?.as(JavaOptionalDouble.self))" - case .string: - return "Optional(javaOptional: \(resultVar)?.as(JavaOptional.self))" - default: - return "\(resultVar)?.as(\(wrapped.description).self)" - } - - case .int64, .int32, .int16, .int8, .int, - .uint64, .uint32, .uint16, .uint8, .uint, - .double, .float, .bool, .string: - return "\(resultType.description).fromJavaObject(\(resultVar)?.javaThis, in: \(environmentVar))" - - default: - return "\(resultVar)!.as(\(resultType.description).self)!" - } - } } enum NativeSwiftConversionCheck { diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 4630da29a..478761c08 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1248,8 +1248,10 @@ struct JNIClosureTests { } let javaInterface_op$ = JavaSwiftModule_schedule_op(javaThis: op, environment: environment) return { - let future$ = javaInterface_op$.apply() - _ = try! future$?.get() + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } + _ = try! future$.get() } }() ) @@ -1272,8 +1274,10 @@ struct JNIClosureTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - let future$ = javaInterface_op$.apply() - _ = try future$?.get() + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } + _ = try future$.get() """ ] ) @@ -1325,9 +1329,11 @@ struct JNIClosureTests { """, """ return { _0 in + guard let future$ = javaInterface_op$.apply(_0) else { + fatalError("Async closure upcall to apply returned a nil future") + } let environment$ = try! JavaVirtualMachine.shared().environment() - let future$ = javaInterface_op$.apply(_0) - let result$ = try! future$?.get() + let result$ = try! future$.get() return String.fromJavaObject(result$?.javaThis, in: environment$) } """, @@ -1356,9 +1362,11 @@ struct JNIClosureTests { """, """ return { _0 in + guard let future$ = javaInterface_op$.apply(_0) else { + fatalError("Async closure upcall to apply returned a nil future") + } let environment$ = try! JavaVirtualMachine.shared().environment() - let future$ = javaInterface_op$.apply(_0) - let result$ = try future$?.get() + let result$ = try future$.get() return String.fromJavaObject(result$?.javaThis, in: environment$) } """, @@ -1412,9 +1420,11 @@ struct JNIClosureTests { """, """ return { _0 in + guard let future$ = javaInterface_op$.apply(_0) else { + fatalError("Async closure upcall to apply returned a nil future") + } let environment$ = try! JavaVirtualMachine.shared().environment() - let future$ = javaInterface_op$.apply(_0) - let result$ = try! future$?.get() + let result$ = try! future$.get() return Int64.fromJavaObject(result$?.javaThis, in: environment$) } """, @@ -1468,9 +1478,11 @@ struct JNIClosureTests { """, """ return { + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } let environment$ = try! JavaVirtualMachine.shared().environment() - let future$ = javaInterface_op$.apply() - let result$ = try! future$?.get() + let result$ = try! future$.get() return Optional(javaOptional: result$?.as(JavaOptionalLong.self)) } """,