Skip to content

Commit bf5f4d9

Browse files
BridgeJS: Add async function support with Promise-based interop
Implements comprehensive async Swift-to-JavaScript interoperability using JSPromise.async wrapper. Includes TypeScript Promise<T> type generation and full integration with module name architecture.
1 parent 48720bb commit bf5f4d9

Some content is hidden

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

43 files changed

+1407
-136
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ let package = Package(
155155
),
156156
.testTarget(
157157
name: "BridgeJSRuntimeTests",
158-
dependencies: ["JavaScriptKit"],
158+
dependencies: ["JavaScriptKit", "JavaScriptEventLoop"],
159159
exclude: [
160160
"bridge-js.config.json",
161161
"bridge-js.d.ts",

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,9 @@ public class ExportSwift {
453453
var callExpr: ExprSyntax =
454454
"\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
455455
if effects.isAsync {
456-
callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr))
456+
callExpr = ExprSyntax(
457+
AwaitExprSyntax(awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), expression: callExpr)
458+
)
457459
}
458460
if effects.isThrows {
459461
callExpr = ExprSyntax(
@@ -463,6 +465,11 @@ public class ExportSwift {
463465
)
464466
)
465467
}
468+
469+
if effects.isAsync, returnType != .void {
470+
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr).jsValue")))
471+
}
472+
466473
let retMutability = returnType == .string ? "var" : "let"
467474
if returnType == .void {
468475
return CodeBlockItemSyntax(item: .init(ExpressionStmtSyntax(expression: callExpr)))
@@ -486,7 +493,40 @@ public class ExportSwift {
486493
}
487494

488495
func lowerReturnValue(returnType: BridgeType) {
489-
abiReturnType = returnType.abiReturnType
496+
if effects.isAsync {
497+
// Async functions always return a Promise, which is a JSObject
498+
_lowerReturnValue(returnType: .jsObject(nil))
499+
} else {
500+
_lowerReturnValue(returnType: returnType)
501+
}
502+
}
503+
504+
private func _lowerReturnValue(returnType: BridgeType) {
505+
switch returnType {
506+
case .void:
507+
abiReturnType = nil
508+
case .bool:
509+
abiReturnType = .i32
510+
case .int:
511+
abiReturnType = .i32
512+
case .float:
513+
abiReturnType = .f32
514+
case .double:
515+
abiReturnType = .f64
516+
case .string:
517+
abiReturnType = nil
518+
case .jsObject:
519+
abiReturnType = .i32
520+
case .swiftHeapObject:
521+
// UnsafeMutableRawPointer is returned as an i32 pointer
522+
abiReturnType = .pointer
523+
}
524+
525+
if effects.isAsync {
526+
// The return value of async function (T of `(...) async -> T`) is
527+
// handled by the JSPromise.async, so we don't need to do anything here.
528+
return
529+
}
490530

491531
switch returnType {
492532
case .void: break
@@ -527,7 +567,14 @@ public class ExportSwift {
527567

528568
func render(abiName: String) -> DeclSyntax {
529569
let body: CodeBlockItemListSyntax
530-
if effects.isThrows {
570+
if effects.isAsync {
571+
body = """
572+
let ret = JSPromise.async {
573+
\(CodeBlockItemListSyntax(self.body))
574+
}.jsObject
575+
return _swift_js_retain(Int32(bitPattern: ret.id))
576+
"""
577+
} else if effects.isThrows {
531578
body = """
532579
do {
533580
\(CodeBlockItemListSyntax(self.body))

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,13 @@ struct BridgeJSLink {
172172
let tmpRetBytes;
173173
let tmpRetException;
174174
return {
175-
/** @param {WebAssembly.Imports} importObject */
176-
addImports: (importObject) => {
175+
/**
176+
* @param {WebAssembly.Imports} importObject
177+
*/
178+
addImports: (importObject, importsContext) => {
177179
const bjs = {};
178180
importObject["bjs"] = bjs;
181+
const imports = options.getImports(importsContext);
179182
bjs["swift_js_return_string"] = function(ptr, len) {
180183
const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : "");
181184
tmpRetString = textDecoder.decode(bytes);
@@ -294,7 +297,7 @@ struct BridgeJSLink {
294297
// Add methods
295298
for method in type.methods {
296299
let methodSignature =
297-
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
300+
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));"
298301
typeDefinitions.append(methodSignature.indent(count: 4))
299302
}
300303

@@ -368,7 +371,7 @@ struct BridgeJSLink {
368371

369372
for method in klass.methods {
370373
let methodSignature =
371-
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
374+
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
372375
dtsLines.append("\(methodSignature)".indent(count: identBaseSize * (parts.count + 2)))
373376
}
374377

@@ -394,7 +397,7 @@ struct BridgeJSLink {
394397

395398
for function in functions {
396399
let signature =
397-
"function \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
400+
"function \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
398401
dtsLines.append("\(signature)".indent(count: identBaseSize * (parts.count + 1)))
399402
}
400403

@@ -446,6 +449,14 @@ struct BridgeJSLink {
446449
}
447450

448451
func call(abiName: String, returnType: BridgeType) -> String? {
452+
if effects.isAsync {
453+
return _call(abiName: abiName, returnType: .jsObject(nil))
454+
} else {
455+
return _call(abiName: abiName, returnType: returnType)
456+
}
457+
}
458+
459+
private func _call(abiName: String, returnType: BridgeType) -> String? {
449460
let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
450461
var returnExpr: String?
451462

@@ -519,8 +530,15 @@ struct BridgeJSLink {
519530
}
520531
}
521532

522-
private func renderTSSignature(parameters: [Parameter], returnType: BridgeType) -> String {
523-
return "(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnType.tsType)"
533+
private func renderTSSignature(parameters: [Parameter], returnType: BridgeType, effects: Effects) -> String {
534+
let returnTypeWithEffect: String
535+
if effects.isAsync {
536+
returnTypeWithEffect = "Promise<\(returnType.tsType)>"
537+
} else {
538+
returnTypeWithEffect = returnType.tsType
539+
}
540+
return
541+
"(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnTypeWithEffect)"
524542
}
525543

526544
func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) {
@@ -538,7 +556,7 @@ struct BridgeJSLink {
538556
)
539557
var dtsLines: [String] = []
540558
dtsLines.append(
541-
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
559+
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
542560
)
543561

544562
return (funcLines, dtsLines)
@@ -581,7 +599,7 @@ struct BridgeJSLink {
581599
jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })
582600

583601
dtsExportEntryLines.append(
584-
"constructor\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
602+
"constructor\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));"
585603
.indent(count: 4)
586604
)
587605
}
@@ -603,7 +621,7 @@ struct BridgeJSLink {
603621
).map { $0.indent(count: 4) }
604622
)
605623
dtsTypeLines.append(
606-
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
624+
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
607625
.indent(count: 4)
608626
)
609627
}
@@ -712,7 +730,7 @@ struct BridgeJSLink {
712730
}
713731

714732
func call(name: String, returnType: BridgeType) {
715-
let call = "options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
733+
let call = "imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
716734
if returnType == .void {
717735
bodyLines.append("\(call);")
718736
} else {
@@ -721,7 +739,7 @@ struct BridgeJSLink {
721739
}
722740

723741
func callConstructor(name: String) {
724-
let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
742+
let call = "new imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
725743
bodyLines.append("let ret = \(call);")
726744
}
727745

@@ -801,9 +819,10 @@ struct BridgeJSLink {
801819
returnExpr: returnExpr,
802820
returnType: function.returnType
803821
)
822+
let effects = Effects(isAsync: false, isThrows: false)
804823
importObjectBuilder.appendDts(
805824
[
806-
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
825+
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: effects));"
807826
]
808827
)
809828
importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines)
@@ -878,7 +897,8 @@ struct BridgeJSLink {
878897
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
879898
importObjectBuilder.appendDts([
880899
"\(type.name): {",
881-
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4),
900+
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType, effects: Effects(isAsync: false, isThrows: false)));"
901+
.indent(count: 4),
882902
"}",
883903
])
884904
}

Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ export type Parameter = {
1212
type: BridgeType;
1313
}
1414

15+
export type Effects = {
16+
isAsync: boolean;
17+
}
18+
1519
export type ImportFunctionSkeleton = {
1620
name: string;
1721
parameters: Parameter[];
1822
returnType: BridgeType;
23+
effects: Effects;
1924
documentation: string | undefined;
2025
}
2126

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class TypeProcessor {
162162
parameters,
163163
returnType: bridgeReturnType,
164164
documentation,
165+
effects: { isAsync: false },
165166
};
166167
}
167168

@@ -341,6 +342,10 @@ export class TypeProcessor {
341342
* @private
342343
*/
343344
visitType(type, node) {
345+
// Treat A<B> and A<C> as the same type
346+
if (isTypeReference(type)) {
347+
type = type.target;
348+
}
344349
const maybeProcessed = this.processedTypes.get(type);
345350
if (maybeProcessed) {
346351
return maybeProcessed;
@@ -364,8 +369,13 @@ export class TypeProcessor {
364369
"object": { "jsObject": {} },
365370
"symbol": { "jsObject": {} },
366371
"never": { "void": {} },
372+
"Promise": {
373+
"jsObject": {
374+
"_0": "JSPromise"
375+
}
376+
},
367377
};
368-
const typeString = this.checker.typeToString(type);
378+
const typeString = type.getSymbol()?.name ?? this.checker.typeToString(type);
369379
if (typeMap[typeString]) {
370380
return typeMap[typeString];
371381
}
@@ -377,7 +387,7 @@ export class TypeProcessor {
377387
if (this.checker.isTypeAssignableTo(type, this.checker.getStringType())) {
378388
return { "string": {} };
379389
}
380-
if (type.getFlags() & ts.TypeFlags.TypeParameter) {
390+
if (type.isTypeParameter()) {
381391
return { "jsObject": {} };
382392
}
383393

@@ -412,3 +422,24 @@ export class TypeProcessor {
412422
return undefined;
413423
}
414424
}
425+
426+
/**
427+
* @param {ts.Type} type
428+
* @returns {type is ts.ObjectType}
429+
*/
430+
function isObjectType(type) {
431+
// @ts-ignore
432+
return typeof type.objectFlags === "number";
433+
}
434+
435+
/**
436+
*
437+
* @param {ts.Type} type
438+
* @returns {type is ts.TypeReference}
439+
*/
440+
function isTypeReference(type) {
441+
return (
442+
isObjectType(type) &&
443+
(type.objectFlags & ts.ObjectFlags.Reference) !== 0
444+
);
445+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function asyncReturnVoid(): Promise<void>;
2+
export function asyncRoundTripInt(v: number): Promise<number>;
3+
export function asyncRoundTripString(v: string): Promise<string>;
4+
export function asyncRoundTripBool(v: boolean): Promise<boolean>;
5+
export function asyncRoundTripFloat(v: number): Promise<number>;
6+
export function asyncRoundTripDouble(v: number): Promise<number>;
7+
export function asyncRoundTripJSObject(v: any): Promise<any>;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@JS func asyncReturnVoid() async {}
2+
@JS func asyncRoundTripInt(_ v: Int) async -> Int {
3+
return v
4+
}
5+
@JS func asyncRoundTripString(_ v: String) async -> String {
6+
return v
7+
}
8+
@JS func asyncRoundTripBool(_ v: Bool) async -> Bool {
9+
return v
10+
}
11+
@JS func asyncRoundTripFloat(_ v: Float) async -> Float {
12+
return v
13+
}
14+
@JS func asyncRoundTripDouble(_ v: Double) async -> Double {
15+
return v
16+
}
17+
@JS func asyncRoundTripJSObject(_ v: JSObject) async -> JSObject {
18+
return v
19+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ export async function createInstantiator(options, swift) {
1515
let tmpRetBytes;
1616
let tmpRetException;
1717
return {
18-
/** @param {WebAssembly.Imports} importObject */
19-
addImports: (importObject) => {
18+
/**
19+
* @param {WebAssembly.Imports} importObject
20+
*/
21+
addImports: (importObject, importsContext) => {
2022
const bjs = {};
2123
importObject["bjs"] = bjs;
24+
const imports = options.getImports(importsContext);
2225
bjs["swift_js_return_string"] = function(ptr, len) {
2326
const bytes = new Uint8Array(memory.buffer, ptr, len);
2427
tmpRetString = textDecoder.decode(bytes);
@@ -50,21 +53,21 @@ export async function createInstantiator(options, swift) {
5053
const TestModule = importObject["TestModule"] = importObject["TestModule"] || {};
5154
TestModule["bjs_checkArray"] = function bjs_checkArray(a) {
5255
try {
53-
options.imports.checkArray(swift.memory.getObject(a));
56+
imports.checkArray(swift.memory.getObject(a));
5457
} catch (error) {
5558
setException(error);
5659
}
5760
}
5861
TestModule["bjs_checkArrayWithLength"] = function bjs_checkArrayWithLength(a, b) {
5962
try {
60-
options.imports.checkArrayWithLength(swift.memory.getObject(a), b);
63+
imports.checkArrayWithLength(swift.memory.getObject(a), b);
6164
} catch (error) {
6265
setException(error);
6366
}
6467
}
6568
TestModule["bjs_checkArray"] = function bjs_checkArray(a) {
6669
try {
67-
options.imports.checkArray(swift.memory.getObject(a));
70+
imports.checkArray(swift.memory.getObject(a));
6871
} catch (error) {
6972
setException(error);
7073
}

0 commit comments

Comments
 (0)