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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ let package = Package(
),
.testTarget(
name: "BridgeJSRuntimeTests",
dependencies: ["JavaScriptKit"],
dependencies: ["JavaScriptKit", "JavaScriptEventLoop"],
exclude: [
"bridge-js.config.json",
"bridge-js.d.ts",
Expand Down
54 changes: 51 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,9 @@ 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 @@ -410,6 +412,12 @@ 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 @@ -433,7 +441,40 @@ 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 @@ -474,7 +515,14 @@ 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
41 changes: 30 additions & 11 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,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 @@ -239,6 +242,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 @@ -312,8 +323,14 @@ 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 @@ -331,7 +348,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 @@ -362,7 +379,7 @@ struct BridgeJSLink {
jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })

dtsExportEntryLines.append(
"new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
"new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name), effects: constructor.effects));"
.indent(count: 4)
)
}
Expand All @@ -384,7 +401,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 @@ -446,7 +463,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 @@ -455,7 +472,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 @@ -533,9 +550,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 @@ -610,7 +628,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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
// DO NOT EDIT.
//
// To update this file, just rebuild your project or run
// `swift package bridge-js`.

export type Exports = {
asyncReturnVoid(): Promise<void>;
asyncRoundTripInt(v: number): Promise<number>;
asyncRoundTripString(v: string): Promise<string>;
asyncRoundTripBool(v: boolean): Promise<boolean>;
asyncRoundTripFloat(v: number): Promise<number>;
asyncRoundTripDouble(v: number): Promise<number>;
asyncRoundTripJSObject(v: any): Promise<any>;
}
export type Imports = {
}
export function createInstantiator(options: {
imports: Imports;
}, swift: any): Promise<{
addImports: (importObject: WebAssembly.Imports) => void;
setInstance: (instance: WebAssembly.Instance) => void;
createExports: (instance: WebAssembly.Instance) => Exports;
}>;
Loading
Loading