Skip to content

Commit 5e1fbb4

Browse files
Merge pull request #373 from swiftwasm/yt/add-import-ts-runtime-tests
BridgeJS: Add support for throwing JSException from Swift
2 parents c2233fd + da16654 commit 5e1fbb4

40 files changed

+528
-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: 111 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,43 @@ 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(
178+
node: throwsClause,
179+
message: "Thrown type is not specified, only JSException is supported for now"
180+
)
181+
return nil
182+
}
183+
guard thrownType.trimmedDescription == "JSException" else {
184+
diagnose(
185+
node: throwsClause,
186+
message: "Only JSException is supported for thrown type, got \(thrownType.trimmedDescription)"
187+
)
188+
return nil
189+
}
190+
isThrows = true
191+
}
192+
return Effects(isAsync: isAsync, isThrows: isThrows)
193+
}
194+
166195
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
167196
guard node.attributes.hasJSAttribute() else { return .skipChildren }
168197
guard case .classBody(let name) = state else {
@@ -180,9 +209,14 @@ class ExportSwift {
180209
parameters.append(Parameter(label: label, name: name, type: type))
181210
}
182211

212+
guard let effects = collectEffects(signature: node.signature) else {
213+
return .skipChildren
214+
}
215+
183216
let constructor = ExportedConstructor(
184217
abiName: "bjs_\(name)_init",
185-
parameters: parameters
218+
parameters: parameters,
219+
effects: effects
186220
)
187221
exportedClasses[name]?.constructor = constructor
188222
return .skipChildren
@@ -245,6 +279,8 @@ class ExportSwift {
245279
246280
@_extern(wasm, module: "bjs", name: "swift_js_retain")
247281
private func _swift_js_retain(_ ptr: Int32) -> Int32
282+
@_extern(wasm, module: "bjs", name: "swift_js_throw")
283+
private func _swift_js_throw(_ id: Int32)
248284
"""
249285

250286
func renderSwiftGlue() -> String? {
@@ -268,6 +304,11 @@ class ExportSwift {
268304
var abiParameterForwardings: [LabeledExprSyntax] = []
269305
var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
270306
var abiReturnType: WasmCoreType?
307+
let effects: Effects
308+
309+
init(effects: Effects) {
310+
self.effects = effects
311+
}
271312

272313
func liftParameter(param: Parameter) {
273314
switch param.type {
@@ -350,35 +391,40 @@ class ExportSwift {
350391
}
351392
}
352393

353-
func call(name: String, returnType: BridgeType) {
394+
private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> StmtSyntax {
395+
var callExpr: ExprSyntax =
396+
"\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
397+
if effects.isAsync {
398+
callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr))
399+
}
400+
if effects.isThrows {
401+
callExpr = ExprSyntax(
402+
TryExprSyntax(
403+
tryKeyword: .keyword(.try).with(\.trailingTrivia, .space),
404+
expression: callExpr
405+
)
406+
)
407+
}
354408
let retMutability = returnType == .string ? "var" : "let"
355-
let callExpr: ExprSyntax =
356-
"\(raw: name)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
357409
if returnType == .void {
358-
body.append("\(raw: callExpr)")
410+
return StmtSyntax("\(raw: callExpr)")
359411
} else {
360-
body.append(
361-
"""
362-
\(raw: retMutability) ret = \(raw: callExpr)
363-
"""
364-
)
412+
return StmtSyntax("\(raw: retMutability) ret = \(raw: callExpr)")
365413
}
366414
}
367415

416+
func call(name: String, returnType: BridgeType) {
417+
let stmt = renderCallStatement(callee: "\(raw: name)", returnType: returnType)
418+
body.append(CodeBlockItemSyntax(item: .stmt(stmt)))
419+
}
420+
368421
func callMethod(klassName: String, methodName: String, returnType: BridgeType) {
369422
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-
}
423+
let stmt = renderCallStatement(
424+
callee: "\(raw: _selfParam).\(raw: methodName)",
425+
returnType: returnType
426+
)
427+
body.append(CodeBlockItemSyntax(item: .stmt(stmt)))
382428
}
383429

384430
func lowerReturnValue(returnType: BridgeType) {
@@ -440,19 +486,54 @@ class ExportSwift {
440486
}
441487

442488
func render(abiName: String) -> DeclSyntax {
489+
let body: CodeBlockItemListSyntax
490+
if effects.isThrows {
491+
body = """
492+
do {
493+
\(CodeBlockItemListSyntax(self.body))
494+
} catch let error {
495+
if let error = error.thrownValue.object {
496+
withExtendedLifetime(error) {
497+
_swift_js_throw(Int32(bitPattern: $0.id))
498+
}
499+
} else {
500+
let jsError = JSError(message: String(describing: error))
501+
withExtendedLifetime(jsError.jsObject) {
502+
_swift_js_throw(Int32(bitPattern: $0.id))
503+
}
504+
}
505+
\(raw: returnPlaceholderStmt())
506+
}
507+
"""
508+
} else {
509+
body = CodeBlockItemListSyntax(self.body)
510+
}
443511
return """
444512
@_expose(wasm, "\(raw: abiName)")
445513
@_cdecl("\(raw: abiName)")
446514
public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) {
447-
\(CodeBlockItemListSyntax(body))
515+
\(body)
448516
}
449517
"""
450518
}
451519

520+
private func returnPlaceholderStmt() -> String {
521+
switch abiReturnType {
522+
case .i32: return "return 0"
523+
case .i64: return "return 0"
524+
case .f32: return "return 0.0"
525+
case .f64: return "return 0.0"
526+
case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1)"
527+
case .none: return "return"
528+
}
529+
}
530+
452531
func parameterSignature() -> String {
453-
abiParameterSignatures.map { "\($0.name): \($0.type.swiftType)" }.joined(
454-
separator: ", "
455-
)
532+
var nameAndType: [(name: String, abiType: String)] = []
533+
for (name, type) in abiParameterSignatures {
534+
nameAndType.append((name, type.swiftType))
535+
}
536+
return nameAndType.map { "\($0.name): \($0.abiType)" }.joined(separator: ", ")
456537
}
457538

458539
func returnSignature() -> String {
@@ -461,7 +542,7 @@ class ExportSwift {
461542
}
462543

463544
func renderSingleExportedFunction(function: ExportedFunction) -> DeclSyntax {
464-
let builder = ExportedThunkBuilder()
545+
let builder = ExportedThunkBuilder(effects: function.effects)
465546
for param in function.parameters {
466547
builder.liftParameter(param: param)
467548
}
@@ -520,7 +601,7 @@ class ExportSwift {
520601
func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] {
521602
var decls: [DeclSyntax] = []
522603
if let constructor = klass.constructor {
523-
let builder = ExportedThunkBuilder()
604+
let builder = ExportedThunkBuilder(effects: constructor.effects)
524605
for param in constructor.parameters {
525606
builder.liftParameter(param: param)
526607
}
@@ -529,7 +610,7 @@ class ExportSwift {
529610
decls.append(builder.render(abiName: constructor.abiName))
530611
}
531612
for method in klass.methods {
532-
let builder = ExportedThunkBuilder()
613+
let builder = ExportedThunkBuilder(effects: method.effects)
533614
builder.liftParameter(
534615
param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
535616
)
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
}

0 commit comments

Comments
 (0)