diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift index bb6e8aee6..c301a76a2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Async.swift @@ -12,33 +12,50 @@ // //===----------------------------------------------------------------------===// + import SwiftJava // 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)) 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 } + +// snippet.asyncClosureDefinition +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() +} +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 1e9810954..f7856072d 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,95 @@ 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); + } + + @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 6b6a4e729..28d9850b5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -65,6 +65,17 @@ extension JNISwift2JavaGenerator { } let paramList = "(\(params.joined(separator: .comma)))" + if type.isAsync { + let futureType: String = + switch self.config.effectiveAsyncFuncMode { + case .completableFuture: + "JavaCompletableFuture?" + case .legacyFuture: + "SwiftJavaSimpleCompletableFuture?" + } + return "\(paramList) -> \(futureType)" + } + if type.resultType.isVoid { return paramList } else { 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+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index f3e6e5189..a7249dd89 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: [], + conversion: .placeholder + ) + } 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..2caaacd12 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. @@ -1759,6 +1755,8 @@ extension JNISwift2JavaGenerator { fn.parameters.isEmpty ? "{" : "{ \(closureParameters) in" + 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. 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/Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift b/Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift new file mode 100644 index 000000000..233c42836 --- /dev/null +++ b/Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// 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("org.swift.swiftkit.core.SimpleCompletableFuture") +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 4ef90684c..478761c08 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1159,4 +1159,334 @@ 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() -> JavaCompletableFuture? + } + """, + """ + @_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 { + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } + _ = try! future$.get() + } + }() + ) + } + """, + ] + ) + } + + @Test + func asyncThrowingClosure_swiftThunks() throws { + let source = """ + public func schedule(op: () async throws -> Void) {} + """ + + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } + _ = try future$.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) -> JavaCompletableFuture? + } + """, + """ + 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 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 + 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 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 + 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 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 { + guard let future$ = javaInterface_op$.apply() else { + fatalError("Async closure upcall to apply returned a nil future") + } + let environment$ = try! JavaVirtualMachine.shared().environment() + let result$ = try! future$.get() + return Optional(javaOptional: result$?.as(JavaOptionalLong.self)) + } + """, + ] + ) + } }