Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,32 @@
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() {
CompletableFuture<Long> future = MySwiftLibrary.asyncSum(10, 12);
void asyncSum() throws Exception {
Future<Long> future = MySwiftLibrary.asyncSum(10, 12);

Long result = future.join();
Long result = future.get();
assertEquals(22, result);
}

@Test
void asyncSleep() {
CompletableFuture<Void> future = MySwiftLibrary.asyncSleep();
future.join();
void asyncSleep() throws Exception {
Future<Void> future = MySwiftLibrary.asyncSleep();
future.get();
}

@Test
void asyncCopy() {
void asyncCopy() throws Exception {
try (var arena = SwiftArena.ofConfined()) {
MySwiftClass obj = MySwiftClass.init(10, 5, arena);
CompletableFuture<MySwiftClass> future = MySwiftLibrary.asyncCopy(obj, arena);
Future<MySwiftClass> future = MySwiftLibrary.asyncCopy(obj, arena);

MySwiftClass result = future.join();
MySwiftClass result = future.get();

assertEquals(10, result.getX());
assertEquals(5, result.getY());
Expand All @@ -59,7 +60,7 @@ void asyncCopy() {

@Test
void asyncThrows() {
CompletableFuture<Void> future = MySwiftLibrary.asyncThrows();
Future<Void> future = MySwiftLibrary.asyncThrows();

ExecutionException ex = assertThrows(ExecutionException.class, future::get);

Expand All @@ -70,14 +71,14 @@ void asyncThrows() {
}

@Test
void asyncOptional() {
CompletableFuture<OptionalLong> future = MySwiftLibrary.asyncOptional(42);
assertEquals(OptionalLong.of(42), future.join());
void asyncOptional() throws Exception {
Future<OptionalLong> future = MySwiftLibrary.asyncOptional(42);
assertEquals(OptionalLong.of(42), future.get());
}

@Test
void asyncString() {
CompletableFuture<String> future = MySwiftLibrary.asyncString("hey");
assertEquals("hey", future.join());
void asyncString() throws Exception {
Future<String> future = MySwiftLibrary.asyncString("hey");
assertEquals("hey", future.get());
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these changes if we did not change the generation mode in this sample?

We should find a way to test both modes, we don't want to have zero coverage of either runtime functionality

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just makes it easier to switch between the two modes when testing. These tests should work no matter what Future type is used.

Copy link
Collaborator

@ktoso ktoso Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we're lucky enough that these modes are source compatible on this basic level. We should make the verification process make use of that though -- in the long term we'd need source directories for specific modes, but for this specific PR how about we add another step that sets an env var, that env var then the ci-validate script picks up and changes the mode in the swift-java config.

We're growing more "modes" and it's probably time we start actually testing them, even if it's a rarely used mode like this.

}
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ void addXWithJavaLong() {
}

@Test
void getAsyncVariable() {
void getAsyncVariable() throws Exception {
try (var arena = SwiftArena.ofConfined()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
assertEquals(42, c1.getGetAsync().join());
assertEquals(42, c1.getGetAsync().get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -501,40 +501,55 @@ extension JNISwift2JavaGenerator {
originalFunctionSignature: SwiftFunctionSignature,
mode: JExtractAsyncFuncMode
) {
// Update translated function
let nativeFutureType: JavaType
let translatedFutureType: JavaType
let completeMethodID: String
let completeExceptionallyMethodID: String

switch mode {
case .completableFuture:
// Update translated function

let nativeFutureType = JavaType.completableFuture(nativeFunctionSignature.result.javaType)
nativeFutureType = .completableFuture(nativeFunctionSignature.result.javaType)
translatedFutureType = .completableFuture(translatedFunctionSignature.resultType.javaType)
completeMethodID = "_JNIMethodIDCache.CompletableFuture.complete"
completeExceptionallyMethodID = "_JNIMethodIDCache.CompletableFuture.completeExceptionally"

case .legacyFuture:
nativeFutureType = .simpleCompletableFuture(nativeFunctionSignature.result.javaType)
translatedFutureType = .future(translatedFunctionSignature.resultType.javaType)
completeMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.complete"
completeExceptionallyMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.completeExceptionally"
}

let futureOutParameter = OutParameter(
name: "$future",
type: nativeFutureType,
allocation: .new
)
let futureOutParameter = OutParameter(
name: "future$",
type: nativeFutureType,
allocation: .new
)

let result = translatedFunctionSignature.resultType
translatedFunctionSignature.resultType = TranslatedResult(
javaType: .completableFuture(translatedFunctionSignature.resultType.javaType),
annotations: result.annotations,
outParameters: result.outParameters + [futureOutParameter],
conversion: .aggregate(variable: nil, [
.print(.placeholder), // Make the downcall
.method(.constant("$future"), function: "thenApply", arguments: [
.lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$"))
])
let result = translatedFunctionSignature.resultType
translatedFunctionSignature.resultType = TranslatedResult(
javaType: translatedFutureType,
annotations: result.annotations,
outParameters: result.outParameters + [futureOutParameter],
conversion: .aggregate(variable: nil, [
.print(.placeholder), // Make the downcall
.method(.constant("future$"), function: "thenApply", arguments: [
.lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$"))
])
)
])
)

// Update native function
nativeFunctionSignature.result.conversion = .asyncCompleteFuture(
swiftFunctionResultType: originalFunctionSignature.result.type,
nativeFunctionSignature: nativeFunctionSignature,
isThrowing: originalFunctionSignature.isThrowing
)
nativeFunctionSignature.result.javaType = .void
nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType))
}
// Update native function
nativeFunctionSignature.result.conversion = .asyncCompleteFuture(
swiftFunctionResultType: originalFunctionSignature.result.type,
nativeFunctionSignature: nativeFunctionSignature,
isThrowing: originalFunctionSignature.isThrowing,
completeMethodID: completeMethodID,
completeExceptionallyMethodID: completeExceptionallyMethodID
)
nativeFunctionSignature.result.javaType = .void
nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType))
}

func translateProtocolParameter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,9 @@ extension JNISwift2JavaGenerator {
indirect case asyncCompleteFuture(
swiftFunctionResultType: SwiftType,
nativeFunctionSignature: NativeFunctionSignature,
isThrowing: Bool
isThrowing: Bool,
completeMethodID: String,
completeExceptionallyMethodID: String
)

/// `{ (args) -> return body }`
Expand Down Expand Up @@ -927,7 +929,9 @@ extension JNISwift2JavaGenerator {
case .asyncCompleteFuture(
let swiftFunctionResultType,
let nativeFunctionSignature,
let isThrowing
let isThrowing,
let completeMethodID,
let completeExceptionallyMethodID
):
var globalRefs: [String] = ["globalFuture"]

Expand All @@ -954,7 +958,7 @@ extension JNISwift2JavaGenerator {
printer.print("environment = try! JavaVirtualMachine.shared().environment()")
let inner = nativeFunctionSignature.result.conversion.render(&printer, "swiftResult$")
if swiftFunctionResultType.isVoid {
printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)])")
printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: nil)])")
} else {
let result: String
if nativeFunctionSignature.result.javaType.requiresBoxing {
Expand All @@ -964,7 +968,7 @@ extension JNISwift2JavaGenerator {
result = inner
}

printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: \(result))])")
printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: \(result))])")
}
}

Expand All @@ -986,7 +990,7 @@ extension JNISwift2JavaGenerator {
"""
let catchEnvironment = try! JavaVirtualMachine.shared().environment()
let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)])
catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)])
catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, \(completeExceptionallyMethodID), [jvalue(l: exception)])
"""
)
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ extension JavaType {
static func completableFuture(_ T: JavaType) -> JavaType {
.class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType])
}

