Skip to content

Commit 6c28fc7

Browse files
BridgeJS: Add support for throwing JSException from Swift
1 parent 3bf63a1 commit 6c28fc7

40 files changed

+518
-37
lines changed

Plugins/BridgeJS/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,4 @@ TBD
135135
declare var Foo: FooConstructor;
136136
```
137137
- [ ] Use `externref` once it's widely available
138+
- [ ] Test SwiftObject roundtrip

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ struct BridgeJSLink {
111111
112112
let tmpRetString;
113113
let tmpRetBytes;
114+
let tmpRetException;
114115
return {
115116
/** @param {WebAssembly.Imports} importObject */
116117
addImports: (importObject) => {
@@ -134,6 +135,9 @@ struct BridgeJSLink {
134135
target.set(tmpRetBytes);
135136
tmpRetBytes = undefined;
136137
}
138+
bjs["swift_js_throw"] = function(id) {
139+
tmpRetException = swift.memory.retainByRef(id);
140+
}
137141
bjs["swift_js_retain"] = function(id) {
138142
return swift.memory.retainByRef(id);
139143
}
@@ -188,6 +192,11 @@ struct BridgeJSLink {
188192
var bodyLines: [String] = []
189193
var cleanupLines: [String] = []
190194
var parameterForwardings: [String] = []
195+
let effects: Effects
196+
197+
init(effects: Effects) {
198+
self.effects = effects
199+
}
191200

192201
func lowerParameter(param: Parameter) {
193202
switch param.type {
@@ -245,7 +254,24 @@ struct BridgeJSLink {
245254
}
246255

247256
func callConstructor(abiName: String) -> String {
248-
return "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
257+
let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
258+
bodyLines.append("const ret = \(call);")
259+
return "ret"
260+
}
261+
262+
func checkExceptionLines() -> [String] {
263+
guard effects.isThrows else {
264+
return []
265+
}
266+
return [
267+
"if (tmpRetException) {",
268+
// TODO: Implement "take" operation
269+
" const error = swift.memory.getObject(tmpRetException);",
270+
" swift.memory.release(tmpRetException);",
271+
" tmpRetException = undefined;",
272+
" throw error;",
273+
"}",
274+
]
249275
}
250276

251277
func renderFunction(
@@ -261,6 +287,7 @@ struct BridgeJSLink {
261287
)
262288
funcLines.append(contentsOf: bodyLines.map { $0.indent(count: 4) })
263289
funcLines.append(contentsOf: cleanupLines.map { $0.indent(count: 4) })
290+
funcLines.append(contentsOf: checkExceptionLines().map { $0.indent(count: 4) })
264291
if let returnExpr = returnExpr {
265292
funcLines.append("return \(returnExpr);".indent(count: 4))
266293
}
@@ -274,7 +301,7 @@ struct BridgeJSLink {
274301
}
275302

276303
func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) {
277-
let thunkBuilder = ExportedThunkBuilder()
304+
let thunkBuilder = ExportedThunkBuilder(effects: function.effects)
278305
for param in function.parameters {
279306
thunkBuilder.lowerParameter(param: param)
280307
}
@@ -304,16 +331,17 @@ struct BridgeJSLink {
304331
jsLines.append("class \(klass.name) extends SwiftHeapObject {")
305332

306333
if let constructor: ExportedConstructor = klass.constructor {
307-
let thunkBuilder = ExportedThunkBuilder()
334+
let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects)
308335
for param in constructor.parameters {
309336
thunkBuilder.lowerParameter(param: param)
310337
}
311-
let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName)
312338
var funcLines: [String] = []
313339
funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
340+
let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName)
314341
funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) })
315-
funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4))
316342
funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) })
343+
funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) })
344+
funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4))
317345
funcLines.append("}")
318346
jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })
319347

@@ -324,7 +352,7 @@ struct BridgeJSLink {
324352
}
325353

326354
for method in klass.methods {
327-
let thunkBuilder = ExportedThunkBuilder()
355+
let thunkBuilder = ExportedThunkBuilder(effects: method.effects)
328356
thunkBuilder.lowerSelf()
329357
for param in method.parameters {
330358
thunkBuilder.lowerParameter(param: param)

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ struct Parameter: Codable {
1616
let type: BridgeType
1717
}
1818

19+
struct Effects: Codable {
20+
var isAsync: Bool
21+
var isThrows: Bool
22+
}
23+
1924
// MARK: - Exported Skeleton
2025

2126
struct ExportedFunction: Codable {
2227
var name: String
2328
var abiName: String
2429
var parameters: [Parameter]
2530
var returnType: BridgeType
31+
var effects: Effects
2632
}
2733

2834
struct ExportedClass: Codable {
@@ -34,6 +40,7 @@ struct ExportedClass: Codable {
3440
struct ExportedConstructor: Codable {
3541
var abiName: String
3642
var parameters: [Parameter]
43+
var effects: Effects
3744
}
3845

3946
struct ExportedSkeleton: Codable {

Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,37 @@ class ExportSwift {
155155
abiName = "bjs_\(className)_\(name)"
156156
}
157157

158+
guard let effects = collectEffects(signature: node.signature) else {
159+
return nil
160+
}
161+
158162
return ExportedFunction(
159163
name: name,
160164
abiName: abiName,
161165
parameters: parameters,
162-
returnType: returnType
166+
returnType: returnType,
167+
effects: effects
163168
)
164169
}
165170

171+
private func collectEffects(signature: FunctionSignatureSyntax) -> Effects? {
172+
let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil
173+
var isThrows = false
174+
if let throwsClause: ThrowsClauseSyntax = signature.effectSpecifiers?.throwsClause {
175+
// Limit the thrown type to JSException for now
176+
guard let thrownType = throwsClause.type else {
177+
diagnose(node: throwsClause, message: "Thrown type is not specified, only JSException is supported for now")
178+
return nil
179+
}
180+
guard thrownType.trimmedDescription == "JSException" else {
181+
diagnose(node: throwsClause, message: "Only JSException is supported for thrown type, got \(thrownType.trimmedDescription)")
182+
return nil
183+
}
184+
isThrows = true
185+
}
186+
return Effects(isAsync: isAsync, isThrows: isThrows)
187+
}
188+
166189
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
167190
guard node.attributes.hasJSAttribute() else { return .skipChildren }
168191
guard case .classBody(let name) = state else {
@@ -180,9 +203,14 @@ class ExportSwift {
180203
parameters.append(Parameter(label: label, name: name, type: type))
181204
}
182205

206+
guard let effects = collectEffects(signature: node.signature) else {
207+
return .skipChildren
208+
}
209+
183210
let constructor = ExportedConstructor(
184211
abiName: "bjs_\(name)_init",
185-
parameters: parameters
212+
parameters: parameters,
213+
effects: effects
186214
)
187215
exportedClasses[name]?.constructor = constructor
188216
return .skipChildren
@@ -245,6 +273,8 @@ class ExportSwift {
245273
246274
@_extern(wasm, module: "bjs", name: "swift_js_retain")
247275
private func _swift_js_retain(_ ptr: Int32) -> Int32
276+
@_extern(wasm, module: "bjs", name: "swift_js_throw")
277+
private func _swift_js_throw(_ id: Int32)
248278
"""
249279

250280
func renderSwiftGlue() -> String? {
@@ -268,6 +298,11 @@ class ExportSwift {
268298
var abiParameterForwardings: [LabeledExprSyntax] = []
269299
var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
270300
var abiReturnType: WasmCoreType?
301+
let effects: Effects
302+
303+
init(effects: Effects) {
304+
self.effects = effects
305+
}
271306

272307
func liftParameter(param: Parameter) {
273308
switch param.type {
@@ -350,35 +385,36 @@ class ExportSwift {
350385
}
351386
}
352387

353-
func call(name: String, returnType: BridgeType) {
388+
private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> StmtSyntax {
389+
var callExpr: ExprSyntax = "\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
390+
if effects.isAsync {
391+
callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr))
392+
}
393+
if effects.isThrows {
394+
callExpr = ExprSyntax(TryExprSyntax(
395+
tryKeyword: .keyword(.try).with(\.trailingTrivia, .space),
396+
expression: callExpr
397+
))
398+
}
354399
let retMutability = returnType == .string ? "var" : "let"
355-
let callExpr: ExprSyntax =
356-
"\(raw: name)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
357400
if returnType == .void {
358-
body.append("\(raw: callExpr)")
401+
return StmtSyntax("\(raw: callExpr)")
359402
} else {
360-
body.append(
361-
"""
362-
\(raw: retMutability) ret = \(raw: callExpr)
363-
"""
364-
)
403+
return StmtSyntax("\(raw: retMutability) ret = \(raw: callExpr)")
365404
}
366405
}
367406

407+
func call(name: String, returnType: BridgeType) {
408+
let stmt = renderCallStatement(callee: "\(raw: name)", returnType: returnType)
409+
body.append(CodeBlockItemSyntax(item: .stmt(stmt)))
410+
}
411+
368412
func callMethod(klassName: String, methodName: String, returnType: BridgeType) {
369413
let _selfParam = self.abiParameterForwardings.removeFirst()
370-
let retMutability = returnType == .string ? "var" : "let"
371-
let callExpr: ExprSyntax =
372-
"\(raw: _selfParam).\(raw: methodName)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
373-
if returnType == .void {
374-
body.append("\(raw: callExpr)")
375-
} else {
376-
body.append(
377-
"""
378-
\(raw: retMutability) ret = \(raw: callExpr)
379-
"""
380-
)
381-
}
414+
let stmt = renderCallStatement(
415+
callee: "\(raw: _selfParam).\(raw: methodName)", returnType: returnType
416+
)
417+
body.append(CodeBlockItemSyntax(item: .stmt(stmt)))
382418
}
383419

384420
func lowerReturnValue(returnType: BridgeType) {
@@ -440,19 +476,54 @@ class ExportSwift {
440476
}
441477

442478
func render(abiName: String) -> DeclSyntax {
479+
let body: CodeBlockItemListSyntax
480+
if effects.isThrows {
481+
body = """
482+
do {
483+
\(CodeBlockItemListSyntax(self.body))
484+
} catch let error {
485+
if let error = error.thrownValue.object {
486+
withExtendedLifetime(error) {
487+
_swift_js_throw(Int32(bitPattern: $0.id))
488+
}
489+
} else {
490+
let jsError = JSError(message: String(describing: error))
491+
withExtendedLifetime(jsError.jsObject) {
492+
_swift_js_throw(Int32(bitPattern: $0.id))
493+
}
494+
}
495+
\(raw: returnPlaceholderStmt())
496+
}
497+
"""
498+
} else {
499+
body = CodeBlockItemListSyntax(self.body)
500+
}
443501
return """
444502
@_expose(wasm, "\(raw: abiName)")
445503
@_cdecl("\(raw: abiName)")
446504
public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) {
447-
\(CodeBlockItemListSyntax(body))
505+
\(body)
448506
}
449507
"""
450508
}
451509

510+
private func returnPlaceholderStmt() -> String {
511+
switch abiReturnType {
512+
case .i32: return "return 0"
513+
case .i64: return "return 0"
514+
case .f32: return "return 0.0"
515+
case .f64: return "return 0.0"
516+
case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1)"
517+
case .none: return "return"
518+
}
519+
}
520+
452521
func parameterSignature() -> String {
453-
abiParameterSignatures.map { "\($0.name): \($0.type.swiftType)" }.joined(
454-
separator: ", "
455-
)
522+
var nameAndType: [(name: String, abiType: String)] = []
523+
for (name, type) in abiParameterSignatures {
524+
nameAndType.append((name, type.swiftType))
525+
}
526+
return nameAndType.map { "\($0.name): \($0.abiType)" }.joined(separator: ", ")
456527
}
457528

458529
func returnSignature() -> String {
@@ -461,7 +532,7 @@ class ExportSwift {
461532
}
462533

463534
func renderSingleExportedFunction(function: ExportedFunction) -> DeclSyntax {
464-
let builder = ExportedThunkBuilder()
535+
let builder = ExportedThunkBuilder(effects: function.effects)
465536
for param in function.parameters {
466537
builder.liftParameter(param: param)
467538
}
@@ -520,7 +591,7 @@ class ExportSwift {
520591
func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] {
521592
var decls: [DeclSyntax] = []
522593
if let constructor = klass.constructor {
523-
let builder = ExportedThunkBuilder()
594+
let builder = ExportedThunkBuilder(effects: constructor.effects)
524595
for param in constructor.parameters {
525596
builder.liftParameter(param: param)
526597
}
@@ -529,7 +600,7 @@ class ExportSwift {
529600
decls.append(builder.render(abiName: constructor.abiName))
530601
}
531602
for method in klass.methods {
532-
let builder = ExportedThunkBuilder()
603+
let builder = ExportedThunkBuilder(effects: method.effects)
533604
builder.liftParameter(
534605
param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
535606
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@JS func throwsSomething() throws(JSException) {
2+
throw JSException(JSError(message: "TestError").jsValue)
3+
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) {
1212

1313
let tmpRetString;
1414
let tmpRetBytes;
15+
let tmpRetException;
1516
return {
1617
/** @param {WebAssembly.Imports} importObject */
1718
addImports: (importObject) => {
@@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) {
3536
target.set(tmpRetBytes);
3637
tmpRetBytes = undefined;
3738
}
39+
bjs["swift_js_throw"] = function(id) {
40+
tmpRetException = swift.memory.retainByRef(id);
41+
}
3842
bjs["swift_js_retain"] = function(id) {
3943
return swift.memory.retainByRef(id);
4044
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) {
1212

1313
let tmpRetString;
1414
let tmpRetBytes;
15+
let tmpRetException;
1516
return {
1617
/** @param {WebAssembly.Imports} importObject */
1718
addImports: (importObject) => {
@@ -35,6 +36,9 @@ export async function createInstantiator(options, swift) {
3536
target.set(tmpRetBytes);
3637
tmpRetBytes = undefined;
3738
}
39+
bjs["swift_js_throw"] = function(id) {
40+
tmpRetException = swift.memory.retainByRef(id);
41+
}
3842
bjs["swift_js_retain"] = function(id) {
3943
return swift.memory.retainByRef(id);
4044
}

0 commit comments

Comments
 (0)