Repository navigation
JNI: cancelling an async function's future cancels the Swift task - #10
Open
piercifani wants to merge 2 commits into
Open
piercifani wants to merge 2 commits into
piercifani wants to merge 2 commits into
Conversation
piercifani
force-pushed
the
jni/08-async-cancellation
branch
from
November 3, 2026 15:19
b8542c5 to
8c4a3b3
Compare
piercifani
force-pushed
the
jni/08-async-cancellation
branch
from
November 3, 2026 15:56
8c4a3b3 to
711dd55
Compare
The thunk for an exported `async` function kept its Task only in a local, and the Java method returned a derived future (`thenApply`), whose cancellation doesn't reach its source. Cancelling the returned future left the Swift work running. - SwiftKitCore's `CompletableFutureCancellation` forwards cancellation from the returned future to the source future, and cancels/releases a Swift task handle when the source future completes. - SwiftJavaRuntimeSupport's `_cancelTaskWhenFutureIsCancelled` registers the task with the future the native call received. - In `completableFuture` mode the generated Java forwards cancellation and the Swift thunk registers the task. `legacyFuture` mode is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
piercifani
force-pushed
the
jni/08-async-cancellation
branch
from
November 3, 2026 16:46
711dd55 to
aafa76a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 8/21 · base:
jni/07-class-inheritance· previous: #8 · next: #13Problem
In JNI mode an
asyncSwift function returns aCompletableFuture, but cancelling it didn't stop the Swift work:Taskand keeps it only in a local variable, so nothing can cancel it.future$.thenApply(...), a derived future. Cancelling a derived future doesn't propagate to its source, so even the future Swift completes never saw the cancellation.So when a caller gives up (in Kotlin, a coroutine cancelled because a screen went away), the Swift task keeps running to completion.
Changes
CompletableFutureCancellation:forwardCancellation(result, source)cancelssourcewhen the returned futureresultis cancelled.cancelTaskWhenCancelled(future, task)registerswhenCompleteonfuture. If it ended cancelled, it calls the native$cancelTask; in every case it calls$releaseTask, so the handle is released exactly once._cancelTaskWhenFutureIsCancelled(_:task:environment:), which retains a handle to the task, plus the two native entry points.completableFuturemode only:CompletableFutureCancellation.forwardCancellation(future$.thenApply(...), future$).result_futurereference, which stays valid for the whole call even if aTask.immediatetask has already finished and deleted its global reference.legacyFuturemode is unchanged.The Swift task sees an ordinary
Taskcancellation (Task.isCancelled,CancellationErrorfromTask.sleep). Completing an already-cancelled future afterwards is a no-op.Kotlin in this PR
CancellationTest.ktwithCancellation.swift:withTimeoutOrNull(100) { download.run().await() }. When the timeout cancels the coroutine,kotlinx-coroutines'await()cancels the future, and with this PR the Swift task sees the cancellation.Why Skip needs it
Skip bridges
asyncSwift functions to Kotlinsuspendfunctions, and Compose cancels coroutines when screens leave composition. A production Skip Fuse app (16 Skip modules) exports 103asyncfunctions; without this their work outlives the screen that started it.Testing
./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:testpasses at this point in the stack.JNIAsyncCancellationTests: forwarding in the Java method, task registration in the Swift thunk, andlegacyFuturemode unchanged.JNIAsyncTestsandJNIActorTestsexpectations updated for the new output.CancellableWorkwith a Java test thatfuture.cancel(true)cancels the Swift task.swift test --disable-experimental-prebuilts --disable-sandboxand./gradlew :Samples:SwiftJavaExtractJNISampleApp:testpass.swift format lint --stricton changed files;git diff --check.Not in this PR
legacyFuturemode (SimpleCompletableFuture) doesn't propagate cancellation.AsyncStreams.🤖 Generated with Claude Code