/// The description of the type java.util.concurrent.Future<T>
static func future(_ T: JavaType) -> JavaType {
.class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType])
}
}
5 changes: 5 additions & 0 deletions Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ extension JavaType {
}
}

/// The description of the type org.swift.swiftkit.core.SimpleCompletableFuture<T>
static func simpleCompletableFuture(_ T: JavaType) -> JavaType {
.class(package: "org.swift.swiftkit.core", name: "SimpleCompletableFuture", typeParameters: [T.boxedType])
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public enum JExtractAsyncFuncMode: String, Codable {
/// Android 23 and below.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have this framed about java versions first and foremost, and about Android versions secondary. CompletableFuture is JDK8 which is pretty ancient

Copy link
Contributor Author

@madsodgaard madsodgaard Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, its not really about JDK versions either. We generate code that uses JDK 17 language features. This is just a result of Android adopting the Java library features at different times and in later API levels

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore android exists for a moment; this is absolutely a JDK question. Android isn't JDK so we mention the Android version, for anyone else, this is specifically about JDK versions.

///
/// - Note: Prefer using the `completableFuture` mode instead, if possible.
// case future
case legacyFuture
}

extension JExtractAsyncFuncMode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@ extension _JNIMethodIDCache {
}
}

public enum SimpleCompletableFuture {
private static let completeMethod = Method(
name: "complete",
signature: "(Ljava/lang/Object;)Z"
)

private static let completeExceptionallyMethod = Method(
name: "completeExceptionally",
signature: "(Ljava/lang/Throwable;)Z"
)

private static let cache = _JNIMethodIDCache(
environment: try! JavaVirtualMachine.shared().environment(),
className: "org/swift/swiftkit/core/SimpleCompletableFuture",
methods: [completeMethod, completeExceptionallyMethod]
)

public static var `class`: jclass {
cache.javaClass
}

public static var complete: jmethodID {
cache.methods[completeMethod]!
}

public static var completeExceptionally: jmethodID {
cache.methods[completeExceptionallyMethod]!
}
}

public enum Exception {
private static let messageConstructor = Method(name: "<init>", signature: "(Ljava/lang/String;)V")

Expand Down
Loading
Loading