Skip to content
Open
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand Down Expand Up @@ -87,4 +88,95 @@ void asyncString() throws Exception {
Future<String> future = MySwiftLibrary.asyncString("hey");
assertEquals("hey", future.get());
}

@Test
void asyncSchedule_voidClosure() throws Exception {
AtomicBoolean called = new AtomicBoolean(false);
Future<Void> 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<Long> future = MySwiftLibrary.asyncCompute(21, (val) -> {
return CompletableFuture.completedFuture(val * 2);
});
Long result = future.get();
assertEquals(42L, result);
}

@Test
void asyncCompute_backgroundThreadCompletion() throws Exception {
Future<Long> 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<Double> 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<Void> future = MySwiftLibrary.asyncScheduleThrowing(() -> {
CompletableFuture<Void> failed = new CompletableFuture<>();
failed.completeExceptionally(new RuntimeException("Java async failed"));
return failed;
});

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hm, optimally we'd want to get the RuntimeException rethrown i guess... that might be hard to do, ok to do in a follow up PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will open the issue for this.

}

@Test
void asyncFetchOptional_present() throws Exception {
Future<OptionalLong> future = MySwiftLibrary.asyncFetchOptional(() -> {
return CompletableFuture.completedFuture(OptionalLong.of(99L));
});
assertEquals(OptionalLong.of(99L), future.get());
}

@Test
void asyncFetchOptional_empty() throws Exception {
Future<OptionalLong> future = MySwiftLibrary.asyncFetchOptional(() -> {
return CompletableFuture.completedFuture(OptionalLong.empty());
});
assertEquals(OptionalLong.empty(), future.get());
}

@Test
void asyncComputeThrowing_success() throws Exception {
Future<Long> future = MySwiftLibrary.asyncComputeThrowing(10, (val) -> {
return CompletableFuture.completedFuture(val + 5);
});
assertEquals(15L, future.get());
}

@Test
void asyncComputeThrowing_exceptionPropagates() {
Future<Long> future = MySwiftLibrary.asyncComputeThrowing(10, (val) -> {
CompletableFuture<Long> failed = new CompletableFuture<>();
failed.completeExceptionally(new RuntimeException("compute throwing failed"));
return failed;
});

assertThrows(ExecutionException.class, future::get);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ void genericEnum() {
try (var arena = SwiftArena.ofConfined()) {
GenericEnum<Long> 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());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JavaObject>?"
case .legacyFuture:
"SwiftJavaSimpleCompletableFuture?"
}
return "\(paramList) -> \(futureType)"
}

if type.resultType.isVoid {
return paramList
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<JavaString>.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)!"
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh no, actually this has an issue -- try writing a test for async -> Int64? , we're missing conversions here.


return TranslatedFunctionType(
name: name,
Expand Down Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
amanmaurya92 marked this conversation as resolved.
// 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
Expand Down Expand Up @@ -1746,19 +1746,17 @@ 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.
let closureHeader =
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.
Expand Down
6 changes: 3 additions & 3 deletions Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ struct KnownJavaFunctionalInterface: Sendable {
}

static func find(_ functionType: SwiftFunctionType) -> KnownJavaFunctionalInterface? {
if functionType.isEscaping {
if functionType.isEscaping || functionType.isAsync {
return nil
}

Expand Down Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions Sources/SwiftJava/SwiftJavaSimpleCompletableFuture.swift
Original file line number Diff line number Diff line change
@@ -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?
}
Loading
Loading