diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index 5fe7c1310..400844fdc 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -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 future = MySwiftLibrary.asyncSum(10, 12); + void asyncSum() throws Exception { + Future future = MySwiftLibrary.asyncSum(10, 12); - Long result = future.join(); + Long result = future.get(); assertEquals(22, result); } @Test - void asyncSleep() { - CompletableFuture future = MySwiftLibrary.asyncSleep(); - future.join(); + void asyncSleep() throws Exception { + Future 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 future = MySwiftLibrary.asyncCopy(obj, arena); + Future future = MySwiftLibrary.asyncCopy(obj, arena); - MySwiftClass result = future.join(); + MySwiftClass result = future.get(); assertEquals(10, result.getX()); assertEquals(5, result.getY()); @@ -59,7 +60,7 @@ void asyncCopy() { @Test void asyncThrows() { - CompletableFuture future = MySwiftLibrary.asyncThrows(); + Future future = MySwiftLibrary.asyncThrows(); ExecutionException ex = assertThrows(ExecutionException.class, future::get); @@ -70,14 +71,14 @@ void asyncThrows() { } @Test - void asyncOptional() { - CompletableFuture future = MySwiftLibrary.asyncOptional(42); - assertEquals(OptionalLong.of(42), future.join()); + void asyncOptional() throws Exception { + Future future = MySwiftLibrary.asyncOptional(42); + assertEquals(OptionalLong.of(42), future.get()); } @Test - void asyncString() { - CompletableFuture future = MySwiftLibrary.asyncString("hey"); - assertEquals("hey", future.join()); + void asyncString() throws Exception { + Future future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.get()); } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index a58386299..860f1641c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -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()); } } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9c3cdbf1a..9a2787c9a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -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( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 1994dce04..24e469067 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -689,7 +689,9 @@ extension JNISwift2JavaGenerator { indirect case asyncCompleteFuture( swiftFunctionResultType: SwiftType, nativeFunctionSignature: NativeFunctionSignature, - isThrowing: Bool + isThrowing: Bool, + completeMethodID: String, + completeExceptionallyMethodID: String ) /// `{ (args) -> return body }` @@ -927,7 +929,9 @@ extension JNISwift2JavaGenerator { case .asyncCompleteFuture( let swiftFunctionResultType, let nativeFunctionSignature, - let isThrowing + let isThrowing, + let completeMethodID, + let completeExceptionallyMethodID ): var globalRefs: [String] = ["globalFuture"] @@ -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 { @@ -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))])") } } @@ -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)]) """ ) } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 5e5a72688..511bf8de6 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -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 + static func future(_ T: JavaType) -> JavaType { + .class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType]) + } } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift index 2ab9c0a22..7319b2942 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -85,4 +85,9 @@ extension JavaType { } } + /// The description of the type org.swift.swiftkit.core.SimpleCompletableFuture + static func simpleCompletableFuture(_ T: JavaType) -> JavaType { + .class(package: "org.swift.swiftkit.core", name: "SimpleCompletableFuture", typeParameters: [T.boxedType]) + } + } diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift index 221649c52..d7fd84623 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift @@ -23,7 +23,7 @@ public enum JExtractAsyncFuncMode: String, Codable { /// Android 23 and below. /// /// - Note: Prefer using the `completableFuture` mode instead, if possible. -// case future + case legacyFuture } extension JExtractAsyncFuncMode { diff --git a/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift similarity index 70% rename from Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift rename to Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 1c3079bc3..56fe0351a 100644 --- a/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -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: "", signature: "(Ljava/lang/String;)V") diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java new file mode 100644 index 000000000..b92527236 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java @@ -0,0 +1,223 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 org.swift.swiftkit.core; + +import java.util.Deque; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * A simple completable {@link Future} for platforms that do not support {@link java.util.concurrent.CompletableFuture}, + * e.g. before Java 8, and/or before Android 23. + *

+ * Prefer using the {@link CompletableFuture} for bridging Swift asynchronous functions, i.e. use the {@code completableFuture} + * mode in {@code swift-java jextract}. + * + * @param The result type + */ +public final class SimpleCompletableFuture implements Future { + // Marker object used to indicate the Future has not yet been completed. + private static final Object PENDING = new Object(); + private static final Object NULL = new Object(); + private final AtomicReference result = new AtomicReference<>(PENDING); + + private final Deque callbacks = new ConcurrentLinkedDeque<>(); + + /** + * Wrapper type we use to indicate that a recorded result was a failure (recorded using {@link SimpleCompletableFuture#completeExceptionally(Throwable)}. + * Since no-one else can instantiate this type, we know for sure that a recorded CompletedExceptionally indicates a failure. + */ + static final class CompletedExceptionally { + private final Throwable exception; + + private CompletedExceptionally(Throwable exception) { + this.exception = exception; + } + } + + /** + * Returns a new future that, when this stage completes + * normally, is executed with this stage's result as the argument + * to the supplied function. + * + *

This method is analogous to + * {@link java.util.Optional#map Optional.map} and + * {@link java.util.stream.Stream#map Stream.map}. + * + * @return the new Future + */ + public Future thenApply(Function fn) { + SimpleCompletableFuture newFuture = new SimpleCompletableFuture<>(); + addCallback(() -> { + Object observed = this.result.get(); + if (observed instanceof CompletedExceptionally) { + newFuture.completeExceptionally(((CompletedExceptionally) observed).exception); + } else { + try { + // We're guaranteed that an observed result is of type T. + // noinspection unchecked + U newResult = fn.apply(observed == NULL ? null : (T) observed); + newFuture.complete(newResult); + } catch (Throwable t) { + newFuture.completeExceptionally(t); + } + } + }); + return newFuture; + } + + /** + * If not already completed, sets the value returned by {@link #get()} and + * related methods to the given value. + * + * @param value the result value + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean complete(T value) { + if (result.compareAndSet(PENDING, value == null ? NULL : value)) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + /** + * If not already completed, causes invocations of {@link #get()} + * and related methods to throw the given exception. + * + * @param ex the exception + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean completeExceptionally(Throwable ex) { + if (result.compareAndSet(PENDING, new CompletedExceptionally(ex))) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + private void runCallbacks() { + // This is a pretty naive implementation; even if we enter this by racing a thenApply, + // with a completion; we're using a concurrent deque so we won't happen to trigger a callback twice. + Runnable callback; + while ((callback = callbacks.pollFirst()) != null) { + callback.run(); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // TODO: If we're representing a Swift Task computation with this future, + // we could trigger a Task.cancel() from here + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return this.result.get() != PENDING; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + Object observed; + // If PENDING check fails immediately, we have no need to take the result lock at all. + while ((observed = result.get()) == PENDING) { + synchronized (result) { + if (result.get() == PENDING) { + result.wait(); + } + } + } + + return getReturn(observed); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + Object observed; + + // Fast path: are we already completed and don't need to do any waiting? + if ((observed = result.get()) != PENDING) { + return get(); + } + + long nanos = unit.toNanos(timeout); + synchronized (result) { + if (!isDone()) { + if (nanos <= 0) { + throw new TimeoutException(); + } + long deadline = System.nanoTime() + nanos; + while (!isDone()) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + throw new TimeoutException(); + } + result.wait(nanos / 1000000, (int) (nanos % 1000000)); + } + } + } + + // Seems we broke out of the wait loop, let's trigger the 'get()' implementation + observed = result.get(); + if (observed == PENDING) { + throw new ExecutionException("Unexpectedly finished wait-loop while future was not completed, this is a bug.", null); + } + return getReturn(observed); + } + + private T getReturn(Object observed) throws ExecutionException { + if (observed instanceof CompletedExceptionally) { + // We observed a failure, unwrap and throw it + Throwable exception = ((CompletedExceptionally) observed).exception; + if (exception instanceof CancellationException) { + throw (CancellationException) exception; + } + throw new ExecutionException(exception); + } else if (observed == NULL) { + return null; + } else { + // We're guaranteed that we only allowed registering completions of type `T` + // noinspection unchecked + return (T) observed; + } + } + + private void addCallback(Runnable action) { + callbacks.add(action); + if (isDone()) { + // This may race, but we don't care since triggering the callbacks is going to be at-most-once + // by means of using the concurrent deque as our list of callbacks. + runCallbacks(); + } + } + +} \ No newline at end of file diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java new file mode 100644 index 000000000..b4bb98b3a --- /dev/null +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 org.swift.swiftkit.core; + +import org.junit.jupiter.api.Test; + +import java.util.Objects; +import java.util.concurrent.*; +import static org.junit.jupiter.api.Assertions.*; + +public class SimpleCompletableFutureTest { + + @Test + void testCompleteAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete("test")); + assertTrue(future.isDone()); + assertEquals("test", future.get()); + } + + @Test + void testCompleteWithNullAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete(null)); + assertTrue(future.isDone()); + assertNull(future.get()); + } + + @Test + void testCompleteExceptionallyAndGet() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Test Exception"); + assertTrue(future.completeExceptionally(ex)); + assertTrue(future.isDone()); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testGetWithTimeout_timesOut() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertThrows(TimeoutException.class, () -> future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTime() throws ExecutionException, InterruptedException, TimeoutException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("fast"); + assertEquals("fast", future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTimeAfterWait() throws Exception { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Thread t = new Thread(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // ignore + } + future.complete("late"); + }); + t.start(); + assertEquals("late", future.get(200, TimeUnit.MILLISECONDS)); + } + + @Test + void testThenApply() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(String::length); + + future.complete("hello"); + + assertEquals(5, mapped.get()); + } + + @Test + void testThenApplyOnCompletedFuture() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("done"); + + Future mapped = future.thenApply(String::length); + + assertEquals(4, mapped.get()); + } + + @Test + void testThenApplyWithNull() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(Objects::isNull); + + future.complete(null); + + assertTrue(mapped.get()); + } + + @Test + void testThenApplyExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Initial Exception"); + Future mapped = future.thenApply(String::length); + + future.completeExceptionally(ex); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testThenApplyTransformationThrows() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Transformation Exception"); + Future mapped = future.thenApply(s -> { + throw ex; + }); + + future.complete("hello"); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testCompleteTwice() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.complete("second")); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteThenCompleteExceptionally() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.completeExceptionally(new RuntimeException("second"))); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteExceptionallyThenComplete() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("first"); + + assertTrue(future.completeExceptionally(ex)); + assertFalse(future.complete("second")); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testIsDone() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.complete("done"); + assertTrue(future.isDone()); + } + + @Test + void testIsDoneExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.completeExceptionally(new RuntimeException()); + assertTrue(future.isDone()); + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index 1930e601a..f246bd0f7 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -14,6 +14,7 @@ import JExtractSwiftLib import Testing +import SwiftJavaConfigurationShared @Suite struct JNIAsyncTests { @@ -33,9 +34,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture asyncVoid() { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$asyncVoid($future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$asyncVoid(future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -107,9 +108,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async() { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async($future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -195,9 +196,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(long i) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(i, $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(i, future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -276,9 +277,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(MyClass c, SwiftArena swiftArena$) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(c.$memoryAddress(), $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); } ); @@ -365,9 +366,9 @@ struct JNIAsyncTests { expectedChunks: [ """ public static java.util.concurrent.CompletableFuture async(java.lang.String s) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(s, $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(s, future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -408,4 +409,65 @@ struct JNIAsyncTests { ] ) } + + @Test("Import: (MyClass) async -> MyClass (Java, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_java() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static java.util.concurrent.Future async(MyClass c, SwiftArena swiftArena$) { + org.swift.swiftkit.core.SimpleCompletableFuture future$ = new org.swift.swiftkit.core.SimpleCompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); + } + ); + } + """, + """ + private static native void $async(long c, org.swift.swiftkit.core.SimpleCompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Swift, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_swift() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, c: jlong, result_future: jobject?) { + ... + var task: Task? = nil + ... + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.SimpleCompletableFuture.complete, [jvalue(l: boxedResult$)]) + ... + } + """ + ] + ) + } }