Skip to content

Commit ab19ba9

Browse files
committed
BridgeJS: support imports of Promise JS as async Swift
1 parent c36a742 commit ab19ba9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1793
-43
lines changed

Benchmarks/Sources/Generated/JavaScript/BridgeJS.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2839,6 +2839,11 @@
28392839
{
28402840
"functions" : [
28412841
{
2842+
"effects" : {
2843+
"isAsync" : false,
2844+
"isStatic" : false,
2845+
"isThrows" : true
2846+
},
28422847
"name" : "benchmarkHelperNoop",
28432848
"parameters" : [
28442849

@@ -2850,6 +2855,11 @@
28502855
}
28512856
},
28522857
{
2858+
"effects" : {
2859+
"isAsync" : false,
2860+
"isStatic" : false,
2861+
"isThrows" : true
2862+
},
28532863
"name" : "benchmarkHelperNoopWithNumber",
28542864
"parameters" : [
28552865
{
@@ -2868,6 +2878,11 @@
28682878
}
28692879
},
28702880
{
2881+
"effects" : {
2882+
"isAsync" : false,
2883+
"isStatic" : false,
2884+
"isThrows" : true
2885+
},
28712886
"name" : "benchmarkRunner",
28722887
"parameters" : [
28732888
{

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@
242242
{
243243
"functions" : [
244244
{
245+
"effects" : {
246+
"isAsync" : false,
247+
"isStatic" : false,
248+
"isThrows" : true
249+
},
245250
"name" : "createTS2Swift",
246251
"parameters" : [
247252

@@ -260,6 +265,11 @@
260265
],
261266
"methods" : [
262267
{
268+
"effects" : {
269+
"isAsync" : false,
270+
"isStatic" : false,
271+
"isThrows" : true
272+
},
263273
"name" : "convert",
264274
"parameters" : [
265275
{

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,40 @@ public struct ImportTS {
278278
}
279279
}
280280

281+
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+
)
288+
if originalReturnType == .void {
289+
body.write("_ = try await promise.value")
290+
} else {
291+
body.write("let resolved = try await promise.value")
292+
let liftExpr: String
293+
switch originalReturnType {
294+
case .double:
295+
liftExpr = "Double(resolved.number!)"
296+
case .float:
297+
liftExpr = "Float(resolved.number!)"
298+
case .integer:
299+
liftExpr = "Int(resolved.number!)"
300+
case .string:
301+
liftExpr = "resolved.string!"
302+
case .bool:
303+
liftExpr = "resolved.boolean!"
304+
case .jsObject:
305+
liftExpr = "resolved.object!"
306+
case .jsValue:
307+
liftExpr = "resolved"
308+
default:
309+
liftExpr = "resolved.object!"
310+
}
311+
body.write("return \(liftExpr)")
312+
}
313+
}
314+
281315
func assignThis(returnType: BridgeType) {
282316
guard case .jsObject = returnType else {
283317
preconditionFailure("assignThis can only be called with a jsObject return type")
@@ -299,9 +333,13 @@ public struct ImportTS {
299333
return "\(raw: printer.lines.joined(separator: "\n"))"
300334
}
301335

302-
func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax {
336+
func renderThunkDecl(
337+
name: String,
338+
parameters: [Parameter],
339+
returnType: BridgeType,
340+
effects: Effects = Effects(isAsync: false, isThrows: true)
341+
) -> DeclSyntax {
303342
let printer = CodeFragmentPrinter()
304-
let effects = Effects(isAsync: false, isThrows: true)
305343
let signature = SwiftSignatureBuilder.buildFunctionSignature(
306344
parameters: parameters,
307345
returnType: returnType,
@@ -359,22 +397,30 @@ public struct ImportTS {
359397
_ function: ImportedFunctionSkeleton,
360398
topLevelDecls: inout [DeclSyntax]
361399
) throws -> [DeclSyntax] {
400+
// For async functions, the ABI return type is always jsObject (the Promise).
401+
// We tell CallJSEmission that the return type is jsObject so it captures the return value.
402+
let abiReturnType: BridgeType = function.effects.isAsync ? .jsObject(nil) : function.returnType
362403
let builder = try CallJSEmission(
363404
moduleName: moduleName,
364405
abiName: function.abiName(context: nil),
365-
returnType: function.returnType
406+
returnType: abiReturnType
366407
)
367408
for param in function.parameters {
368409
try builder.lowerParameter(param: param)
369410
}
370411
try builder.call()
371-
try builder.liftReturnValue()
412+
if function.effects.isAsync {
413+
builder.liftAsyncReturnValue(originalReturnType: function.returnType)
414+
} else {
415+
try builder.liftReturnValue()
416+
}
372417
topLevelDecls.append(builder.renderImportDecl())
373418
return [
374419
builder.renderThunkDecl(
375420
name: Self.thunkName(function: function),
376421
parameters: function.parameters,
377-
returnType: function.returnType
422+
returnType: function.returnType,
423+
effects: function.effects
378424
)
379425
.with(\.leadingTrivia, Self.renderDocumentation(documentation: function.documentation))
380426
]
@@ -385,41 +431,53 @@ public struct ImportTS {
385431
var decls: [DeclSyntax] = []
386432

387433
func renderMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
434+
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
388435
let builder = try CallJSEmission(
389436
moduleName: moduleName,
390437
abiName: method.abiName(context: type),
391-
returnType: method.returnType
438+
returnType: abiReturnType
392439
)
393440
try builder.lowerParameter(param: selfParameter)
394441
for param in method.parameters {
395442
try builder.lowerParameter(param: param)
396443
}
397444
try builder.call()
398-
try builder.liftReturnValue()
445+
if method.effects.isAsync {
446+
builder.liftAsyncReturnValue(originalReturnType: method.returnType)
447+
} else {
448+
try builder.liftReturnValue()
449+
}
399450
topLevelDecls.append(builder.renderImportDecl())
400451
return [
401452
builder.renderThunkDecl(
402453
name: Self.thunkName(type: type, method: method),
403454
parameters: [selfParameter] + method.parameters,
404-
returnType: method.returnType
455+
returnType: method.returnType,
456+
effects: method.effects
405457
)
406458
]
407459
}
408460

409461
func renderStaticMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
410462
let abiName = method.abiName(context: type, operation: "static")
411-
let builder = try CallJSEmission(moduleName: moduleName, abiName: abiName, returnType: method.returnType)
463+
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
464+
let builder = try CallJSEmission(moduleName: moduleName, abiName: abiName, returnType: abiReturnType)
412465
for param in method.parameters {
413466
try builder.lowerParameter(param: param)
414467
}
415468
try builder.call()
416-
try builder.liftReturnValue()
469+
if method.effects.isAsync {
470+
builder.liftAsyncReturnValue(originalReturnType: method.returnType)
471+
} else {
472+
try builder.liftReturnValue()
473+
}
417474
topLevelDecls.append(builder.renderImportDecl())
418475
return [
419476
builder.renderThunkDecl(
420477
name: Self.thunkName(type: type, method: method),
421478
parameters: method.parameters,
422-
returnType: method.returnType
479+
returnType: method.returnType,
480+
effects: method.effects
423481
)
424482
]
425483
}

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,7 +2067,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
20672067
let valueType: BridgeType
20682068
}
20692069

2070-
/// Validates effects (throws required, async not supported)
2070+
/// Validates effects (throws required, async only supported for @JSFunction)
20712071
private func validateEffects(
20722072
_ effects: FunctionEffectSpecifiersSyntax?,
20732073
node: some SyntaxProtocol,
@@ -2083,7 +2083,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
20832083
)
20842084
return nil
20852085
}
2086-
if effects.isAsync {
2086+
if effects.isAsync && attributeName != "JSFunction" {
20872087
errors.append(
20882088
DiagnosticError(
20892089
node: node,
@@ -2420,7 +2420,12 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24202420
_ jsFunction: AttributeSyntax,
24212421
_ node: FunctionDeclSyntax,
24222422
) -> ImportedFunctionSkeleton? {
2423-
guard validateEffects(node.signature.effectSpecifiers, node: node, attributeName: "JSFunction") != nil
2423+
guard
2424+
let effects = validateEffects(
2425+
node.signature.effectSpecifiers,
2426+
node: node,
2427+
attributeName: "JSFunction"
2428+
)
24242429
else {
24252430
return nil
24262431
}
@@ -2446,6 +2451,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24462451
from: from,
24472452
parameters: parameters,
24482453
returnType: returnType,
2454+
effects: effects,
24492455
documentation: nil
24502456
)
24512457
}

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,7 +1233,7 @@ public struct BridgeJSLink {
12331233
for method in type.methods {
12341234
let methodName = method.jsName ?? method.name
12351235
let methodSignature =
1236-
"\(renderTSPropertyName(methodName))\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));"
1236+
"\(renderTSPropertyName(methodName))\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
12371237
printer.write(methodSignature)
12381238
}
12391239

@@ -3124,21 +3124,23 @@ extension BridgeJSLink {
31243124
}
31253125
let jsName = function.jsName ?? function.name
31263126
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
31273130
let returnExpr = try thunkBuilder.call(
31283131
name: jsName,
31293132
fromObjectExpr: importRootExpr,
3130-
returnType: function.returnType
3133+
returnType: abiReturnType
31313134
)
31323135
let funcLines = thunkBuilder.renderFunction(
31333136
name: function.abiName(context: nil),
31343137
returnExpr: returnExpr,
3135-
returnType: function.returnType
3138+
returnType: abiReturnType
31363139
)
3137-
let effects = Effects(isAsync: false, isThrows: false)
31383140
if function.from == nil {
31393141
importObjectBuilder.appendDts(
31403142
[
3141-
"\(renderTSPropertyName(jsName))\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: effects));"
3143+
"\(renderTSPropertyName(jsName))\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
31423144
]
31433145
)
31443146
}
@@ -3337,11 +3339,12 @@ extension BridgeJSLink {
33373339
for param in method.parameters {
33383340
try thunkBuilder.liftParameter(param: param)
33393341
}
3340-
let returnExpr = try thunkBuilder.callMethod(name: method.jsName ?? method.name, returnType: method.returnType)
3342+
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
3343+
let returnExpr = try thunkBuilder.callMethod(name: method.jsName ?? method.name, returnType: abiReturnType)
33413344
let funcLines = thunkBuilder.renderFunction(
33423345
name: method.abiName(context: context),
33433346
returnExpr: returnExpr,
3344-
returnType: method.returnType
3347+
returnType: abiReturnType
33453348
)
33463349
return (funcLines, [])
33473350
}

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ public struct ImportedFunctionSkeleton: Codable {
923923
public let from: JSImportFrom?
924924
public let parameters: [Parameter]
925925
public let returnType: BridgeType
926+
public let effects: Effects
926927
public let documentation: String?
927928

928929
public init(
@@ -931,13 +932,15 @@ public struct ImportedFunctionSkeleton: Codable {
931932
from: JSImportFrom? = nil,
932933
parameters: [Parameter],
933934
returnType: BridgeType,
935+
effects: Effects = Effects(isAsync: false, isThrows: true),
934936
documentation: String? = nil
935937
) {
936938
self.name = name
937939
self.jsName = jsName
938940
self.from = from
939941
self.parameters = parameters
940942
self.returnType = returnType
943+
self.effects = effects
941944
self.documentation = documentation
942945
}
943946

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,8 @@ export class TypeProcessor {
313313
const parameters = signature.getParameters();
314314
const parameterNameMap = this.buildParameterNameMap(parameters);
315315
const params = this.renderParameters(parameters, decl);
316-
const returnType = this.visitType(signature.getReturnType(), decl);
317-
const effects = this.renderEffects({ isAsync: false });
316+
const { returnType, isAsync } = this.unwrapPromiseReturnType(signature.getReturnType(), decl);
317+
const effects = this.renderEffects({ isAsync });
318318
const annotation = this.renderMacroAnnotation("JSFunction", args);
319319

320320
this.emitDocComment(decl, { indent: "", parameterNameMap });
@@ -581,8 +581,8 @@ export class TypeProcessor {
581581
const parameters = signature.getParameters();
582582
const parameterNameMap = this.buildParameterNameMap(parameters);
583583
const params = this.renderParameters(parameters, node);
584-
const returnType = this.visitType(signature.getReturnType(), node);
585-
const effects = this.renderEffects({ isAsync: false });
584+
const { returnType, isAsync } = this.unwrapPromiseReturnType(signature.getReturnType(), node);
585+
const effects = this.renderEffects({ isAsync });
586586
const swiftFuncName = this.renderIdentifier(swiftName);
587587

588588
this.emitDocComment(node, { parameterNameMap });
@@ -1210,8 +1210,8 @@ export class TypeProcessor {
12101210
const parameters = signature.getParameters();
12111211
const parameterNameMap = this.buildParameterNameMap(parameters);
12121212
const params = this.renderParameters(parameters, node);
1213-
const returnType = this.visitType(signature.getReturnType(), node);
1214-
const effects = this.renderEffects({ isAsync: false });
1213+
const { returnType, isAsync } = this.unwrapPromiseReturnType(signature.getReturnType(), node);
1214+
const effects = this.renderEffects({ isAsync });
12151215
const swiftMethodName = this.renderIdentifier(swiftName);
12161216
const isStatic = node.modifiers?.some(
12171217
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
@@ -1281,6 +1281,27 @@ export class TypeProcessor {
12811281
return parts.join(" ");
12821282
}
12831283

1284+
/**
1285+
* Check if a type is Promise<T> and extract the return type and async flag.
1286+
* @param {ts.Type} type - The return type to check
1287+
* @param {ts.Node} node - The node for type visiting context
1288+
* @returns {{ returnType: string, isAsync: boolean }}
1289+
* @private
1290+
*/
1291+
unwrapPromiseReturnType(type, node) {
1292+
if (isTypeReference(type)) {
1293+
const symbol = type.target?.getSymbol();
1294+
if (symbol?.name === "Promise") {
1295+
const typeArgs = this.checker.getTypeArguments(/** @type {ts.TypeReference} */ (type));
1296+
const innerType = typeArgs && typeArgs.length > 0
1297+
? this.visitType(typeArgs[0], node)
1298+
: "Void";
1299+
return { returnType: innerType, isAsync: true };
1300+
}
1301+
}
1302+
return { returnType: this.visitType(type, node), isAsync: false };
1303+
}
1304+
12841305
/**
12851306
* @param {ts.Node} node
12861307
* @returns {boolean}

0 commit comments

Comments
 (0)