From 9ce9c74ac55fa75b5259c14f5a45eae565e25008 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Fri, 7 Aug 2026 15:41:56 +0530 Subject: [PATCH 1/3] feat(ffm): add support for async Swift functions via CompletableFuture --- ...Swift2JavaGenerator+FunctionLowering.swift | 187 ++++++++++++++---- ...t2JavaGenerator+JavaBindingsPrinting.swift | 42 +++- ...MSwift2JavaGenerator+JavaTranslation.swift | 10 +- 3 files changed, 201 insertions(+), 38 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 20319b5c2..3b3fa897d 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: knownTypes.optionalSugar(.function(completionType)) + ) + ], + conversion: .placeholder + ) + + if isThrowing { + let errorCompletionType = SwiftFunctionType( + convention: .c, + parameters: [ + SwiftParameter(convention: .byValue, type: knownTypes.optionalSugar(knownTypes.unsafePointer(knownTypes.int8))) + ], + resultType: .tuple([]) + ) + asyncErrorOutParameter = LoweredParameter( + cdeclParameters: [ + SwiftParameter( + convention: .byValue, + parameterName: "async$error", + type: knownTypes.optionalSugar(.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: awaitKeyword)\(raw: tryKeyword)\(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: awaitKeyword)\(raw: tryKeyword)\(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 taskStmt: StmtSyntax = """ + Task {\(CodeBlockItemListSyntax(taskBody)) + } """ bodyItems = [ - CodeBlockItemSyntax(item: .stmt(doStmt)) + CodeBlockItemSyntax(item: .stmt(taskStmt)) ] + } 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..d84c8432f 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -505,8 +505,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 +583,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..88014afc1 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 ) } From 5f18d624e63354bee29ec06da012f7261ca3125f Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Mon, 10 Aug 2026 14:03:40 +0530 Subject: [PATCH 2/3] test(ffm): add source generation tests and documentation for async support --- .agents/workflows/review-swift-java-pr.md | 5 + .../MySwiftLibrary/MySwiftLibrary.swift | 13 ++ .../com/example/swift/MySwiftLibraryTest.java | 27 ++++ Snippets/AsyncJavaFFM.java | 91 +++++++++++ .../Documentation.docc/FeaturesJextract.md | 4 +- .../FFM/FFMAsyncTests.swift | 141 ++++++++++++++++++ 6 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 .agents/workflows/review-swift-java-pr.md create mode 100644 Snippets/AsyncJavaFFM.java create mode 100644 Tests/JExtractSwiftTests/FFM/FFMAsyncTests.swift diff --git a/.agents/workflows/review-swift-java-pr.md b/.agents/workflows/review-swift-java-pr.md new file mode 100644 index 000000000..12ab7b121 --- /dev/null +++ b/.agents/workflows/review-swift-java-pr.md @@ -0,0 +1,5 @@ +--- +description: +--- + +Review the current staged changes and draft a Pull Request description. Before drafting, verify that the code includes both source and runtime tests. Check specifically if the Samples/FFM sample has been updated with runtime tests for any substantial new functionality. Also, verify that the supported feature list in the documentation has been updated. If any of these are missing, list them clearly as action items before generating the PR draft. diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index f2dc75125..ec3d21d67 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -159,6 +159,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 5375017d5..bb7468320 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -189,4 +189,31 @@ void call_globalCallMeDoubleSupplier_noThrow() { double result = MySwiftLibrary.globalCallMeDoubleSupplier(() -> { return 2.0; }); assertEquals(2.0, result); } + + // ==== ---------------------------------------------------------------- + // Async functions + + @Test + void call_asyncSum() throws Exception { + java.util.concurrent.CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); + Long result = future.get(); + assertEquals(22, result); + } + + @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")); + } } diff --git a/Snippets/AsyncJavaFFM.java b/Snippets/AsyncJavaFFM.java new file mode 100644 index 000000000..865e1e1e3 --- /dev/null +++ b/Snippets/AsyncJavaFFM.java @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import com.example.swift.MySwiftClass; +import com.example.swift.MySwiftLibrary; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.AllocatingSwiftArena; +import org.swift.swiftkit.ffm.generated.SwiftJavaErrorException; + +import java.time.Duration; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.*; + +public class AsyncTest { + @Test + void asyncSum() throws Exception { + // snippet.asyncUsageJava + Future future = MySwiftLibrary.asyncSum(10, 12); + + Long result = future.get(); + assertEquals(22, result); + // snippet.end + } + + @Test + void asyncSleep() throws Exception { + // snippet.asyncSleepUsageJava + Future future = MySwiftLibrary.asyncSleep(); + future.get(); + // snippet.end + } + + @Test + void asyncCopy() throws Exception { + // snippet.asyncCopyUsageJava + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftClass obj = MySwiftClass.init(10, 5, arena); + Future future = MySwiftLibrary.asyncCopy(obj, arena); + + MySwiftClass result = future.get(); + + assertEquals(10, result.getX()); + assertEquals(5, result.getY()); + } + // snippet.end + } + + @Test + void asyncThrows() { + Future future = MySwiftLibrary.asyncThrows(); + + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + + Throwable cause = ex.getCause(); + assertNotNull(cause); + assertEquals(SwiftJavaErrorException.class, cause.getClass()); + assertTrue(cause.getMessage().contains("swiftError")); + } + + @Test + void asyncOptional() throws Exception { + Future future = MySwiftLibrary.asyncOptional(42); + assertEquals(OptionalLong.of(42), future.get()); + } + + @Test + void asyncString() throws Exception { + Future future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.get()); + } +} 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..da4b249cd --- /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 { + 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 { + do { + try await asyncThrowsVoid() + $async$completion?() + } catch { + let errorString = String(describing: error) + errorString.withCString { errorCString in + $async$error?(errorCString) + } + } + } + } + """ + ] + ) + } +} From f9bb3b92b1a98962939930ac7319624e1bd53653 Mon Sep 17 00:00:00 2001 From: amanmaurya92 Date: Mon, 10 Aug 2026 15:05:04 +0530 Subject: [PATCH 3/3] chore: remove and ignore agent workflow files --- .agents/workflows/review-swift-java-pr.md | 5 ----- .gitignore | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 .agents/workflows/review-swift-java-pr.md diff --git a/.agents/workflows/review-swift-java-pr.md b/.agents/workflows/review-swift-java-pr.md deleted file mode 100644 index 12ab7b121..000000000 --- a/.agents/workflows/review-swift-java-pr.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -description: ---- - -Review the current staged changes and draft a Pull Request description. Before drafting, verify that the code includes both source and runtime tests. Check specifically if the Samples/FFM sample has been updated with runtime tests for any substantial new functionality. Also, verify that the supported feature list in the documentation has been updated. If any of these are missing, list them clearly as action items before generating the PR draft. diff --git a/.gitignore b/.gitignore index 4a1ca5a72..d19506223 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ Package.resolved BuildLogic/.kotlin/ + +# Ignore Gemini IDE agent files +.agents/