Skip to content

Commit 89de6cd

Browse files
committed
Box JSException storage in a class to fit the direct typed-error convention
1 parent 982e9fb commit 89de6cd

10 files changed

Lines changed: 139 additions & 188 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -610,37 +610,6 @@ private enum ExportSwiftConstants {
610610
static let supportedRawTypes = SwiftEnumRawType.supportedTypeNames
611611
}
612612

613-
/// Warns about Swift closures handed to JavaScript with an `async throws(JSException)` signature.
614-
/// Captureless closure values lose their thrown error at runtime due to a Swift compiler bug.
615-
private func asyncThrowsClosureWarning(node: some SyntaxProtocol) -> DiagnosticError {
616-
DiagnosticError(
617-
node: node,
618-
message:
619-
"async throwing closures passed to JavaScript may lose thrown errors due to a Swift compiler bug "
620-
+ "(swiftlang/swift#89320) unless the closure value captures state",
621-
hint:
622-
"Pass a closure that captures state, or see the BridgeJS closure documentation for details",
623-
severity: .warning
624-
)
625-
}
626-
627-
extension BridgeType {
628-
fileprivate var containsAsyncThrowsClosure: Bool {
629-
switch self {
630-
case .closure(let signature, _):
631-
return signature.isAsync && signature.isThrows
632-
case .nullable(let wrapped, _):
633-
return wrapped.containsAsyncThrowsClosure
634-
case .array(let element):
635-
return element.containsAsyncThrowsClosure
636-
case .dictionary(let value):
637-
return value.containsAsyncThrowsClosure
638-
default:
639-
return false
640-
}
641-
}
642-
}
643-
644613
extension AttributeSyntax {
645614
/// The attribute name as text when it is a simple identifier (e.g. "JS", "JSFunction").
646615
/// Prefer this over `attributeName.trimmedDescription` for name checks to avoid unnecessary string work.
@@ -1233,9 +1202,6 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
12331202

12341203
guard let type = resolvedType else { return nil }
12351204
returnType = type
1236-
if returnType.containsAsyncThrowsClosure {
1237-
errors.append(asyncThrowsClosureWarning(node: returnClause.type))
1238-
}
12391205
} else {
12401206
returnType = .void
12411207
}
@@ -2895,11 +2861,6 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
28952861
guard let bridgeType = withLookupErrors({ parent.lookupType(for: type, errors: &$0) }) else {
28962862
return nil
28972863
}
2898-
if case .closure(let signature, useJSTypedClosure: true) = bridgeType,
2899-
signature.isAsync, signature.isThrows
2900-
{
2901-
errors.append(asyncThrowsClosureWarning(node: type))
2902-
}
29032864
let nameToken = param.secondName ?? param.firstName
29042865
let name = SwiftToSkeleton.normalizeIdentifier(nameToken.text)
29052866
let labelToken = param.secondName == nil ? nil : param.firstName

Plugins/BridgeJS/Tests/BridgeJSToolTests/ClosureAsyncThrowsWarningTests.swift

Lines changed: 0 additions & 122 deletions
This file was deleted.

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Bringing-Swift-Closures-to-JavaScript.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ const count = await fetchCount("/items"); // Promise<number>
7878
7979
**Cancellation is a non-goal.** There is no propagation between a Swift `Task` and a JavaScript `Promise` in either direction.
8080

