Skip to content
Open
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
15 changes: 15 additions & 0 deletions Benchmarks/Sources/Generated/JavaScript/BridgeJS.json
Original file line number Diff line number Diff line change
Expand Up @@ -2839,6 +2839,11 @@
{
"functions" : [
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"name" : "benchmarkHelperNoop",
"parameters" : [

Expand All @@ -2850,6 +2855,11 @@
}
},
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"name" : "benchmarkHelperNoopWithNumber",
"parameters" : [
{
Expand All @@ -2868,6 +2878,11 @@
}
},
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"name" : "benchmarkRunner",
"parameters" : [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@
{
"functions" : [
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"name" : "createTS2Swift",
"parameters" : [

Expand All @@ -260,6 +265,11 @@
],
"methods" : [
{
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : true
},
"name" : "convert",
"parameters" : [
{
Expand Down
78 changes: 76 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ public struct ClosureCodegen {
public init() {}

private func swiftClosureType(for signature: ClosureSignature) -> String {
let closureParams = signature.parameters.map { "\($0.closureSwiftType)" }.joined(separator: ", ")
let sendingPrefix = signature.sendingParameters ? "sending " : ""
let closureParams = signature.parameters.map { "\(sendingPrefix)\($0.closureSwiftType)" }.joined(
separator: ", "
)
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
let swiftReturnType = signature.returnType.closureSwiftType
return "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
Expand Down Expand Up @@ -188,7 +191,78 @@ public struct ClosureCodegen {
let collector = ClosureSignatureCollectorVisitor()
var walker = BridgeTypeWalker(visitor: collector)
walker.walk(skeleton)
let closureSignatures = walker.visitor.signatures
var closureSignatures = walker.visitor.signatures

// When async imports exist, inject closure signatures for the typed resolve
// and reject callbacks used by _bjs_awaitPromise.
// - Reject always uses (sending JSValue) -> Void
// - Resolve uses a typed closure matching the return type (or () -> Void for void)
// All async callback closures use `sending` parameters so values can be
// transferred through the checked continuation without Sendable constraints.
if let imported = skeleton.imported {
for file in imported.children {
for function in file.functions where function.effects.isAsync {
// Reject callback
closureSignatures.insert(
ClosureSignature(
parameters: [.jsValue],
returnType: .void,
moduleName: skeleton.moduleName,
sendingParameters: true
)
)
// Resolve callback (typed per return type)
if function.returnType == .void {
closureSignatures.insert(
ClosureSignature(
parameters: [],
returnType: .void,
moduleName: skeleton.moduleName
)
)
} else {
closureSignatures.insert(
ClosureSignature(
parameters: [function.returnType],
returnType: .void,
moduleName: skeleton.moduleName,
sendingParameters: true
)
)
}
}
for type in file.types {
for method in type.methods where method.effects.isAsync {
closureSignatures.insert(
ClosureSignature(
parameters: [.jsValue],
returnType: .void,
moduleName: skeleton.moduleName,
sendingParameters: true
)
)
if method.returnType == .void {
closureSignatures.insert(
ClosureSignature(
parameters: [],
returnType: .void,
moduleName: skeleton.moduleName
)
)
} else {
closureSignatures.insert(
ClosureSignature(
parameters: [method.returnType],
returnType: .void,
moduleName: skeleton.moduleName,
sendingParameters: true
)
)
}
}
}
}
}

guard !closureSignatures.isEmpty else { return nil }

Expand Down
107 changes: 91 additions & 16 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,16 @@ public struct ImportTS {
}
}

func call() throws {
/// Prepends `resolveRef: Int32, rejectRef: Int32` parameters to the ABI parameter list.
///
/// Used for async imports where the JS side receives closure-backed
/// resolve/reject callbacks as object references.
func prependClosureCallbackParams() {
abiParameterSignatures.insert(contentsOf: [("resolveRef", .i32), ("rejectRef", .i32)], at: 0)
abiParameterForwardings.insert(contentsOf: ["resolveRef", "rejectRef"], at: 0)
}

func call(skipExceptionCheck: Bool = false) throws {
for stmt in stackLoweringStmts {
body.write(stmt.description)
}
Expand Down Expand Up @@ -243,8 +252,9 @@ public struct ImportTS {
}
}

// Add exception check for ImportTS context
if context == .importTS {
// Add exception check for ImportTS context (skipped for async, where
// errors are funneled through the JS-side reject path)
if !skipExceptionCheck && context == .importTS {
body.write("if let error = _swift_js_take_exception() { throw error }")
}
}
Expand Down Expand Up @@ -278,6 +288,41 @@ public struct ImportTS {
}
}

func liftAsyncReturnValue(originalReturnType: BridgeType) {
// For async imports, the extern function takes leading `resolveRef: Int32, rejectRef: Int32`
// and returns void. The JS side calls the resolve/reject closures when the Promise settles.
// The resolve closure is typed to match the return type, so the ABI conversion is handled
// by the existing closure codegen infrastructure — no manual JSValue-to-type switch needed.
abiReturnType = nil

// Wrap the existing body (parameter lowering + extern call) in _bjs_awaitPromise
let innerBody = body
body = CodeFragmentPrinter()

let rejectFactory = "makeRejectClosure: { JSTypedClosure<(sending JSValue) -> Void>($0) }"
if originalReturnType == .void {
let resolveFactory = "makeResolveClosure: { JSTypedClosure<() -> Void>($0) }"
body.write(
"try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
)
} else {
let resolveSwiftType = originalReturnType.closureSwiftType
let resolveFactory =
"makeResolveClosure: { JSTypedClosure<(sending \(resolveSwiftType)) -> Void>($0) }"
body.write(
"let resolved = try await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
)
}
body.indent {
body.write(lines: innerBody.lines)
}
body.write("}")

if originalReturnType != .void {
body.write("return resolved")
}
}

func assignThis(returnType: BridgeType) {
guard case .jsObject = returnType else {
preconditionFailure("assignThis can only be called with a jsObject return type")
Expand All @@ -299,9 +344,13 @@ public struct ImportTS {
return "\(raw: printer.lines.joined(separator: "\n"))"
}

func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax {
func renderThunkDecl(
name: String,
parameters: [Parameter],
returnType: BridgeType,
effects: Effects = Effects(isAsync: false, isThrows: true)
) -> DeclSyntax {
let printer = CodeFragmentPrinter()
let effects = Effects(isAsync: false, isThrows: true)
let signature = SwiftSignatureBuilder.buildFunctionSignature(
parameters: parameters,
returnType: returnType,
Expand Down Expand Up @@ -359,22 +408,33 @@ public struct ImportTS {
_ function: ImportedFunctionSkeleton,
topLevelDecls: inout [DeclSyntax]
) throws -> [DeclSyntax] {
// For async functions, the extern returns void (the JS side resolves/rejects
// via continuation callbacks). For sync functions, use the actual return type.
let abiReturnType: BridgeType = function.effects.isAsync ? .void : function.returnType
let builder = try CallJSEmission(
moduleName: moduleName,
abiName: function.abiName(context: nil),
returnType: function.returnType
returnType: abiReturnType
)
if function.effects.isAsync {
builder.prependClosureCallbackParams()
}
for param in function.parameters {
try builder.lowerParameter(param: param)
}
try builder.call()
try builder.liftReturnValue()
try builder.call(skipExceptionCheck: function.effects.isAsync)
if function.effects.isAsync {
builder.liftAsyncReturnValue(originalReturnType: function.returnType)
} else {
try builder.liftReturnValue()
}
topLevelDecls.append(builder.renderImportDecl())
return [
builder.renderThunkDecl(
name: Self.thunkName(function: function),
parameters: function.parameters,
returnType: function.returnType
returnType: function.returnType,
effects: function.effects
)
.with(\.leadingTrivia, Self.renderDocumentation(documentation: function.documentation))
]
Expand All @@ -385,41 +445,56 @@ public struct ImportTS {
var decls: [DeclSyntax] = []

func renderMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
let abiReturnType: BridgeType = method.effects.isAsync ? .void : method.returnType
let builder = try CallJSEmission(
moduleName: moduleName,
abiName: method.abiName(context: type),
returnType: method.returnType
returnType: abiReturnType
)
if method.effects.isAsync {
builder.prependClosureCallbackParams()
}
try builder.lowerParameter(param: selfParameter)
for param in method.parameters {
try builder.lowerParameter(param: param)
}
try builder.call()
try builder.liftReturnValue()
try builder.call(skipExceptionCheck: method.effects.isAsync)
if method.effects.isAsync {
builder.liftAsyncReturnValue(originalReturnType: method.returnType)
} else {
try builder.liftReturnValue()
}
topLevelDecls.append(builder.renderImportDecl())
return [
builder.renderThunkDecl(
name: Self.thunkName(type: type, method: method),
parameters: [selfParameter] + method.parameters,
returnType: method.returnType
returnType: method.returnType,
effects: method.effects
)
]
}

func renderStaticMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
let abiName = method.abiName(context: type, operation: "static")
let builder = try CallJSEmission(moduleName: moduleName, abiName: abiName, returnType: method.returnType)
let abiReturnType: BridgeType = method.effects.isAsync ? .jsObject(nil) : method.returnType
let builder = try CallJSEmission(moduleName: moduleName, abiName: abiName, returnType: abiReturnType)
for param in method.parameters {
try builder.lowerParameter(param: param)
}
try builder.call()
try builder.liftReturnValue()
if method.effects.isAsync {
builder.liftAsyncReturnValue(originalReturnType: method.returnType)
} else {
try builder.liftReturnValue()
}
topLevelDecls.append(builder.renderImportDecl())
return [
builder.renderThunkDecl(
name: Self.thunkName(type: type, method: method),
parameters: method.parameters,
returnType: method.returnType
returnType: method.returnType,
effects: method.effects
)
]
}
Expand Down
12 changes: 9 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2137,7 +2137,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
let valueType: BridgeType
}

/// Validates effects (throws required, async not supported)
/// Validates effects (throws required, async only supported for @JSFunction)
private func validateEffects(
_ effects: FunctionEffectSpecifiersSyntax?,
node: some SyntaxProtocol,
Expand All @@ -2153,7 +2153,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
)
return nil
}
if effects.isAsync {
if effects.isAsync && attributeName != "JSFunction" {
errors.append(
DiagnosticError(
node: node,
Expand Down Expand Up @@ -2490,7 +2490,12 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
_ jsFunction: AttributeSyntax,
_ node: FunctionDeclSyntax,
) -> ImportedFunctionSkeleton? {
guard validateEffects(node.signature.effectSpecifiers, node: node, attributeName: "JSFunction") != nil
guard
let effects = validateEffects(
node.signature.effectSpecifiers,
node: node,
attributeName: "JSFunction"
)
else {
return nil
}
Expand All @@ -2516,6 +2521,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
from: from,
parameters: parameters,
returnType: returnType,
effects: effects,
documentation: nil
)
}
Expand Down
Loading
Loading