Skip to content

BridgeJS: Async function support #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions Examples/ImportTS/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
const { exports } = await init({
imports: {
consoleLog: (message) => {
console.log(message);
},
getDocument: () => {
return document;
},
getImports() {
return {
consoleLog: (message) => {
console.log(message);
},
getDocument: () => {
return document;
},
}
}
});

Expand Down
6 changes: 4 additions & 2 deletions Examples/PlayBridgeJS/Sources/JavaScript/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ export class BridgeJSPlayground {
// Import the BridgeJS module
const { init } = await import("../../.build/plugins/PackageToJS/outputs/Package/index.js");
const { exports } = await init({
imports: {
createTS2Skeleton: this.createTS2Skeleton
getImports() {
return {
createTS2Skeleton: this.createTS2Skeleton
}
}
});
this.playBridgeJS = new exports.PlayBridgeJS();
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ let package = Package(
),
.testTarget(
name: "BridgeJSRuntimeTests",
dependencies: ["JavaScriptKit"],
dependencies: ["JavaScriptKit", "JavaScriptEventLoop"],
exclude: [
"bridge-js.config.json",
"bridge-js.d.ts",
Expand Down
53 changes: 50 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,9 @@ public class ExportSwift {
var callExpr: ExprSyntax =
"\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
if effects.isAsync {
callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr))
callExpr = ExprSyntax(
AwaitExprSyntax(awaitKeyword: .keyword(.await).with(\.trailingTrivia, .space), expression: callExpr)
)
}
if effects.isThrows {
callExpr = ExprSyntax(
Expand All @@ -463,6 +465,11 @@ public class ExportSwift {
)
)
}

if effects.isAsync, returnType != .void {
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr).jsValue")))
}

let retMutability = returnType == .string ? "var" : "let"
if returnType == .void {
return CodeBlockItemSyntax(item: .init(ExpressionStmtSyntax(expression: callExpr)))
Expand All @@ -486,7 +493,40 @@ public class ExportSwift {
}

func lowerReturnValue(returnType: BridgeType) {
abiReturnType = returnType.abiReturnType
if effects.isAsync {
// Async functions always return a Promise, which is a JSObject
_lowerReturnValue(returnType: .jsObject(nil))
} else {
_lowerReturnValue(returnType: returnType)
}
}

private func _lowerReturnValue(returnType: BridgeType) {
switch returnType {
case .void:
abiReturnType = nil
case .bool:
abiReturnType = .i32
case .int:
abiReturnType = .i32
case .float:
abiReturnType = .f32
case .double:
abiReturnType = .f64
case .string:
abiReturnType = nil
case .jsObject:
abiReturnType = .i32
case .swiftHeapObject:
// UnsafeMutableRawPointer is returned as an i32 pointer
abiReturnType = .pointer
}

if effects.isAsync {
// The return value of async function (T of `(...) async -> T`) is
// handled by the JSPromise.async, so we don't need to do anything here.
return
}

switch returnType {
case .void: break
Expand Down Expand Up @@ -527,7 +567,14 @@ public class ExportSwift {

func render(abiName: String) -> DeclSyntax {
let body: CodeBlockItemListSyntax
if effects.isThrows {
if effects.isAsync {
body = """
let ret = JSPromise.async {
\(CodeBlockItemListSyntax(self.body))
}.jsObject
return _swift_js_retain(Int32(bitPattern: ret.id))
"""
} else if effects.isThrows {
body = """
do {
\(CodeBlockItemListSyntax(self.body))
Expand Down
48 changes: 34 additions & 14 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,13 @@ struct BridgeJSLink {
let tmpRetBytes;
let tmpRetException;
return {
/** @param {WebAssembly.Imports} importObject */
addImports: (importObject) => {
/**
* @param {WebAssembly.Imports} importObject
*/
addImports: (importObject, importsContext) => {
const bjs = {};
importObject["bjs"] = bjs;
const imports = options.getImports(importsContext);
bjs["swift_js_return_string"] = function(ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : "");
tmpRetString = textDecoder.decode(bytes);
Expand Down Expand Up @@ -294,7 +297,7 @@ struct BridgeJSLink {
// Add methods
for method in type.methods {
let methodSignature =
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));"
typeDefinitions.append(methodSignature.indent(count: 4))
}

Expand Down Expand Up @@ -368,7 +371,7 @@ struct BridgeJSLink {

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

Expand All @@ -394,7 +397,7 @@ struct BridgeJSLink {

for function in functions {
let signature =
"function \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
"function \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
dtsLines.append("\(signature)".indent(count: identBaseSize * (parts.count + 1)))
}

Expand Down Expand Up @@ -446,6 +449,14 @@ struct BridgeJSLink {
}

func call(abiName: String, returnType: BridgeType) -> String? {
if effects.isAsync {
return _call(abiName: abiName, returnType: .jsObject(nil))
} else {
return _call(abiName: abiName, returnType: returnType)
}
}

private func _call(abiName: String, returnType: BridgeType) -> String? {
let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
var returnExpr: String?

Expand Down Expand Up @@ -519,8 +530,15 @@ struct BridgeJSLink {
}
}

private func renderTSSignature(parameters: [Parameter], returnType: BridgeType) -> String {
return "(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnType.tsType)"
private func renderTSSignature(parameters: [Parameter], returnType: BridgeType, effects: Effects) -> String {
let returnTypeWithEffect: String
if effects.isAsync {
returnTypeWithEffect = "Promise<\(returnType.tsType)>"
} else {
returnTypeWithEffect = returnType.tsType
}
return
"(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnTypeWithEffect)"
}

func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) {
Expand All @@ -538,7 +556,7 @@ struct BridgeJSLink {
)
var dtsLines: [String] = []
dtsLines.append(
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: function.effects));"
)

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

dtsExportEntryLines.append(
"constructor\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
"constructor\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));"
.indent(count: 4)
)
}
Expand All @@ -603,7 +621,7 @@ struct BridgeJSLink {
).map { $0.indent(count: 4) }
)
dtsTypeLines.append(
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
.indent(count: 4)
)
}
Expand Down Expand Up @@ -712,7 +730,7 @@ struct BridgeJSLink {
}

func call(name: String, returnType: BridgeType) {
let call = "options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
let call = "imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
if returnType == .void {
bodyLines.append("\(call);")
} else {
Expand All @@ -721,7 +739,7 @@ struct BridgeJSLink {
}

func callConstructor(name: String) {
let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
let call = "new imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
bodyLines.append("let ret = \(call);")
}

Expand Down Expand Up @@ -801,9 +819,10 @@ struct BridgeJSLink {
returnExpr: returnExpr,
returnType: function.returnType
)
let effects = Effects(isAsync: false, isThrows: false)
importObjectBuilder.appendDts(
[
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType, effects: effects));"
]
)
importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines)
Expand Down Expand Up @@ -878,7 +897,8 @@ struct BridgeJSLink {
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
importObjectBuilder.appendDts([
"\(type.name): {",
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4),
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType, effects: Effects(isAsync: false, isThrows: false)));"
.indent(count: 4),
"}",
])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ export type Parameter = {
type: BridgeType;
}

export type Effects = {
isAsync: boolean;
}

export type ImportFunctionSkeleton = {
name: string;
parameters: Parameter[];
returnType: BridgeType;
effects: Effects;
documentation: string | undefined;
}

Expand Down
35 changes: 33 additions & 2 deletions Plugins/BridgeJS/Sources/TS2Skeleton/JavaScript/src/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class TypeProcessor {
parameters,
returnType: bridgeReturnType,
documentation,
effects: { isAsync: false },
};
}

Expand Down Expand Up @@ -341,6 +342,10 @@ export class TypeProcessor {
* @private
*/
visitType(type, node) {
// Treat A<B> and A<C> as the same type
if (isTypeReference(type)) {
type = type.target;
}
const maybeProcessed = this.processedTypes.get(type);
if (maybeProcessed) {
return maybeProcessed;
Expand All @@ -364,8 +369,13 @@ export class TypeProcessor {
"object": { "jsObject": {} },
"symbol": { "jsObject": {} },
"never": { "void": {} },
"Promise": {
"jsObject": {
"_0": "JSPromise"
}
},
};
const typeString = this.checker.typeToString(type);
const typeString = type.getSymbol()?.name ?? this.checker.typeToString(type);
if (typeMap[typeString]) {
return typeMap[typeString];
}
Expand All @@ -377,7 +387,7 @@ export class TypeProcessor {
if (this.checker.isTypeAssignableTo(type, this.checker.getStringType())) {
return { "string": {} };
}
if (type.getFlags() & ts.TypeFlags.TypeParameter) {
if (type.isTypeParameter()) {
return { "jsObject": {} };
}

Expand Down Expand Up @@ -412,3 +422,24 @@ export class TypeProcessor {
return undefined;
}
}

/**
* @param {ts.Type} type
* @returns {type is ts.ObjectType}
*/
function isObjectType(type) {
// @ts-ignore
return typeof type.objectFlags === "number";
}

/**
*
* @param {ts.Type} type
* @returns {type is ts.TypeReference}
*/
function isTypeReference(type) {
return (
isObjectType(type) &&
(type.objectFlags & ts.ObjectFlags.Reference) !== 0
);
}
7 changes: 7 additions & 0 deletions Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Async.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function asyncReturnVoid(): Promise<void>;
export function asyncRoundTripInt(v: number): Promise<number>;
export function asyncRoundTripString(v: string): Promise<string>;
export function asyncRoundTripBool(v: boolean): Promise<boolean>;
export function asyncRoundTripFloat(v: number): Promise<number>;
export function asyncRoundTripDouble(v: number): Promise<number>;
export function asyncRoundTripJSObject(v: any): Promise<any>;
19 changes: 19 additions & 0 deletions Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@JS func asyncReturnVoid() async {}
@JS func asyncRoundTripInt(_ v: Int) async -> Int {
return v
}
@JS func asyncRoundTripString(_ v: String) async -> String {
return v
}
@JS func asyncRoundTripBool(_ v: Bool) async -> Bool {
return v
}
@JS func asyncRoundTripFloat(_ v: Float) async -> Float {
return v
}
@JS func asyncRoundTripDouble(_ v: Double) async -> Double {
return v
}
@JS func asyncRoundTripJSObject(_ v: JSObject) async -> JSObject {
return v
}
Loading