81-
> Note: The reject path of async throwing typed closures is affected by a Swift compiler bug ([swiftlang/swift#89320](https://github.com/swiftlang/swift/issues/89320)); BridgeJS emits a build-time warning for this signature. See <doc:Exporting-Swift-Closure> for details.
82-
8381
## Lifetime and release()
8482

8583
A ``JSTypedClosure`` keeps the Swift closure alive and exposes a JavaScript function that calls into it. To avoid leaks and use-after-free:

Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,6 @@ Notes:
239239
- The same `JavaScriptEventLoop.installGlobalExecutor()` requirement applies as for async functions; there is no special handling for closures.
240240
- **Cancellation is a non-goal.** There is no propagation between a Swift `Task` and a JavaScript `Promise` in either direction; cancelling one side does not cancel the other.
241241
242-
> Warning: When an async throwing closure handed to JavaScript throws, the error is currently lost instead of rejecting the `Promise` with it, due to a Swift compiler bug on `wasm32` ([swiftlang/swift#89320](https://github.com/swiftlang/swift/issues/89320), fix in progress in [swiftlang/swift#89715](https://github.com/swiftlang/swift/pull/89715)). Closures that capture state are unaffected, as are throwing JavaScript callbacks passed into Swift. BridgeJS emits a build-time warning for this signature.
243-
244242
## Supported Features
245243
246244
| Swift Feature | Status |
@@ -251,7 +249,7 @@ Notes:
251249
| Optional types in closures | ✅ |
252250
| Closure-typed `@JS` properties | ❌ |
253251
| Async closures `(A) async -> B` | ✅ |
254-
| Async throwing closures `(A) async throws(JSException) -> B` | ✅ (reject path of closures handed to JS pending [swiftlang/swift#89320](https://github.com/swiftlang/swift/issues/89320)) |
252+
| Async throwing closures `(A) async throws(JSException) -> B` | ✅ (rejecting into JS requires Swift 6.3 or later) |
255253
| Throwing closures `(A) throws(JSException) -> B` | ✅ |
256254
257255
## See Also

Sources/JavaScriptKit/JSException.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,72 @@
1313
/// }
1414
/// ```
1515
public struct JSException: Error, Equatable, CustomStringConvertible {
16+
#if compiler(>=6.3)
17+
/// Boxes the exception payload in a class so `JSException` stays within the direct
18+
/// typed-error convention on wasm32. The boxed layout crashes the Swift 6.1 wasm IRGen and is unverified on 6.2,
19+
/// so older compilers keep the original stored properties.
20+
private final class Storage {
21+
let thrownValue: JSValue
22+
let description: String
23+
let stack: String?
24+
25+
init(thrownValue: JSValue, description: String, stack: String?) {
26+
self.thrownValue = thrownValue
27+
self.description = description
28+
self.stack = stack
29+
}
30+
}
31+
32+
/// The boxed payload of the exception.
33+
///
34+
/// Marked as `nonisolated(unsafe)` to satisfy `Sendable` requirement
35+
/// from `Error` protocol.
36+
private nonisolated(unsafe) let storage: Storage
37+
38+
/// The value thrown from JavaScript.
39+
/// This can be any JavaScript value (error object, string, number, etc.).
40+
public var thrownValue: JSValue {
41+
return storage.thrownValue
42+
}
43+
44+
/// A description of the exception.
45+
public var description: String {
46+
return storage.description
47+
}
48+
49+
/// The stack trace of the exception.
50+
public var stack: String? {
51+
return storage.stack
52+
}
53+
54+
/// Initializes a new JSException instance with a value thrown from JavaScript.
55+
///
56+
/// Only available within the package. This must be called on the thread where the exception object created.
57+
/// The stringified representation is captured on the object owner thread to bring useful info
58+
/// to the catching thread even if they are different threads.
59+
@usableFromInline
60+
package init(_ thrownValue: JSValue) {
61+
if let errorObject = thrownValue.object, let stack = errorObject.stack.string {
62+
self.storage = Storage(
63+
thrownValue: thrownValue,
64+
description: "JSException(\(stack))",
65+
stack: stack
66+
)
67+
} else {
68+
self.storage = Storage(
69+
thrownValue: thrownValue,
70+
description: "JSException(\(thrownValue))",
71+
stack: nil
72+
)
73+
}
74+
}
75+
76+
public static func == (lhs: JSException, rhs: JSException) -> Bool {
77+
return lhs.storage.thrownValue == rhs.storage.thrownValue
78+
&& lhs.storage.description == rhs.storage.description
79+
&& lhs.storage.stack == rhs.storage.stack
80+
}
81+
#else
1682
/// The value thrown from JavaScript.
1783
/// This can be any JavaScript value (error object, string, number, etc.).
1884
public var thrownValue: JSValue {
@@ -47,6 +113,7 @@ public struct JSException: Error, Equatable, CustomStringConvertible {
47113
self.stack = nil
48114
}
49115
}
116+
#endif
50117

51118
/// Initializes a new JavaScript `Error` instance with a message and prepare it to be thrown.
52119
///

Tests/BridgeJSRuntimeTests/ClosureAsyncAPIs.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ import JavaScriptEventLoop
4242
return AsyncRecorderState.lastRecorded
4343
}
4444

45+
@JS func asyncThrowsClosureRejectSupported() -> Bool {
46+
#if compiler(>=6.3)
47+
return true
48+
#else
49+
return false
50+
#endif
51+
}
52+
4553
@JS func makeAsyncPointMaker() -> JSTypedClosure<(Double) async -> DataPoint> {
4654
return JSTypedClosure { (seed: Double) async -> DataPoint in
4755
await Task.yield()

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7333,6 +7333,17 @@ public func _bjs_lastRecordedValue() -> Void {
73337333
#endif
73347334
}
73357335

7336+
@_expose(wasm, "bjs_asyncThrowsClosureRejectSupported")
7337+
@_cdecl("bjs_asyncThrowsClosureRejectSupported")
7338+
public func _bjs_asyncThrowsClosureRejectSupported() -> Int32 {
7339+
#if arch(wasm32)
7340+
let ret = asyncThrowsClosureRejectSupported()
7341+
return ret.bridgeJSLowerReturn()
7342+
#else
7343+
fatalError("Only available on WebAssembly")
7344+
#endif
7345+
}
7346+
73367347
@_expose(wasm, "bjs_makeAsyncPointMaker")
73377348
@_cdecl("bjs_makeAsyncPointMaker")
73387349
public func _bjs_makeAsyncPointMaker() -> Int32 {

Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11632,6 +11632,23 @@
1163211632
}
1163311633
}
1163411634
},
11635+
{
11636+
"abiName" : "bjs_asyncThrowsClosureRejectSupported",
11637+
"effects" : {
11638+
"isAsync" : false,
11639+
"isStatic" : false,
11640+
"isThrows" : false
11641+
},
11642+
"name" : "asyncThrowsClosureRejectSupported",
11643+
"parameters" : [
11644+
11645+
],
11646+
"returnType" : {
11647+
"bool" : {
11648+
11649+
}
11650+
}
11651+
},
1163511652
{
1163611653
"abiName" : "bjs_makeAsyncPointMaker",
1163711654
"effects" : {

0 commit comments

Comments
 (0)