From dad750087acace6b37f3ed5f0314f9b1356ace5e Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Mon, 31 Aug 2026 00:04:39 +0530 Subject: [PATCH 1/4] feat(ffm): add support for async Swift functions via CompletableFuture --- .../MySwiftLibrary/MySwiftLibrary.swift | 13 ++ .../com/example/swift/MySwiftLibraryTest.java | 29 +++ Snippets/AsyncJavaFFM.java | 1 + .../FFM/CDeclLowering/CRepresentation.swift | 17 +- ...Swift2JavaGenerator+FunctionLowering.swift | 187 ++++++++++++++---- ...t2JavaGenerator+JavaBindingsPrinting.swift | 76 +++++-- ...MSwift2JavaGenerator+JavaTranslation.swift | 43 +++- .../Documentation.docc/FeaturesJextract.md | 4 +- .../FFM/FFMAsyncTests.swift | 141 +++++++++++++ 9 files changed, 450 insertions(+), 61 deletions(-) create mode 120000 Snippets/AsyncJavaFFM.java create mode 100644 Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index a0fd01681..8c63359ed 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -163,6 +163,19 @@ public func globalThrowingString(doThrow: Bool) throws -> String { return "Hello from throwing Swift!" } +// ==== ----------------------------------------------------------------------- +// MARK: Async functions + +public func asyncSum(a: Int64, b: Int64) async -> Int64 { + a + b +} + +public func asyncThrowsVoid(doThrow: Bool) async throws { + if doThrow { + throw SwiftExampleError(message: "expected error in asyncThrowsVoid") + } +} + // ==== ----------------------------------------------------------------------- // MARK: Overloaded functions diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index cb60b1c62..6e1126dd7 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -190,6 +190,35 @@ void call_globalCallMeDoubleSupplier_noThrow() { assertEquals(2.0, result); } + // ==== ---------------------------------------------------------------- + // Async functions + + @Test + void call_asyncSum() throws Exception { + // snippet.asyncUsageJava + java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); + Long result = future.get(); + assertEquals(22, result); + // snippet.end + } + + @Test + void call_asyncThrowsVoid_noThrow() throws Exception { + java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(false); + future.get(); // Should complete normally + } + + @Test + void call_asyncThrowsVoid_throws() { + java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(true); + java.util.concurrent.ExecutionException ex = assertThrows(java.util.concurrent.ExecutionException.class, future::get); + + Throwable cause = ex.getCause(); + assertNotNull(cause); + assertTrue(cause instanceof SwiftJavaErrorException); + assertTrue(cause.getMessage().contains("expected error in asyncThrowsVoid")); + } + @Test void call_globalCallMeIntConsumer_noThrow() { MySwiftLibrary.globalCallMeIntConsumer((int a) -> { }); diff --git a/Snippets/AsyncJavaFFM.java b/Snippets/AsyncJavaFFM.java new file mode 120000 index 000000000..eb9abb694 --- /dev/null +++ b/Snippets/AsyncJavaFFM.java @@ -0,0 +1 @@ +../Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 727f226e3..780b25f1a 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -33,9 +33,20 @@ extension CType { } switch knownType { - case .optional(let wrapped) where wrapped.isPointer: - try self.init(cdeclType: wrapped) - return + case .optional(let wrapped): + let isNullableInC: Bool + if wrapped.isPointer { + isNullableInC = true + } else if case .function(let fn) = wrapped, fn.convention == .c { + isNullableInC = true + } else { + isNullableInC = false + } + + if isNullableInC { + try self.init(cdeclType: wrapped) + return + } case .unsafePointer(let pointee): self = .pointer( diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 20319b5c2..8f274a0ab 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -123,21 +123,22 @@ struct CdeclLowering { } var isThrowing = false + var isAsync = false for effect in signature.effectSpecifiers { switch effect { case .throws: isThrowing = true case .async: - throw LoweringError.effectNotSupported(effect) + isAsync = true } } // Lower the result. - let loweredResult = try lowerResult(signature.result.type) + var loweredResult = try lowerResult(signature.result.type) - // If the function throws, create an error out parameter + // If the function throws (and isn't async), create an error out parameter let errorOutParameter: LoweredParameter? = - if isThrowing { + if isThrowing && !isAsync { LoweredParameter( cdeclParameters: [ SwiftParameter( @@ -152,10 +153,62 @@ struct CdeclLowering { nil } + let asyncCompletionOutParameter: LoweredParameter? + let asyncErrorOutParameter: LoweredParameter? + + if isAsync { + let completionType = SwiftFunctionType( + convention: .c, + parameters: [ + SwiftParameter(convention: .byValue, type: loweredResult.cdeclResultType) + ], + resultType: .tuple([]) + ) + asyncCompletionOutParameter = LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "async$completion", + type: .function(completionType) + ) + ], + conversion: .placeholder + ) + + if isThrowing { + let errorCompletionType = SwiftFunctionType( + convention: .c, + parameters: [ + SwiftParameter(convention: .byValue, type: knownTypes.unsafePointer(knownTypes.int8)) + ], + resultType: .tuple([]) + ) + asyncErrorOutParameter = LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "async$error", + type: .function(errorCompletionType) + ) + ], + conversion: .placeholder + ) + } else { + asyncErrorOutParameter = nil + } + + loweredResult.cdeclResultType = .tuple([]) + } else { + asyncCompletionOutParameter = nil + asyncErrorOutParameter = nil + } + // When throwing with a non-void pointer return, make the return type // optional so the catch block can return nil (nullable pointer in C) let cdeclReturnTypeForThunk: SwiftType - if isThrowing && loweredResult.cdeclResultType.isPointer { + if isAsync { + cdeclReturnTypeForThunk = .tuple([]) + } else if isThrowing && loweredResult.cdeclResultType.isPointer { cdeclReturnTypeForThunk = knownTypes.optionalSugar(loweredResult.cdeclResultType) } else { cdeclReturnTypeForThunk = loweredResult.cdeclResultType @@ -167,6 +220,8 @@ struct CdeclLowering { parameters: loweredParameters, result: loweredResult, errorOutParameter: errorOutParameter, + asyncCompletionOutParameter: asyncCompletionOutParameter, + asyncErrorOutParameter: asyncErrorOutParameter, cdeclReturnTypeForThunk: cdeclReturnTypeForThunk, ) } @@ -928,13 +983,16 @@ public struct LoweredFunctionSignature: Equatable { var parameters: [LoweredParameter] var result: LoweredResult var errorOutParameter: LoweredParameter? + var asyncCompletionOutParameter: LoweredParameter? + var asyncErrorOutParameter: LoweredParameter? /// The cdecl return type for the thunk. When the function is throwing and /// returns a pointer, this is the optional-wrapped version of /// `result.cdeclResultType` so the catch block can return nil var cdeclReturnTypeForThunk: SwiftType - var isThrowing: Bool { errorOutParameter != nil } + var isThrowing: Bool { errorOutParameter != nil || asyncErrorOutParameter != nil } + var isAsync: Bool { asyncCompletionOutParameter != nil } var allLoweredParameters: [SwiftParameter] { var all: [SwiftParameter] = [] @@ -952,6 +1010,12 @@ public struct LoweredFunctionSignature: Equatable { if let errorOutParameter { all += errorOutParameter.cdeclParameters } + if let asyncCompletionOutParameter { + all += asyncCompletionOutParameter.cdeclParameters + } + if let asyncErrorOutParameter { + all += asyncErrorOutParameter.cdeclParameters + } return all } @@ -1098,45 +1162,100 @@ extension LoweredFunctionSignature { resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } - // Lower the result. let tryKeyword: String = isThrowing ? "try " : "" - if !original.result.type.isVoid { - let loweredResult: ExprSyntax? = result.conversion.asExprSyntax( - placeholder: resultExpr.description, - bodyItems: &bodyItems, - ) + let awaitKeyword: String = isAsync ? "await " : "" + + if isAsync { + var taskBodyItems: [CodeBlockItemSyntax] = [] + + if !original.result.type.isVoid { + // Use a temporary variable to hold the async result before conversion, since conversion + // might assume a simple placeholder name or expression. + taskBodyItems.append("let async$result = \(raw: tryKeyword)\(raw: awaitKeyword)\(resultExpr)") + + let loweredResult: ExprSyntax? = result.conversion.asExprSyntax( + placeholder: "async$result", + bodyItems: &taskBodyItems, + ) - if let loweredResult { - let returnKeyword = !result.cdeclResultType.isVoid ? "return " : "" - bodyItems.append("\(raw: returnKeyword)\(raw: tryKeyword)\(loweredResult)") + if let loweredResult { + taskBodyItems.append("async$completion(\(loweredResult))") + } + } else { + taskBodyItems.append("\(raw: tryKeyword)\(raw: awaitKeyword)\(resultExpr)") + taskBodyItems.append("async$completion()") } - } else { - bodyItems.append("\(raw: tryKeyword)\(resultExpr)") - } - // If throwing, wrap body in do/catch. - if isThrowing { - let doBody = bodyItems.map { item in - item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) + if isThrowing { + let doBody = taskBodyItems.map { item in + item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) + } + + let doStmt: StmtSyntax = """ + do {\(CodeBlockItemListSyntax(doBody)) + } catch { + let errorString = String(describing: error) + errorString.withCString { errorCString in + async$error(errorCString) + } + } + """ + taskBodyItems = [ + CodeBlockItemSyntax(item: .stmt(doStmt)) + ] } - let dummyReturnStmt: String - if !result.cdeclResultType.isVoid { - let dummyReturn = result.cdeclResultType.isPointer ? "nil" : "0" - dummyReturnStmt = "\n return \(dummyReturn)" - } else { - dummyReturnStmt = "" + let taskBody = taskBodyItems.map { item in + item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) } - let doStmt: StmtSyntax = """ - do {\(CodeBlockItemListSyntax(doBody)) - } catch { - result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque()\(raw: dummyReturnStmt) - } + + let taskExpr: ExprSyntax = """ + Task.immediate {\(CodeBlockItemListSyntax(taskBody)) + } """ bodyItems = [ - CodeBlockItemSyntax(item: .stmt(doStmt)) + CodeBlockItemSyntax(item: .expr(taskExpr)) ] + } else { + if !original.result.type.isVoid { + let loweredResult: ExprSyntax? = result.conversion.asExprSyntax( + placeholder: resultExpr.description, + bodyItems: &bodyItems, + ) + + if let loweredResult { + let returnKeyword = !result.cdeclResultType.isVoid ? "return " : "" + bodyItems.append("\(raw: returnKeyword)\(raw: tryKeyword)\(loweredResult)") + } + } else { + bodyItems.append("\(raw: tryKeyword)\(resultExpr)") + } + + // If throwing, wrap body in do/catch. + if isThrowing { + let doBody = bodyItems.map { item in + item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) + } + + let dummyReturnStmt: String + if !result.cdeclResultType.isVoid { + let dummyReturn = result.cdeclResultType.isPointer ? "nil" : "0" + dummyReturnStmt = "\n return \(dummyReturn)" + } else { + dummyReturnStmt = "" + } + let doStmt: StmtSyntax = """ + do {\(CodeBlockItemListSyntax(doBody)) + } catch { + result$throws.pointee = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque()\(raw: dummyReturnStmt) + } + """ + + bodyItems = [ + CodeBlockItemSyntax(item: .stmt(doStmt)) + ] + } } loweredCDecl.body!.statements = CodeBlockItemListSyntax { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 46ddc38bb..b0094a155 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -332,24 +332,33 @@ extension FFMSwift2JavaGenerator { ) } else { // Otherwise, the lambda must be wrapped with the lowered function instance. - let apiParams = functionType.parameters.map { - "\($0.parameter.type) \($0.parameter.name)" - } + let (interfaceName, isKnownFuncInterface) = + if let known = KnownJavaFunctionalInterface.find(functionType) { + (known.javaType.description, true) + } else { + (functionType.name, false) + } - printer.print( - """ - @FunctionalInterface - public interface \(functionType.name) { - \(functionType.result.javaResultType) apply(\(apiParams.joined(separator: .comma))); + if !isKnownFuncInterface { + let apiParams = functionType.parameters.map { + "\($0.parameter.type) \($0.parameter.name)" } - """ - ) + + printer.print( + """ + @FunctionalInterface + public interface \(interfaceName) { + \(functionType.result.javaResultType) apply(\(apiParams.joined(separator: .comma))); + } + """ + ) + } let cdeclParams = functionType.cdeclType.parameters.map({ "\($0.parameterName!)" }) printer.printBraceBlock( """ - private static MemorySegment $toUpcallStub(\(functionType.name) fi, Arena arena) + private static MemorySegment $toUpcallStub(\(interfaceName) fi, Arena arena) """ ) { printer in printer.print( @@ -364,7 +373,8 @@ extension FFMSwift2JavaGenerator { convertedArgs.append(arg) } - let call = "fi.apply(\(convertedArgs.joined(separator: .comma)))" + let methodName = isKnownFuncInterface ? KnownJavaFunctionalInterface.find(functionType)!.method : "apply" + let call = "fi.\(methodName)(\(convertedArgs.joined(separator: .comma)))" let result = functionType.result.conversion.render(&printer, call) if functionType.result.javaResultType == .void { printer.print("\(result);") @@ -505,8 +515,43 @@ extension FFMSwift2JavaGenerator { ) } + if translatedSignature.isAsync { + printer.print("java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture();") + + let completionName = "$async$completion" + printer.print("MemorySegment \(completionName) = \(thunkName).\(completionName).toUpcallStub((result$) -> {") + printer.indent() + if translatedSignature.result.javaResultType == .void || translatedSignature.result.javaResultType == .completableFuture(.void) { + printer.print("future$.complete(null);") + } else { + let result = translatedSignature.result.conversion.render( + &printer, + "result$", + placeholderForDowncall: nil + ) + printer.print("future$.complete(\(result));") + } + printer.outdent() + printer.print("}, Arena.ofAuto());") + downCallArguments.append(completionName) + + if translatedSignature.isThrowing { + let errorName = "$async$error" + printer.print("MemorySegment \(errorName) = \(thunkName).\(errorName).toUpcallStub((error$) -> {") + printer.indent() + printer.print("if (!error$.equals(MemorySegment.NULL)) {") + printer.indent() + printer.print("future$.completeExceptionally(new \(JavaType.swiftJavaErrorException.className!)(error$, AllocatingSwiftArena.ofAuto()));") + printer.outdent() + printer.print("}") + printer.outdent() + printer.print("}, Arena.ofAuto());") + downCallArguments.append(errorName) + } + } + // Error out parameter for throwing functions. - if translatedSignature.isThrowing { + if translatedSignature.isThrowing && !translatedSignature.isAsync { printer.print("MemorySegment result$throws = arena$.allocate(ValueLayout.ADDRESS);") printer.print("result$throws.set(ValueLayout.ADDRESS, 0, MemorySegment.NULL);") downCallArguments.append("result$throws") @@ -548,7 +593,10 @@ extension FFMSwift2JavaGenerator { } //=== Part 4: Convert the return value. - if translatedSignature.result.javaResultType == .void { + if translatedSignature.isAsync { + printer.print("\(downCall);") + printer.print("return future$;") + } else if translatedSignature.result.javaResultType == .void { // Trivial downcall with no conversion needed, no callback either printer.print("\(downCall);") printErrorCheck(&printer) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 7b6cb7e56..d54edf35c 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -131,6 +131,7 @@ extension FFMSwift2JavaGenerator { var parameters: [TranslatedParameter] var result: TranslatedResult var isThrowing: Bool = false + var isAsync: Bool = false /// Whether any parameter or the result requires a 32-bit integer overflow check, /// which means the Java method must declare `throws SwiftIntegerOverflowException` @@ -328,17 +329,22 @@ extension FFMSwift2JavaGenerator { } // Result. - let result = try self.translateResult( + var result = try self.translateResult( swiftResult: swiftSignature.result, loweredResult: loweredFunctionSignature.result, methodName: methodName ) + if loweredFunctionSignature.isAsync { + result.javaResultType = .completableFuture(result.javaResultType) + } + return TranslatedFunctionSignature( selfParameter: selfParameter, parameters: parameters, result: result, - isThrowing: loweredFunctionSignature.isThrowing + isThrowing: loweredFunctionSignature.isThrowing, + isAsync: loweredFunctionSignature.isAsync ) } @@ -1065,9 +1071,17 @@ extension CType { return inner.javaType case .tag(_): - fatalError("unsupported") - case .integral(.signed(bits: _)), .integral(.unsigned(bits: _)): - fatalError("unreachable") + return .javaForeignMemorySegment + case .integral(.signed(bits: let bits)): + if bits <= 8 { return .byte } + if bits <= 16 { return .short } + if bits <= 32 { return .int } + return .long + case .integral(.unsigned(bits: let bits)): + if bits <= 8 { return .byte } + if bits <= 16 { return .char } + if bits <= 32 { return .int } + return .long } } @@ -1098,9 +1112,22 @@ extension CType { return inner.foreignValueLayout case .tag(_): - fatalError("unsupported") - case .void, .integral(.signed(bits: _)), .integral(.unsigned(bits: _)): - fatalError("unreachable") + return .SwiftPointer + + case .void: + return .SwiftPointer + + case .integral(.signed(bits: let bits)): + if bits <= 8 { return .SwiftInt8 } + if bits <= 16 { return .SwiftInt16 } + if bits <= 32 { return .SwiftInt32 } + return .SwiftInt64 + + case .integral(.unsigned(bits: let bits)): + if bits <= 8 { return .SwiftUInt8 } + if bits <= 16 { return .SwiftUInt16 } + if bits <= 32 { return .SwiftUInt32 } + return .SwiftUInt64 } } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index 5b5802b75..f48b45edc 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -195,8 +195,8 @@ There are two modes of extracting them, configurable using the `asyncFuncMode` s @Tab("Java (JNI)") { @Snippet(path: "Snippets/AsyncJavaJNI", slice: "asyncUsageJava") } - @Tab("Java (FFM): not supported") { - @Snippet(path: "Snippets/NotSupportedYetJavaFFM", slice: "notSupportedYet") + @Tab("Java (FFM)") { + @Snippet(path: "Snippets/AsyncJavaFFM", slice: "asyncUsageJava") } } diff --git a/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift new file mode 100644 index 000000000..9a9c1395b --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 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 JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +@Suite +struct FFMAsyncTests { + + @Test("Import: async -> Void (Java, CompletableFuture)") + func completableFuture_asyncVoid_java() throws { + try assertOutput( + input: "public func asyncVoid() async", + .ffm, + .java, + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_asyncVoid(void **$async$completion, void **$async$error) + * } + */ + private static class swiftjava_SwiftModule_asyncVoid { + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func asyncVoid() async + * } + */ + public static java.util.concurrent.CompletableFuture asyncVoid() { + try (var arena$ = org.swift.swiftkit.core.AllocatingSwiftArena.ofConfined()) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + java.lang.foreign.MemorySegment $async$completion = swiftjava_SwiftModule_asyncVoid.$async$completion.toUpcallStub((result$) -> { + future$.complete(null); + }, java.lang.foreign.Arena.ofAuto()); + swiftjava_SwiftModule_asyncVoid.call($async$completion); + return future$; + } + } + """, + ] + ) + } + + @Test("Import: async -> Void (Swift, CompletableFuture)") + func completableFuture_asyncVoid_swift() throws { + try assertOutput( + input: "public func asyncVoid() async", + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_asyncVoid") + public func swiftjava_SwiftModule_asyncVoid(_ $async$completion: @convention(c) () -> Void) { + Task.immediate { + await asyncVoid() + $async$completion() + } + } + """ + ] + ) + } + + @Test("Import: async throws -> Void (Java, CompletableFuture)") + func completableFuture_asyncThrowsVoid_java() throws { + try assertOutput( + input: "public func asyncThrowsVoid() async throws", + .ffm, + .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func asyncThrowsVoid() async throws + * } + */ + public static java.util.concurrent.CompletableFuture asyncThrowsVoid() { + try (var arena$ = org.swift.swiftkit.core.AllocatingSwiftArena.ofConfined()) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + java.lang.foreign.MemorySegment $async$completion = swiftjava_SwiftModule_asyncThrowsVoid.$async$completion.toUpcallStub((result$) -> { + future$.complete(null); + }, java.lang.foreign.Arena.ofAuto()); + java.lang.foreign.MemorySegment $async$error = swiftjava_SwiftModule_asyncThrowsVoid.$async$error.toUpcallStub((error$) -> { + if (!error$.equals(java.lang.foreign.MemorySegment.NULL)) { + future$.completeExceptionally(new org.swift.swiftkit.ffm.generated.SwiftJavaErrorException(error$, org.swift.swiftkit.core.AllocatingSwiftArena.ofAuto())); + } + }, java.lang.foreign.Arena.ofAuto()); + swiftjava_SwiftModule_asyncThrowsVoid.call($async$completion, $async$error); + return future$; + } + } + """ + ] + ) + } + + @Test("Import: async throws -> Void (Swift, CompletableFuture)") + func completableFuture_asyncThrowsVoid_swift() throws { + try assertOutput( + input: "public func asyncThrowsVoid() async throws", + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_asyncThrowsVoid") + public func swiftjava_SwiftModule_asyncThrowsVoid(_ $async$completion: @convention(c) () -> Void, _ $async$error: @convention(c) (UnsafePointer) -> Void) { + Task.immediate { + do { + try await asyncThrowsVoid() + $async$completion() + } catch { + let errorString = String(describing: error) + errorString.withCString { errorCString in + $async$error(errorCString) + } + } + } + } + """ + ] + ) + } +} From a64e48b9b2c8a59dd5bc27544b66f149bc64fa82 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Thu, 3 Sep 2026 01:01:07 +0530 Subject: [PATCH 2/4] fix(ffm): resolve Swift 6.1 compatibility, error boxing, and CompletableFuture in async --- ...Swift2JavaGenerator+FunctionLowering.swift | 36 +++-- ...t2JavaGenerator+JavaBindingsPrinting.swift | 11 +- ...MSwift2JavaGenerator+JavaTranslation.swift | 4 +- .../FFM/FFMAsyncTests.swift | 131 ++++++++++++------ 4 files changed, 123 insertions(+), 59 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 8f274a0ab..2fae87fdd 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -157,11 +157,13 @@ struct CdeclLowering { let asyncErrorOutParameter: LoweredParameter? if isAsync { + let completionParams: [SwiftParameter] = loweredResult.cdeclResultType.isVoid + ? [] + : [SwiftParameter(convention: .byValue, type: loweredResult.cdeclResultType)] + let completionType = SwiftFunctionType( convention: .c, - parameters: [ - SwiftParameter(convention: .byValue, type: loweredResult.cdeclResultType) - ], + parameters: completionParams, resultType: .tuple([]) ) asyncCompletionOutParameter = LoweredParameter( @@ -179,7 +181,10 @@ struct CdeclLowering { let errorCompletionType = SwiftFunctionType( convention: .c, parameters: [ - SwiftParameter(convention: .byValue, type: knownTypes.unsafePointer(knownTypes.int8)) + SwiftParameter( + convention: .byValue, + type: knownTypes.optionalSugar(knownTypes.unsafeMutableRawPointer) + ) ], resultType: .tuple([]) ) @@ -1194,10 +1199,8 @@ extension LoweredFunctionSignature { let doStmt: StmtSyntax = """ do {\(CodeBlockItemListSyntax(doBody)) } catch { - let errorString = String(describing: error) - errorString.withCString { errorCString in - async$error(errorCString) - } + let errorPtr = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + async$error(errorPtr) } """ taskBodyItems = [ @@ -1209,14 +1212,21 @@ extension LoweredFunctionSignature { item.with(\.leadingTrivia, [.newlines(1), .spaces(4)]) } - let taskExpr: ExprSyntax = """ - Task.immediate {\(CodeBlockItemListSyntax(taskBody)) + let taskStatements: CodeBlockItemListSyntax = """ + var task: Task? = nil + #if swift(>=6.2) + if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) { + task = Task.immediate {\(CodeBlockItemListSyntax(taskBody)) + } + } + #endif + if task == nil { + task = Task {\(CodeBlockItemListSyntax(taskBody)) + } } """ - bodyItems = [ - CodeBlockItemSyntax(item: .expr(taskExpr)) - ] + bodyItems = Array(taskStatements) } else { if !original.result.type.isVoid { let loweredResult: ExprSyntax? = result.conversion.asExprSyntax( diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index b0094a155..1cac3dd3f 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -419,7 +419,7 @@ extension FFMSwift2JavaGenerator { var throwsClauses: [String] = [] // If a Swift function is 'throws' we throw a checked error for the Java side // TODO: When we support typed throws on Swift side we'll want to throw the right type here instead - if translatedSignature.isThrowing { + if translatedSignature.isThrowing && !translatedSignature.isAsync { throwsClauses.append(JavaType.swiftJavaErrorException.className!) } if translatedSignature.canThrowSwiftIntegerOverflowException { @@ -516,12 +516,15 @@ extension FFMSwift2JavaGenerator { } if translatedSignature.isAsync { - printer.print("java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture();") + printer.print("\(translatedSignature.result.javaResultType) future$ = new \(translatedSignature.result.javaResultType)();") + + let isVoidResult = translatedSignature.result.javaResultType == .void || translatedSignature.result.javaResultType == .completableFuture(.void) + let completionLambdaArgs = isVoidResult ? "()" : "(result$)" let completionName = "$async$completion" - printer.print("MemorySegment \(completionName) = \(thunkName).\(completionName).toUpcallStub((result$) -> {") + printer.print("MemorySegment \(completionName) = \(thunkName).\(completionName).toUpcallStub(\(completionLambdaArgs) -> {") printer.indent() - if translatedSignature.result.javaResultType == .void || translatedSignature.result.javaResultType == .completableFuture(.void) { + if isVoidResult { printer.print("future$.complete(null);") } else { let result = translatedSignature.result.conversion.render( diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index d54edf35c..6321c2c9e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -1015,7 +1015,7 @@ extension FFMSwift2JavaGenerator.TranslatedFunctionSignature { /// Whether or not if the down-calling requires temporary "Arena" which is /// only used during the down-calling. var requiresTemporaryArena: Bool { - if self.isThrowing { + if self.isThrowing && !self.isAsync { return true } if self.parameters.contains(where: { $0.conversion.requiresTemporaryArena }) { @@ -1115,7 +1115,7 @@ extension CType { return .SwiftPointer case .void: - return .SwiftPointer + fatalError("unreachable") case .integral(.signed(bits: let bits)): if bits <= 8 { return .SwiftInt8 } diff --git a/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift index 9a9c1395b..1fd358747 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift @@ -29,7 +29,7 @@ struct FFMAsyncTests { """ /** * {@snippet lang=c : - * void swiftjava_SwiftModule_asyncVoid(void **$async$completion, void **$async$error) + * void swiftjava_SwiftModule_asyncVoid(void (*async$completion)(void)) * } */ private static class swiftjava_SwiftModule_asyncVoid { @@ -42,14 +42,12 @@ struct FFMAsyncTests { * } */ public static java.util.concurrent.CompletableFuture asyncVoid() { - try (var arena$ = org.swift.swiftkit.core.AllocatingSwiftArena.ofConfined()) { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - java.lang.foreign.MemorySegment $async$completion = swiftjava_SwiftModule_asyncVoid.$async$completion.toUpcallStub((result$) -> { - future$.complete(null); - }, java.lang.foreign.Arena.ofAuto()); - swiftjava_SwiftModule_asyncVoid.call($async$completion); - return future$; - } + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + MemorySegment $async$completion = swiftjava_SwiftModule_asyncVoid.$async$completion.toUpcallStub(() -> { + future$.complete(null); + }, Arena.ofAuto()); + swiftjava_SwiftModule_asyncVoid.call($async$completion); + return future$; } """, ] @@ -66,13 +64,22 @@ struct FFMAsyncTests { expectedChunks: [ """ @_cdecl("swiftjava_SwiftModule_asyncVoid") - public func swiftjava_SwiftModule_asyncVoid(_ $async$completion: @convention(c) () -> Void) { - Task.immediate { - await asyncVoid() - $async$completion() - } - } + public func swiftjava_SwiftModule_asyncVoid(_ async$completion: @convention(c) () -> ()) { + """, + """ + task = Task.immediate { + await asyncVoid() + async$completion() + } + """, """ + if task == nil { + task = Task { + await asyncVoid() + async$completion() + } + } + """, ] ) } @@ -84,6 +91,14 @@ struct FFMAsyncTests { .ffm, .java, expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_asyncThrowsVoid(void (*async$completion)(void), void (*async$error)(void *)) + * } + */ + private static class swiftjava_SwiftModule_asyncThrowsVoid { + """, """ /** * Downcall to Swift: @@ -92,21 +107,19 @@ struct FFMAsyncTests { * } */ public static java.util.concurrent.CompletableFuture asyncThrowsVoid() { - try (var arena$ = org.swift.swiftkit.core.AllocatingSwiftArena.ofConfined()) { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - java.lang.foreign.MemorySegment $async$completion = swiftjava_SwiftModule_asyncThrowsVoid.$async$completion.toUpcallStub((result$) -> { - future$.complete(null); - }, java.lang.foreign.Arena.ofAuto()); - java.lang.foreign.MemorySegment $async$error = swiftjava_SwiftModule_asyncThrowsVoid.$async$error.toUpcallStub((error$) -> { - if (!error$.equals(java.lang.foreign.MemorySegment.NULL)) { - future$.completeExceptionally(new org.swift.swiftkit.ffm.generated.SwiftJavaErrorException(error$, org.swift.swiftkit.core.AllocatingSwiftArena.ofAuto())); - } - }, java.lang.foreign.Arena.ofAuto()); - swiftjava_SwiftModule_asyncThrowsVoid.call($async$completion, $async$error); - return future$; - } + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + MemorySegment $async$completion = swiftjava_SwiftModule_asyncThrowsVoid.$async$completion.toUpcallStub(() -> { + future$.complete(null); + }, Arena.ofAuto()); + MemorySegment $async$error = swiftjava_SwiftModule_asyncThrowsVoid.$async$error.toUpcallStub((error$) -> { + if (!error$.equals(MemorySegment.NULL)) { + future$.completeExceptionally(new SwiftJavaErrorException(error$, AllocatingSwiftArena.ofAuto())); + } + }, Arena.ofAuto()); + swiftjava_SwiftModule_asyncThrowsVoid.call($async$completion, $async$error); + return future$; } - """ + """, ] ) } @@ -121,20 +134,58 @@ struct FFMAsyncTests { expectedChunks: [ """ @_cdecl("swiftjava_SwiftModule_asyncThrowsVoid") - public func swiftjava_SwiftModule_asyncThrowsVoid(_ $async$completion: @convention(c) () -> Void, _ $async$error: @convention(c) (UnsafePointer) -> Void) { - Task.immediate { - do { - try await asyncThrowsVoid() - $async$completion() - } catch { - let errorString = String(describing: error) - errorString.withCString { errorCString in - $async$error(errorCString) + public func swiftjava_SwiftModule_asyncThrowsVoid(_ async$completion: @convention(c) () -> (), _ async$error: @convention(c) (UnsafeMutableRawPointer?) -> ()) { + """, + """ + do { + try await asyncThrowsVoid() + async$completion() + } catch { + let errorPtr = Unmanaged.passRetained(SwiftJavaError(error)).toOpaque() + async$error(errorPtr) } - } - } + """, + ] + ) + } + + @Test("Import: async -> Int64 (Java, CompletableFuture)") + func completableFuture_asyncSum_java() throws { + try assertOutput( + input: "public func asyncSum(a: Int64, b: Int64) async -> Int64", + .ffm, + .java, + expectedChunks: [ + """ + public static java.util.concurrent.CompletableFuture asyncSum(long a, long b) { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + MemorySegment $async$completion = swiftjava_SwiftModule_asyncSum_a_b.$async$completion.toUpcallStub((result$) -> { + future$.complete(result$); + }, Arena.ofAuto()); + swiftjava_SwiftModule_asyncSum_a_b.call(a, b, $async$completion); + return future$; } + """, + ] + ) + } + + @Test("Import: async -> Int64 (Swift, CompletableFuture)") + func completableFuture_asyncSum_swift() throws { + try assertOutput( + input: "public func asyncSum(a: Int64, b: Int64) async -> Int64", + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ """ + @_cdecl("swiftjava_SwiftModule_asyncSum_a_b") + public func swiftjava_SwiftModule_asyncSum_a_b(_ a: Int64, _ b: Int64, _ async$completion: @convention(c) (Int64) -> ()) { + """, + """ + let async$result = await asyncSum(a: a, b: b) + async$completion(async$result) + """, ] ) } From 4aae3e650979ad7fbfb7eb1634f83a75fc94d711 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Thu, 3 Sep 2026 08:09:15 +0530 Subject: [PATCH 3/4] style: apply swift-format formatting --- .../FFMSwift2JavaGenerator+FunctionLowering.swift | 3 ++- Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 2fae87fdd..6d5fc454b 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -157,7 +157,8 @@ struct CdeclLowering { let asyncErrorOutParameter: LoweredParameter? if isAsync { - let completionParams: [SwiftParameter] = loweredResult.cdeclResultType.isVoid + let completionParams: [SwiftParameter] = + loweredResult.cdeclResultType.isVoid ? [] : [SwiftParameter(convention: .byValue, type: loweredResult.cdeclResultType)] diff --git a/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift index 1fd358747..a257dcbb4 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift @@ -165,7 +165,7 @@ struct FFMAsyncTests { swiftjava_SwiftModule_asyncSum_a_b.call(a, b, $async$completion); return future$; } - """, + """ ] ) } From 8c755e50814fe25bb27c54284d6f831b63bd4aa6 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Sat, 5 Sep 2026 10:25:15 +0530 Subject: [PATCH 4/4] test: import CompletableFuture and avoid FQNs in MySwiftLibraryTest --- .../java/com/example/swift/MySwiftLibraryTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 6e1126dd7..4d803db45 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -18,7 +18,9 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.ffm.generated.SwiftJavaErrorException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.*; @@ -196,7 +198,7 @@ void call_globalCallMeDoubleSupplier_noThrow() { @Test void call_asyncSum() throws Exception { // snippet.asyncUsageJava - java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); + CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); Long result = future.get(); assertEquals(22, result); // snippet.end @@ -204,14 +206,14 @@ void call_asyncSum() throws Exception { @Test void call_asyncThrowsVoid_noThrow() throws Exception { - java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(false); + CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(false); future.get(); // Should complete normally } @Test void call_asyncThrowsVoid_throws() { - java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(true); - java.util.concurrent.ExecutionException ex = assertThrows(java.util.concurrent.ExecutionException.class, future::get); + CompletableFuture future = MySwiftLibrary.asyncThrowsVoid(true); + ExecutionException ex = assertThrows(ExecutionException.class, future::get); Throwable cause = ex.getCause(); assertNotNull(cause);