BridgeJS: Support throws and async for closures#766
Merged
Conversation
142d0c7 to
c020734
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.
Overview
Adds
throws(JSException)andasync(andasync throws(JSException)) support to BridgeJS-bridged closures, in both directions: closures Swift receives from JavaScript, and closures Swift hands to JavaScript. Previously closures were limited to synchronous, non-throwing signatures (#537).Async settlement reuses the
_bjs_makePromise+ per-typePromise_resolve_<mangled>/Promise_rejectmachinery introduced in #758, so every bridged return type is inherited for free, including the associated-value enums added in #764; no parallel mechanism is introduced.1. Effect-aware closure mangling. Closure ABI symbol names encode effects with the Swift ABI operators (
Yafor async,Kfor throws), so signatures that differ only by effects no longer collide.2. Throwing closures (both directions). A JavaScript callback's thrown
JSExceptionpropagates into Swift; a Swift closure's thrownJSExceptionis routed back to JavaScript. Onlythrows(JSException)is supported; plainthrowsis diagnosed.3. Async closures (both directions). A JS-to-Swift callback is awaited via
_bjs_awaitPromise; a Swift-to-JS closure returns aPromisesettled via_bjs_makePromise. TypeScript surfaces these as(args) => Promise<R>. Supported return types mirror async functions, including@JS struct, enums (associated-value included), andOptional/Array/Dictionarycompositions; the remaining unsupported async return types (protocols, namespace enums) are diagnosed rather than miscompiled.4. Lifetime. The closure box survives across suspension without an explicit pin: the extracted closure value, captured by the settling
Task, keeps it alive even if JavaScript releases the closure mid-flight. Covered by release-race and concurrent-invocation tests.Commit structure
The work is split so the parts can be reviewed (or dropped) independently:
Commit 1 is the feature itself, self-contained.
Commit 2 adds a build-time warning for async throwing closures handed to JavaScript (introducing a warning severity to the BridgeJS diagnostics). It was written when the swiftlang/swift#89320 miscompile made that signature lose thrown errors at runtime; with commit 3 in place the warning no longer fires for a real risk, so this commit can be dropped, or kept if the warning-severity infrastructure is useful on its own.
Commit 3 works around swiftlang/swift#89320 by boxing
JSException's stored properties into a private final class, shrinking the struct to a single stored reference so it travels in the direct typed-error convention and the miscompiled indirect-error path is never taken. The public API is unchanged; the cost is one heap allocation per thrown exception, on the error path only. With it, the previously gated Swift-to-JSasync throwsreject assertions run unconditionally and pass, and theJSClosure.asyncreject path gains its previously missing test. Removal once swiftlang/swift#89715 ships is tracked in #767 and is a library-internal layout change, not a breaking API change. Original exploration: PassiveLogic#13. The codegen-level counterpart for zero-parameter async throwing exports landed in #760 (tracked for removal in #761).Tests
Codegen snapshots and end-to-end runtime round-trips (both directions, throwing and async, resolve and reject,
Void,@JS struct, associated-value enum returns, the release-race, and concurrent calls) pass.