Skip to content

Commit 2b8bcaf

Browse files
committed
JSTypedClosure-based approach
1 parent d0b207c commit 2b8bcaf

File tree

6 files changed

+512
-193
lines changed

6 files changed

+512
-193
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,16 @@ public struct ImportTS {
212212
}
213213
}
214214

215-
func call() throws {
215+
/// Prepends a `continuationPtr: Int32` parameter to the ABI parameter list.
216+
///
217+
/// Used for async imports where the JS side needs the continuation pointer
218+
/// to resolve/reject the Promise.
219+
func prependContinuationPtr() {
220+
abiParameterSignatures.insert(("continuationPtr", .i32), at: 0)
221+
abiParameterForwardings.insert("continuationPtr", at: 0)
222+
}
223+
224+
func call(skipExceptionCheck: Bool = false) throws {
216225
for stmt in stackLoweringStmts {
217226
body.write(stmt.description)
218227
}
@@ -243,8 +252,9 @@ public struct ImportTS {
243252
}
244253
}
245254

246-
// Add exception check for ImportTS context
247-
if context == .importTS {
255+
// Add exception check for ImportTS context (skipped for async, where
256+
// errors are funneled through the JS-side reject path)
257+
if !skipExceptionCheck && context == .importTS {
248258
body.write("if let error = _swift_js_take_exception() { throw error }")
249259
}
250260
}
@@ -279,16 +289,27 @@ public struct ImportTS {
279289
}
280290

281291
func liftAsyncReturnValue(originalReturnType: BridgeType) {
282-
// For async imports, the extern function returns a Promise object ID (i32).
283-
// We wrap it in JSPromise, await the resolved value, then lift to the target type.
284-
abiReturnType = .i32
285-
body.write(
286-
"let promise = JSPromise(unsafelyWrapping: JSObject(id: UInt32(bitPattern: ret)))"
287-
)
292+
// For async imports, we use the continuation-pointer pattern.
293+
// The extern function takes a leading `continuationPtr: Int32` and returns void.
294+
// The JS side attaches .then/.catch handlers and calls back into Wasm
295+
// via bjs_resolve_promise_continuation / bjs_reject_promise_continuation.
296+
abiReturnType = nil
297+
298+
// Wrap the existing body (parameter lowering + extern call) in _bjs_awaitPromise
299+
let innerBody = body
300+
body = CodeFragmentPrinter()
301+
288302
if originalReturnType == .void {
289-
body.write("_ = try await promise.value")
303+
body.write("_ = try await _bjs_awaitPromise { continuationPtr in")
290304
} else {
291-
body.write("let resolved = try await promise.value")
305+
body.write("let resolved = try await _bjs_awaitPromise { continuationPtr in")
306+
}
307+
body.indent {
308+
body.write(lines: innerBody.lines)
309+
}
310+
body.write("}")
311+
312+
if originalReturnType != .void {
292313
let liftExpr: String
293314
switch originalReturnType {
294315
case .double:
@@ -401,18 +422,21 @@ public struct ImportTS {
401422
_ function: ImportedFunctionSkeleton,
402423
topLevelDecls: inout [DeclSyntax]
403424
) throws -> [DeclSyntax] {
404-
// For async functions, the ABI return type is always jsObject (the Promise).
405-
// We tell CallJSEmission that the return type is jsObject so it captures the return value.
406-
let abiReturnType: BridgeType = function.effects.isAsync ? .jsObject(nil) : function.returnType
425+
// For async functions, the extern returns void (the JS side resolves/rejects
426+
// via continuation callbacks). For sync functions, use the actual return type.
427+
let abiReturnType: BridgeType = function.effects.isAsync ? .void : function.returnType
407428
let builder = try CallJSEmission(
408429
moduleName: moduleName,
409430
abiName: function.abiName(context: nil),
410431
returnType: abiReturnType
411432
)
433+
if function.effects.isAsync {
434+
builder.prependContinuationPtr()
435+
}
412436
for param in function.parameters {
413437
try builder.lowerParameter(param: param)
414438
}
415-
try builder.call()
439+
try builder.call(skipExceptionCheck: function.effects.isAsync)
416440
if function.effects.isAsync {
417441
builder.liftAsyncReturnValue(originalReturnType: function.returnType)
418442
} else {
@@ -435,17 +459,20 @@ public struct ImportTS {
435459
var decls: [DeclSyntax] = []
436460

437461
func renderMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
438-
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
462+
let abiReturnType: BridgeType = method.effects.isAsync ? .void : method.returnType
439463
let builder = try CallJSEmission(
440464
moduleName: moduleName,
441465
abiName: method.abiName(context: type),
442466
returnType: abiReturnType
443467
)
468+
if method.effects.isAsync {
469+
builder.prependContinuationPtr()
470+
}
444471
try builder.lowerParameter(param: selfParameter)
445472
for param in method.parameters {
446473
try builder.lowerParameter(param: param)
447474
}
448-
try builder.call()
475+
try builder.call(skipExceptionCheck: method.effects.isAsync)
449476
if method.effects.isAsync {
450477
builder.liftAsyncReturnValue(originalReturnType: method.returnType)
451478
} else {

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 180 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public struct BridgeJSLink {
135135
var importObjectBuilders: [ImportObjectBuilder] = []
136136
var enumStaticAssignments: [String] = []
137137
var needsImportsObject: Bool = false
138+
var hasAsyncImports: Bool = false
138139
}
139140

140141
private func collectLinkData() throws -> LinkData {
@@ -237,12 +238,19 @@ public struct BridgeJSLink {
237238
if function.from == nil {
238239
data.needsImportsObject = true
239240
}
241+
if function.effects.isAsync {
242+
data.hasAsyncImports = true
243+
}
240244
try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function)
241245
}
242246
for type in fileSkeleton.types {
243247
if type.constructor != nil, type.from == nil {
244248
data.needsImportsObject = true
245249
}
250+
// Check for async methods in imported types
251+
for method in type.methods where method.effects.isAsync {
252+
data.hasAsyncImports = true
253+
}
246254
try renderImportedType(importObjectBuilder: importObjectBuilder, type: type)
247255
}
248256
}
@@ -314,6 +322,85 @@ public struct BridgeJSLink {
314322
]
315323
}
316324

325+
/// Generates helper functions for the continuation-pointer pattern used by async imports.
326+
///
327+
/// These encode a JS value as `(kind, payload1, payload2)` matching the `RawJSValue`
328+
/// encoding from `_CJavaScriptKit.h`, then call the appropriate Wasm export to resume
329+
/// the Swift continuation.
330+
private func generatePromiseContinuationHelpers() -> [String] {
331+
let printer = CodeFragmentPrinter()
332+
// Helper to encode a JS value into (kind, payload1, payload2) and call the resolve export
333+
printer.write("function bjs_resolvePromiseContinuation(ptr, value) {")
334+
printer.indent {
335+
printer.write(lines: generateJSValueEncoding(variableName: "value"))
336+
printer.write(
337+
"\(JSGlueVariableScope.reservedInstance).exports.bjs_resolve_promise_continuation(ptr, kind, payload1, payload2);"
338+
)
339+
}
340+
printer.write("}")
341+
// Helper to encode a JS value into (kind, payload1, payload2) and call the reject export
342+
printer.write("function bjs_rejectPromiseContinuation(ptr, error) {")
343+
printer.indent {
344+
printer.write(lines: generateJSValueEncoding(variableName: "error"))
345+
printer.write(
346+
"\(JSGlueVariableScope.reservedInstance).exports.bjs_reject_promise_continuation(ptr, kind, payload1, payload2);"
347+
)
348+
}
349+
printer.write("}")
350+
return printer.lines
351+
}
352+
353+
/// Generates JS code that encodes a variable into `(kind, payload1, payload2)`.
354+
///
355+
/// The encoding matches `JavaScriptValueKind` from `_CJavaScriptKit.h`:
356+
/// - Boolean(0): payload1 = 1 or 0
357+
/// - String(1): payload1 = retained object ID
358+
/// - Number(2): payload2 = the number
359+
/// - Object(3): payload1 = retained object ID
360+
/// - Null(4): no payload
361+
/// - Undefined(5): no payload
362+
/// - Symbol(7): payload1 = retained object ID
363+
/// - BigInt(8): payload1 = retained object ID
364+
private func generateJSValueEncoding(variableName: String) -> [String] {
365+
let s = JSGlueVariableScope.reservedSwift
366+
return [
367+
"let kind, payload1 = 0, payload2 = 0;",
368+
"if (\(variableName) === null) {",
369+
" kind = 4;",
370+
"} else if (\(variableName) === undefined) {",
371+
" kind = 5;",
372+
"} else {",
373+
" const type = typeof \(variableName);",
374+
" switch (type) {",
375+
" case \"boolean\":",
376+
" kind = 0;",
377+
" payload1 = \(variableName) ? 1 : 0;",
378+
" break;",
379+
" case \"number\":",
380+
" kind = 2;",
381+
" payload2 = \(variableName);",
382+
" break;",
383+
" case \"string\":",
384+
" kind = 1;",
385+
" payload1 = \(s).memory.retain(\(variableName));",
386+
" break;",
387+
" case \"symbol\":",
388+
" kind = 7;",
389+
" payload1 = \(s).memory.retain(\(variableName));",
390+
" break;",
391+
" case \"bigint\":",
392+
" kind = 8;",
393+
" payload1 = \(s).memory.retain(\(variableName));",
394+
" break;",
395+
" default:",
396+
" kind = 3;",
397+
" payload1 = \(s).memory.retain(\(variableName));",
398+
" break;",
399+
" }",
400+
"}",
401+
]
402+
}
403+
317404
private func generateAddImports(needsImportsObject: Bool) throws -> CodeFragmentPrinter {
318405
let printer = CodeFragmentPrinter()
319406
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
@@ -970,6 +1057,11 @@ public struct BridgeJSLink {
9701057
try printer.indent {
9711058
printer.write(lines: generateVariableDeclarations())
9721059

1060+
// Generate Promise continuation helpers when async imports exist
1061+
if data.hasAsyncImports {
1062+
printer.write(lines: generatePromiseContinuationHelpers())
1063+
}
1064+
9731065
let bodyPrinter = CodeFragmentPrinter()
9741066
let allStructs = exportedSkeletons.flatMap { $0.structs }
9751067
for structDef in allStructs {
@@ -2232,6 +2324,46 @@ extension BridgeJSLink {
22322324
return printer.lines
22332325
}
22342326

2327+
/// Generates the call expression for an async import.
2328+
///
2329+
/// Instead of lowering the return value, this assigns the result to `promise`
2330+
/// and attaches `.then`/`.catch` handlers that call the resolve/reject continuations.
2331+
func callAsync(name: String, fromObjectExpr: String) {
2332+
let calleeExpr = Self.propertyAccessExpr(objectExpr: fromObjectExpr, propertyName: name)
2333+
let callExpr = "\(calleeExpr)(\(parameterForwardings.joined(separator: ", ")))"
2334+
body.write("const promise = \(callExpr);")
2335+
body.write("promise.then(")
2336+
body.indent {
2337+
body.write("(value) => { bjs_resolvePromiseContinuation(continuationPtr, value); },")
2338+
body.write("(error) => { bjs_rejectPromiseContinuation(continuationPtr, error); }")
2339+
}
2340+
body.write(");")
2341+
}
2342+
2343+
/// Renders an async import function with continuation-pointer pattern.
2344+
///
2345+
/// The generated function takes `continuationPtr` as the first parameter,
2346+
/// wraps the import call in try/catch, attaches Promise handlers, and
2347+
/// calls reject continuation on synchronous errors.
2348+
func renderAsyncFunction(name: String?) -> [String] {
2349+
let printer = CodeFragmentPrinter()
2350+
let allParams = ["continuationPtr"] + parameterNames
2351+
printer.write("function\(name.map { " \($0)" } ?? "")(\(allParams.joined(separator: ", "))) {")
2352+
printer.indent {
2353+
printer.write("try {")
2354+
printer.indent {
2355+
printer.write(contentsOf: body)
2356+
}
2357+
printer.write("} catch (error) {")
2358+
printer.indent {
2359+
printer.write("bjs_rejectPromiseContinuation(continuationPtr, error);")
2360+
}
2361+
printer.write("}")
2362+
}
2363+
printer.write("}")
2364+
return printer.lines
2365+
}
2366+
22352367
func call(name: String, fromObjectExpr: String, returnType: BridgeType) throws -> String? {
22362368
let calleeExpr = Self.propertyAccessExpr(objectExpr: fromObjectExpr, propertyName: name)
22372369
return try self.call(calleeExpr: calleeExpr, returnType: returnType)
@@ -2285,6 +2417,20 @@ extension BridgeJSLink {
22852417
)
22862418
}
22872419

2420+
/// Generates an async method call with continuation-pointer pattern.
2421+
func callAsyncMethod(name: String) {
2422+
let objectExpr = "\(JSGlueVariableScope.reservedSwift).memory.getObject(self)"
2423+
let calleeExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name)
2424+
let callExpr = "\(calleeExpr)(\(parameterForwardings.joined(separator: ", ")))"
2425+
body.write("const promise = \(callExpr);")
2426+
body.write("promise.then(")
2427+
body.indent {
2428+
body.write("(value) => { bjs_resolvePromiseContinuation(continuationPtr, value); },")
2429+
body.write("(error) => { bjs_rejectPromiseContinuation(continuationPtr, error); }")
2430+
}
2431+
body.write(");")
2432+
}
2433+
22882434
func callStaticMethod(on objectExpr: String, name: String, returnType: BridgeType) throws -> String? {
22892435
let calleeExpr = Self.propertyAccessExpr(objectExpr: objectExpr, propertyName: name)
22902436
return try call(
@@ -3124,19 +3270,27 @@ extension BridgeJSLink {
31243270
}
31253271
let jsName = function.jsName ?? function.name
31263272
let importRootExpr = function.from == .global ? "globalThis" : "imports"
3127-
// For async functions, the JS handler returns the Promise as a jsObject.
3128-
// The Swift side handles awaiting and lifting the resolved value.
3129-
let abiReturnType: BridgeType = function.effects.isAsync ? .jsObject(nil) : function.returnType
3130-
let returnExpr = try thunkBuilder.call(
3131-
name: jsName,
3132-
fromObjectExpr: importRootExpr,
3133-
returnType: abiReturnType
3134-
)
3135-
let funcLines = thunkBuilder.renderFunction(
3136-
name: function.abiName(context: nil),
3137-
returnExpr: returnExpr,
3138-
returnType: abiReturnType
3139-
)
3273+
3274+
let funcLines: [String]
3275+
if function.effects.isAsync {
3276+
// For async functions, use the continuation-pointer pattern.
3277+
// The generated function takes continuationPtr as first param,
3278+
// calls the import, attaches .then/.catch on the returned Promise,
3279+
// and calls resolve/reject continuations.
3280+
thunkBuilder.callAsync(name: jsName, fromObjectExpr: importRootExpr)
3281+
funcLines = thunkBuilder.renderAsyncFunction(name: function.abiName(context: nil))
3282+
} else {
3283+
let returnExpr = try thunkBuilder.call(
3284+
name: jsName,
3285+
fromObjectExpr: importRootExpr,
3286+
returnType: function.returnType
3287+
)
3288+
funcLines = thunkBuilder.renderFunction(
3289+
name: function.abiName(context: nil),
3290+
returnExpr: returnExpr,
3291+
returnType: function.returnType
3292+
)
3293+
}
31403294
if function.from == nil {
31413295
importObjectBuilder.appendDts(
31423296
[
@@ -3339,13 +3493,19 @@ extension BridgeJSLink {
33393493
for param in method.parameters {
33403494
try thunkBuilder.liftParameter(param: param)
33413495
}
3342-
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
3343-
let returnExpr = try thunkBuilder.callMethod(name: method.jsName ?? method.name, returnType: abiReturnType)
3344-
let funcLines = thunkBuilder.renderFunction(
3345-
name: method.abiName(context: context),
3346-
returnExpr: returnExpr,
3347-
returnType: abiReturnType
3348-
)
3496+
3497+
let funcLines: [String]
3498+
if method.effects.isAsync {
3499+
thunkBuilder.callAsyncMethod(name: method.jsName ?? method.name)
3500+
funcLines = thunkBuilder.renderAsyncFunction(name: method.abiName(context: context))
3501+
} else {
3502+
let returnExpr = try thunkBuilder.callMethod(name: method.jsName ?? method.name, returnType: method.returnType)
3503+
funcLines = thunkBuilder.renderFunction(
3504+
name: method.abiName(context: context),
3505+
returnExpr: returnExpr,
3506+
returnType: method.returnType
3507+
)
3508+
}
33493509
return (funcLines, [])
33503510
}
33513511

0 commit comments

Comments
 (0)