Skip to content

Commit 746c781

Browse files
committed
BridgeJS: Support throws and async for closures
1 parent 453b841 commit 746c781

27 files changed

Lines changed: 3820 additions & 86 deletions

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public struct ClosureCodegen {
1616
let closureParams = signature.parameters.map { "\(sendingPrefix)\($0.closureSwiftType)" }.joined(
1717
separator: ", "
1818
)
19-
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
19+
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws(JSException)" : "")
2020
let swiftReturnType = signature.returnType.closureSwiftType
2121
return "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
2222
}
@@ -73,7 +73,17 @@ public struct ClosureCodegen {
7373
helperEnumDeclPrinter.indent {
7474
helperEnumDeclPrinter.write("let callback = JSObject.bridgeJSLiftParameter(callbackId)")
7575
let parameters: String
76-
if signature.parameters.isEmpty {
76+
if signature.isThrows || signature.isAsync {
77+
let sendingPrefix = signature.sendingParameters ? "sending " : ""
78+
let typedParams =
79+
signature.parameters.enumerated().map { index, paramType in
80+
"param\(index): \(sendingPrefix)\(paramType.closureSwiftType)"
81+
}.joined(separator: ", ")
82+
let returnType = signature.returnType.closureSwiftType
83+
let effects =
84+
(signature.isAsync ? " async" : "") + (signature.isThrows ? " throws(JSException)" : "")
85+
parameters = " (\(typedParams))\(effects) -> \(returnType)"
86+
} else if signature.parameters.isEmpty {
7787
parameters = ""
7888
} else if signature.parameters.count == 1 {
7989
parameters = " param0"
@@ -146,22 +156,25 @@ public struct ClosureCodegen {
146156
liftedParams.append("\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))")
147157
}
148158

149-
let closureCallExpr = ExprSyntax("closure(\(raw: liftedParams.joined(separator: ", ")))")
159+
let tryPrefix = signature.isThrows ? "try " : ""
160+
let closureCallExpr = ExprSyntax("\(raw: tryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))")
161+
let asyncTryPrefix = (signature.isThrows ? "try " : "") + "await "
162+
let asyncClosureCallExpr = ExprSyntax(
163+
"\(raw: asyncTryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))"
164+
)
150165

151-
let abiReturnWasmType = try signature.returnType.loweringReturnInfo().returnType
166+
let abiReturnWasmType =
167+
signature.isAsync
168+
? try BridgeType.jsObject(nil).loweringReturnInfo().returnType
169+
: try signature.returnType.loweringReturnInfo().returnType
152170

153171
// Build signature using SwiftSignatureBuilder
154172
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
155173
abiParameters: abiParams,
156174
returnType: abiReturnWasmType
157175
)
158176

159-
// Build function declaration using helper
160-
let funcDecl = SwiftCodePattern.buildExposedFunctionDecl(
161-
abiName: abiName,
162-
signature: funcSignature
163-
) { printer in
164-
printer.write("let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure")
177+
let emitCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
165178
if signature.returnType == .void {
166179
printer.write(closureCallExpr.description)
167180
} else {
@@ -189,6 +202,79 @@ public struct ClosureCodegen {
189202
}
190203
}
191204

205+
let emitAsyncCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
206+
printer.write("let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure")
207+
let resolveType = signature.returnType
208+
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
209+
let rejectName = "Promise_reject"
210+
let closureHead: String
211+
if signature.isThrows {
212+
let returnSpelling = resolveType == .void ? "" : " -> \(resolveType.closureSwiftType)"
213+
closureHead = " () async throws(JSException)\(returnSpelling) in"
214+
} else {
215+
closureHead = ""
216+
}
217+
printer.write("return _bjs_makePromise(resolve: \(resolveName), reject: \(rejectName)) {\(closureHead)")
218+
printer.indent {
219+
if resolveType == .void {
220+
printer.write(asyncClosureCallExpr.description)
221+
} else {
222+
printer.write("return \(asyncClosureCallExpr)")
223+
}
224+
}
225+
printer.write("}")
226+
}
227+
228+
let catchPlaceholderStmt = abiReturnWasmType?.swiftReturnPlaceholderStmt
229+
230+
// Build function declaration using helper
231+
let funcDecl = SwiftCodePattern.buildExposedFunctionDecl(
232+
abiName: abiName,
233+
signature: funcSignature
234+
) { printer in
235+
if signature.isAsync {
236+
emitAsyncCallAndLower(printer)
237+
} else if signature.isThrows {
238+
printer.write(
239+
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
240+
)
241+
printer.write("do {")
242+
printer.indent {
243+
emitCallAndLower(printer)
244+
}
245+
printer.write("} catch let error {")
246+
printer.indent {
247+
printer.write("if let error = error.thrownValue.object {")
248+
printer.indent {
249+
printer.write("withExtendedLifetime(error) {")
250+
printer.indent {
251+
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
252+
}
253+
printer.write("}")
254+
}
255+
printer.write("} else {")
256+
printer.indent {
257+
printer.write("let jsError = JSError(message: error.description)")
258+
printer.write("withExtendedLifetime(jsError.jsObject) {")
259+
printer.indent {
260+
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
261+
}
262+
printer.write("}")
263+
}
264+
printer.write("}")
265+
if let catchPlaceholderStmt {
266+
printer.write(catchPlaceholderStmt)
267+
}
268+
}
269+
printer.write("}")
270+
} else {
271+
printer.write(
272+
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
273+
)
274+
emitCallAndLower(printer)
275+
}
276+
}
277+
192278
return DeclSyntax(funcDecl)
193279
}
194280

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,7 @@ public struct ImportTS {
272272
}
273273
}
274274

275-
// Add exception check for ImportTS context (skipped for async, where
276-
// errors are funneled through the JS-side reject path)
277-
if !effects.isAsync && context == .importTS {
275+
if !effects.isAsync && (context == .importTS || effects.isThrows) {
278276
body.write("if let error = _swift_js_take_exception() { throw error }")
279277
}
280278
}
@@ -323,18 +321,19 @@ public struct ImportTS {
323321
let innerBody = body
324322
body = CodeFragmentPrinter()
325323

324+
let tryKeyword = effects.isThrows ? "try" : "try!"
326325
let rejectFactory = "makeRejectClosure: { JSTypedClosure<(sending JSValue) -> Void>($0) }"
327326
if returnType == .void {
328327
let resolveFactory = "makeResolveClosure: { JSTypedClosure<() -> Void>($0) }"
329328
body.write(
330-
"try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
329+
"\(tryKeyword) await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
331330
)
332331
} else {
333332
let resolveSwiftType = returnType.closureSwiftType
334333
let resolveFactory =
335334
"makeResolveClosure: { JSTypedClosure<(sending \(resolveSwiftType)) -> Void>($0) }"
336335
body.write(
337-
"let resolved = try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
336+
"let resolved = \(tryKeyword) await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
338337
)
339338
}
340339
body.indent {

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,40 @@ public final class SwiftToSkeleton {
191191
}
192192

193193
let isAsync = functionType.effectSpecifiers?.asyncSpecifier != nil
194-
let isThrows = functionType.effectSpecifiers?.throwsClause != nil
194+
195+
if isAsync, !returnType.isAsyncResolvable {
196+
errors.append(
197+
DiagnosticError(
198+
node: functionType,
199+
message:
200+
"Returning '\(returnType.swiftType)' from an async closure is not yet supported",
201+
hint:
202+
"Return a type lowerable through the async resolve ABI "
203+
+ "(String/Int/Bool/Double/Float/raw-value or case-only enum/@JS struct/JSObject/Optional/Array/Dictionary), "
204+
+ "or make the closure non-async."
205+
)
206+
)
207+
return nil
208+
}
209+
210+
var isThrows = false
211+
if let throwsClause = functionType.effectSpecifiers?.throwsClause {
212+
guard let thrownType = throwsClause.type,
213+
thrownType.trimmedDescription == "JSException"
214+
else {
215+
errors.append(
216+
DiagnosticError(
217+
node: throwsClause,
218+
message:
219+
"Only JSException is supported for thrown type of Swift closures, "
220+
+ "got \(throwsClause.type?.trimmedDescription ?? "unspecified")",
221+
hint: "Annotate the closure as `throws(JSException)`"
222+
)
223+
)
224+
return nil
225+
}
226+
isThrows = true
227+
}
195228

196229
return .closure(
197230
ClosureSignature(
@@ -1028,22 +1061,6 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
10281061
guard let type = resolvedType else {
10291062
continue // Skip unsupported types
10301063
}
1031-
if case .closure(let signature, _) = type {
1032-
if signature.isAsync {
1033-
diagnose(
1034-
node: param.type,
1035-
message: "Async is not supported for Swift closures yet."
1036-
)
1037-
continue
1038-
}
1039-
if signature.isThrows {
1040-
diagnose(
1041-
node: param.type,
1042-
message: "Throws is not supported for Swift closures yet."
1043-
)
1044-
continue
1045-
}
1046-
}
10471064
if case .nullable(let wrappedType, _) = type, wrappedType.isOptional {
10481065
diagnoseNestedOptional(node: param.type, type: param.type.trimmedDescription)
10491066
continue

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ public struct BridgeJSLink {
894894
) throws -> [String] {
895895
let printer = CodeFragmentPrinter()
896896
let builder = ExportedThunkBuilder(
897-
effects: Effects(isAsync: false, isThrows: true),
897+
effects: Effects(isAsync: signature.isAsync, isThrows: signature.isAsync ? signature.isThrows : true),
898898
hasDirectAccessToSwiftClass: false,
899899
intrinsicRegistry: intrinsicRegistry
900900
)
@@ -3743,7 +3743,9 @@ extension BridgeType {
37433743
let paramTypes = signature.parameters.enumerated().map { index, param in
37443744
"arg\(index): \(param.tsType)"
37453745
}.joined(separator: ", ")
3746-
return "(\(paramTypes)) => \(signature.returnType.tsType)"
3746+
let returnTS =
3747+
signature.isAsync ? "Promise<\(signature.returnType.tsType)>" : signature.returnType.tsType
3748+
return "(\(paramTypes)) => \(returnTS)"
37473749
case .array(let elementType):
37483750
let inner = elementType.tsType
37493751
if inner.contains("|") || inner.contains("=>") {

0 commit comments

Comments
 (0)