Skip to content

Add BridgedJob so cancelling a Swift task cancels the bridged Kotlin coroutine - #120

Merged
marcprux merged 4 commits into
skiptools:mainfrom
piercifani:async-cancellation
Sep 26, 2026
Merged

marcprux merged 4 commits into
skiptools:mainfrom
piercifani:async-cancellation

Conversation

@piercifani

@piercifani piercifani commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Runtime half of skiptools/skipstone PR link: cancelling a Swift task that awaits a bridged Kotlin async throws API now cancels the Kotlin coroutine.

BridgedJob is what the generated Swift uses around the await:

let f_job = BridgedJob()
return try await withTaskCancellationHandler {
    try await withCheckedThrowingContinuation { f_continuation in
        …
        let f_job_java: JavaObjectPointer = try! Java_peer.call(method: …callback_…, …)
        f_job.attach(f_job_java)
    }
} onCancel: {
    f_job.cancel()
}
  • attach(_:) adopts the kotlinx.coroutines.Job the Kotlin callback_ function now returns, cancelling it immediately if the task was already cancelled (the handler can run before the call is made).
  • cancel() calls Job.cancel(null) through JNI.
  • error(_:options:) maps a throwable delivered after the cancellation to CancellationError; otherwise it defers to JThrowable.toError.

Testing

SkipBridgeToSwiftSamples gains kotlinAsyncParkingFunction, a Kotlin function parked in suspendCancellableCoroutine whose invokeOnCancellation records the cancellation (the pure-Swift branch polls Task.isCancelled so the plain macOS run keeps the same semantics). BridgeToSwiftSamplesTests.testAsyncCancellation cancels it from Swift once it is parked and again before the call starts. The test support waits for the awaiting task with a 5 s deadline instead of awaiting it directly, so an unfixed bridge fails instead of hanging the suite.

swift test --filter 'BridgeToSwiftSamplesTests|XCSkipTests' (Robolectric run through the real JNI bridge):

  • with a skip built from Propagate Swift task cancellation to bridged Kotlin async throws calls skipstone#281: 70/70 pass, and the parked coroutine observed the cancellation.
  • with the released skip 1.9.11 generator the same test fails with "The await did not resume within 5 s of the cancellation" (that is the bug). So that this PR can land ahead of the skipstone release, the test probes the generated bridge (callback_kotlinAsyncParkingFunction returning kotlinx.coroutines.Job) and throws XCTSkip when the generator predates the fix; it becomes a real check as soon as skip ships with #281.

BridgedJob is additive and the rest of the library is untouched.

Stacked on #119 (whose loadPeerLibrary change lets the Robolectric run find swiftbuild's product folder) so CI here is green on today's Swift 6.4 hosts; those commits drop out once #119 merges.

🤖 Generated with Claude Code

The --build-system native pin from skiptools#106 was a stopgap for
swiftlang/swift-build#1363, which is fixed; swiftbuild now writes
per-architecture product folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@piercifani

Copy link
Copy Markdown
Contributor Author

The ubuntu-24.04, nightly-main failure here is environmental, not from this change. Every sample module (including the untouched SkipBridgeToKotlinSamples) fails with missing library: …/swift/x86_64-unknown-linux-gnu/debug/lib*.so: the host swift build for the Robolectric run completes, but under today's toolchain it uses the swiftbuild engine ([Computing dependencies] / [Planning …] output), which does not put the libraries where the test runner looks. The last green nightly on main (09-23) built them with the native engine and a Swift 6.3.3 host; today's runners have a Swift 6.4 host.

Baseline: unchanged main dispatched through the same workflow today fails both ubuntu jobs with the identical signature — https://github.com/piercifani/skip-bridge/actions/runs/35981813831. The plain ubuntu-24.04 job on this PR passed, and it is the one that exercises the new test.

@piercifani

piercifani commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

The ubuntu-24.04, nightly-main failure was the Robolectric loader not knowing swiftbuild's output layout, fixed by #119 (loadPeerLibrary fallback). This PR is now stacked on #119 so its own CI runs green; the other three matrix jobs already passed.

piercifani and others added 3 commits September 25, 2026 23:58
loadPeerLibrary only looked in the native build system's
<triple>/debug folder, so Robolectric tests could not find the host
library once swift build defaulted to swiftbuild, which writes to
out/Products/<configuration>. Fall back to that folder when the native
path does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nightly toolchain's swiftbuild reads -module-cache-path from the
swiftc flags and took the following -Xfrontend as its value ("Cannot
recursively create directory at non-absolute path: -Xfrontend"). The
driver option is equivalent under the native build system.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…coroutine

Generated Swift for a bridged `async throws` API (skipstone) now awaits the
Kotlin `callback_` function inside `withTaskCancellationHandler`. The Kotlin
side returns the `kotlinx.coroutines.Job` running the call; `BridgedJob`
adopts it, cancels it when the Swift task is cancelled (also when the
cancellation arrives before the job exists), and maps a throwable delivered
after that cancellation to `CancellationError`.

The samples gain a Kotlin function parked in `suspendCancellableCoroutine`
whose `invokeOnCancellation` records the cancellation, and the bridge-to-Swift
tests cancel it from Swift both once it is parked and before it starts. The test
skips itself when the generated bridge predates the skipstone change, so this
can land ahead of that release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@marcprux
marcprux merged commit 1f3c1bf into skiptools:main Sep 26, 2026
5 checks passed
@piercifani
piercifani deleted the async-cancellation branch September 26, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants