Skip to content

Commit ab2bdb3

Browse files
authored
Merge pull request swiftwasm#760 from PassiveLogic/kr/fix-async-thunk-captureless
BridgeJS: Fix reject path of zero-parameter async throwing exports
2 parents 453b841 + 1f2fe86 commit ab2bdb3

10 files changed

Lines changed: 139 additions & 27 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -426,21 +426,45 @@ public class ExportSwift {
426426
/// A throwing async body needs an explicit closure type, otherwise Swift infers
427427
/// `throws(any Error)` instead of `throws(JSException)`.
428428
/// See: https://github.com/swiftlang/swift/issues/76165
429-
private func asyncThrowsClosureHead(returnSpelling: String?) -> String {
429+
private func asyncThrowsClosureHead(returnSpelling: String?, forcesCapture: Bool) -> String {
430430
guard effects.isThrows else { return "" }
431431
let returns = returnSpelling.map { " -> \($0)" } ?? ""
432-
return " () async throws(JSException)\(returns) in"
432+
let capture = forcesCapture ? "[__bjs_capture] " : ""
433+
return " \(capture)() async throws(JSException)\(returns) in"
434+
}
435+
436+
/// A captureless throwing async body closure lowers via `thin_to_thick_function`,
437+
/// which miscompiles typed-error calls on wasm32. Forcing a capture that the body
438+
/// reads turns the closure into a partial apply with a context, avoiding the
439+
/// broken convention. An unread capture list entry is dropped by capture analysis,
440+
/// so the body must also read the captured value.
441+
/// See: https://github.com/swiftlang/swift/issues/89320
442+
private var asyncThrowsBodyForcesCapture: Bool {
443+
effects.isThrows && abiParameterSignatures.isEmpty && asyncHoistedBindings.isEmpty
433444
}
434445

435446
func render(abiName: String) -> DeclSyntax {
436447
let body: CodeBlockItemListSyntax
437448
if effects.isAsync, let resolveType = asyncResolveReturnType {
438449
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
439-
let closureHead = asyncThrowsClosureHead(returnSpelling: resolveType.swiftType)
450+
let forcesCapture = asyncThrowsBodyForcesCapture
451+
let closureHead = asyncThrowsClosureHead(
452+
returnSpelling: resolveType.swiftType,
453+
forcesCapture: forcesCapture
454+
)
455+
var hoistedBindings = asyncHoistedBindings
456+
var bodyItems = self.body
457+
if forcesCapture {
458+
hoistedBindings.append("let __bjs_capture = 0")
459+
if !bodyItems.isEmpty {
460+
bodyItems[0] = bodyItems[0].with(\.leadingTrivia, .newline)
461+
}
462+
bodyItems.insert("_ = __bjs_capture", at: 0)
463+
}
440464
body = """
441-
\(CodeBlockItemListSyntax(asyncHoistedBindings))
465+
\(CodeBlockItemListSyntax(hoistedBindings))
442466
return _bjs_makePromise(resolve: \(raw: resolveName), reject: Promise_reject) {\(raw: closureHead)
443-
\(CodeBlockItemListSyntax(self.body))
467+
\(CodeBlockItemListSyntax(bodyItems))
444468
}
445469
"""
446470
} else if effects.isThrows {

Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Async.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
return v
3232
}
3333

34+
@JS func asyncThrowsZeroArg() async throws(JSException) -> String {
35+
return "ok"
36+
}
37+
3438
@JS func asyncCombineStructs(_ a: AsyncPoint, _ b: AsyncPoint) async -> AsyncPoint {
3539
return AsyncPoint(x: a.x + b.x, y: a.y + b.y)
3640
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,23 @@
283283
}
284284
}
285285
},
286+
{
287+
"abiName" : "bjs_asyncThrowsZeroArg",
288+
"effects" : {
289+
"isAsync" : true,
290+
"isStatic" : false,
291+
"isThrows" : true
292+
},
293+
"name" : "asyncThrowsZeroArg",
294+
"parameters" : [
295+
296+
],
297+
"returnType" : {
298+
"string" : {
299+
300+
}
301+
}
302+
},
286303
{
287304
"abiName" : "bjs_asyncCombineStructs",
288305
"effects" : {

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ public func _bjs_asyncRoundTripStructThrows() -> Int32 {
194194
#endif
195195
}
196196

197+
@_expose(wasm, "bjs_asyncThrowsZeroArg")
198+
@_cdecl("bjs_asyncThrowsZeroArg")
199+
public func _bjs_asyncThrowsZeroArg() -> Int32 {
200+
#if arch(wasm32)
201+
let __bjs_capture = 0
202+
return _bjs_makePromise(resolve: Promise_resolve_SS, reject: Promise_reject) { [__bjs_capture] () async throws(JSException) -> String in
203+
_ = __bjs_capture
204+
return try await asyncThrowsZeroArg()
205+
}
206+
#else
207+
fatalError("Only available on WebAssembly")
208+
#endif
209+
}
210+
197211
@_expose(wasm, "bjs_asyncCombineStructs")
198212
@_cdecl("bjs_asyncCombineStructs")
199213
public func _bjs_asyncCombineStructs() -> Int32 {

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type Exports = {
3434
asyncRoundTripJSObject(v: any): Promise<any>;
3535
asyncRoundTripStruct(v: AsyncPoint): Promise<AsyncPoint>;
3636
asyncRoundTripStructThrows(v: AsyncPoint): Promise<AsyncPoint>;
37+
asyncThrowsZeroArg(): Promise<string>;
3738
asyncCombineStructs(a: AsyncPoint, b: AsyncPoint): Promise<AsyncPoint>;
3839
asyncRoundTripEnum(v: AsyncDirectionTag): Promise<AsyncDirectionTag>;
3940
asyncRoundTripRawEnum(v: AsyncThemeTag): Promise<AsyncThemeTag>;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,18 @@ export async function createInstantiator(options, swift) {
585585
}
586586
return ret1;
587587
},
588+
asyncThrowsZeroArg: function bjs_asyncThrowsZeroArg() {
589+
const ret = instance.exports.bjs_asyncThrowsZeroArg();
590+
const ret1 = swift.memory.getObject(ret);
591+
swift.memory.release(ret);
592+
if (tmpRetException) {
593+
const error = swift.memory.getObject(tmpRetException);
594+
swift.memory.release(tmpRetException);
595+
tmpRetException = undefined;
596+
throw error;
597+
}
598+
return ret1;
599+
},
588600
asyncCombineStructs: function bjs_asyncCombineStructs(a, b) {
589601
structHelpers.AsyncPoint.lower(a);
590602
structHelpers.AsyncPoint.lower(b);

Tests/BridgeJSRuntimeTests/ExportAPITests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ struct TestError: Error {
9696
@JS func throwsWithSwiftHeapObjectResult() throws(JSException) -> Greeter { return Greeter(name: "Test") }
9797
@JS func throwsWithJSObjectResult() throws(JSException) -> JSObject { return JSObject() }
9898

99+
@JS func zeroArgAsyncThrows() async throws(JSException) -> String {
100+
throw JSException(JSError(message: "ZeroArgAsyncThrowsError").jsValue)
101+
}
102+
99103
@JS func asyncRoundTripVoid() async -> Void { return }
100104
@JS func asyncRoundTripInt(v: Int) async -> Int { return v }
101105
@JS func asyncRoundTripFloat(v: Float) async -> Float { return v }

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7189,6 +7189,20 @@ public func _bjs_throwsWithJSObjectResult() -> Int32 {
71897189
#endif
71907190
}
71917191

7192+
@_expose(wasm, "bjs_zeroArgAsyncThrows")
7193+
@_cdecl("bjs_zeroArgAsyncThrows")
7194+
public func _bjs_zeroArgAsyncThrows() -> Int32 {
7195+
#if arch(wasm32)
7196+
let __bjs_capture = 0
7197+
return _bjs_makePromise(resolve: Promise_resolve_SS, reject: Promise_reject) { [__bjs_capture] () async throws(JSException) -> String in
7198+
_ = __bjs_capture
7199+
return try await zeroArgAsyncThrows()
7200+
}
7201+
#else
7202+
fatalError("Only available on WebAssembly")
7203+
#endif
7204+
}
7205+
71927206
@_expose(wasm, "bjs_asyncRoundTripVoid")
71937207
@_cdecl("bjs_asyncRoundTripVoid")
71947208
public func _bjs_asyncRoundTripVoid() -> Int32 {
@@ -11403,6 +11417,28 @@ func _$Promise_reject(_ promise: JSObject, _ value: JSValue) throws(JSException)
1140311417
if let error = _swift_js_take_exception() { throw error }
1140411418
}
1140511419

11420+
@JSFunction func Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException)
11421+
11422+
#if arch(wasm32)
11423+
@_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_SS")
11424+
fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void
11425+
#else
11426+
fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void {
11427+
fatalError("Only available on WebAssembly")
11428+
}
11429+
#endif
11430+
@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_SS(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void {
11431+
return promise_resolve_BridgeJSRuntimeTests_SS_extern(promise, valueBytes, valueLength)
11432+
}
11433+
11434+
func _$Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) -> Void {
11435+
let promiseValue = promise.bridgeJSLowerParameter()
11436+
value.bridgeJSWithLoweredParameter { (valueBytes, valueLength) in
11437+
promise_resolve_BridgeJSRuntimeTests_SS(promiseValue, valueBytes, valueLength)
11438+
}
11439+
if let error = _swift_js_take_exception() { throw error }
11440+
}
11441+
1140611442
@JSFunction func Promise_resolve_y(_ promise: JSObject) throws(JSException)
1140711443

1140811444
#if arch(wasm32)
@@ -11507,28 +11543,6 @@ func _$Promise_resolve_Sb(_ promise: JSObject, _ value: Bool) throws(JSException
1150711543
if let error = _swift_js_take_exception() { throw error }
1150811544
}
1150911545

11510-
@JSFunction func Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException)
11511-
11512-
#if arch(wasm32)
11513-
@_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_SS")
11514-
fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void
11515-
#else
11516-
fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void {
11517-
fatalError("Only available on WebAssembly")
11518-
}
11519-
#endif
11520-
@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_SS(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void {
11521-
return promise_resolve_BridgeJSRuntimeTests_SS_extern(promise, valueBytes, valueLength)
11522-
}
11523-
11524-
func _$Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) -> Void {
11525-
let promiseValue = promise.bridgeJSLowerParameter()
11526-
value.bridgeJSWithLoweredParameter { (valueBytes, valueLength) in
11527-
promise_resolve_BridgeJSRuntimeTests_SS(promiseValue, valueBytes, valueLength)
11528-
}
11529-
if let error = _swift_js_take_exception() { throw error }
11530-
}
11531-
1153211546
@JSFunction func Promise_resolve_7GreeterC(_ promise: JSObject, _ value: Greeter) throws(JSException)
1153311547

1153411548
#if arch(wasm32)

Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12171,6 +12171,23 @@
1217112171
}
1217212172
}
1217312173
},
12174+
{
12175+
"abiName" : "bjs_zeroArgAsyncThrows",
12176+
"effects" : {
12177+
"isAsync" : true,
12178+
"isStatic" : false,
12179+
"isThrows" : true
12180+
},
12181+
"name" : "zeroArgAsyncThrows",
12182+
"parameters" : [
12183+
12184+
],
12185+
"returnType" : {
12186+
"string" : {
12187+
12188+
}
12189+
}
12190+
},
1217412191
{
1217512192
"abiName" : "bjs_asyncRoundTripVoid",
1217612193
"effects" : {

Tests/BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export async function runAsyncWorksTests(exports) {
6464
(error) => error instanceof Error && error.message === "async struct failure"
6565
);
6666

67+
await assert.rejects(
68+
() => exports.zeroArgAsyncThrows(),
69+
(error) => error instanceof Error && error.message === "ZeroArgAsyncThrowsError"
70+
);
71+
6772
const richContact = {
6873
name: "Alice",
6974
age: 30,

0 commit comments

Comments
 (0)