Skip to content
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
413 changes: 297 additions & 116 deletions Benchmarks/Sources/Generated/BridgeJS.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,11 @@ public func _bjs_PlayBridgeJS_init() -> UnsafeMutableRawPointer {
@_cdecl("bjs_PlayBridgeJS_updateDetailed")
public func _bjs_PlayBridgeJS_updateDetailed(_ _self: UnsafeMutableRawPointer, _ swiftSourceBytes: Int32, _ swiftSourceLength: Int32, _ dtsSourceBytes: Int32, _ dtsSourceLength: Int32) -> Void {
#if arch(wasm32)
let _tmp_dtsSource = String.bridgeJSLiftParameter(dtsSourceBytes, dtsSourceLength)
let _tmp_swiftSource = String.bridgeJSLiftParameter(swiftSourceBytes, swiftSourceLength)
let _tmp__self = PlayBridgeJS.bridgeJSLiftParameter(_self)
do {
let ret = try PlayBridgeJS.bridgeJSLiftParameter(_self).updateDetailed(swiftSource: String.bridgeJSLiftParameter(swiftSourceBytes, swiftSourceLength), dtsSource: String.bridgeJSLiftParameter(dtsSourceBytes, dtsSourceLength))
let ret = try _tmp__self.updateDetailed(swiftSource: _tmp_swiftSource, dtsSource: _tmp_dtsSource)
return ret.bridgeJSLowerReturn()
} catch let error {
if let error = error.thrownValue.object {
Expand Down
162 changes: 19 additions & 143 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,14 @@ public struct ClosureCodegen {

let helperEnumDecl: DeclSyntax = "\(raw: helperEnumDeclPrinter.lines.joined(separator: "\n"))"

let initAccessModifier = accessLevel.modifierKeyword.map { "\($0) " } ?? ""
let accessModifier = accessLevel.modifierKeyword.map { "\($0) " } ?? ""
let declaration = signature.sendingParameters ? "static func sending" : "init"
let returnClause = signature.sendingParameters ? " -> Self" : ""
let initializer = signature.sendingParameters ? "Self" : "self.init"
let typedClosureExtension: DeclSyntax = """
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
\(raw: initAccessModifier)init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
self.init(
\(raw: accessModifier)\(raw: declaration)(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType))\(raw: returnClause) {
\(raw: initializer)(
makeClosure: \(raw: externABIName),
body: body,
fileID: fileID,
Expand All @@ -135,149 +138,22 @@ public struct ClosureCodegen {
let swiftClosureType = swiftClosureType(for: signature)
let boxType = "_BridgeJSTypedClosureBox<\(swiftClosureType)>"
let abiName = "invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)"

// Build ABI parameters directly with WasmCoreType (no string conversion needed)
var abiParams: [(name: String, type: WasmCoreType)] = [("boxPtr", .pointer)]
var liftedParams: [String] = []

for (index, paramType) in signature.parameters.enumerated() {
let paramName = "param\(index)"
let liftInfo = try paramType.liftParameterInfo()

for (argName, wasmType) in liftInfo.parameters {
let fullName =
liftInfo.parameters.count > 1 ? "\(paramName)\(argName.capitalizedFirstLetter)" : paramName
abiParams.append((fullName, wasmType))
}

let argNames = liftInfo.parameters.map { (argName, _) in
liftInfo.parameters.count > 1 ? "\(paramName)\(argName.capitalizedFirstLetter)" : paramName
}
liftedParams.append(
"\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))"
)
}

let tryPrefix = signature.isThrows ? "try " : ""
let closureCallExpr = ExprSyntax("\(raw: tryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))")
let asyncTryPrefix = (signature.isThrows ? "try " : "") + "await "
let asyncClosureCallExpr = ExprSyntax(
"\(raw: asyncTryPrefix)closure(\(raw: liftedParams.joined(separator: ", ")))"
let builder = try ExportSwift.ExportedThunkBuilder(
effects: Effects(isAsync: signature.isAsync, isThrows: signature.isThrows),
returnType: signature.returnType
)

let abiReturnWasmType =
signature.isAsync
? try BridgeType.jsObject(nil).loweringReturnInfo().returnType
: try signature.returnType.loweringReturnInfo().returnType

// Build signature using SwiftSignatureBuilder
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
abiParameters: abiParams,
returnType: abiReturnWasmType
builder.abiParameterSignatures.append(("boxPtr", .pointer))
builder.parameterBindings.append(
"let closure = Unmanaged<\(raw: boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
)

let emitCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
if signature.returnType == .void {
printer.write(closureCallExpr.description)
} else {
printer.write("let result = \(closureCallExpr)")
switch signature.returnType {
case .swiftProtocol(let protocolName):
printer.write(
"return _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn()"
)
case .nullable(.swiftProtocol(let protocolName), _):
printer.write("if let result {")
printer.indent {
printer.write(
"_swift_js_return_optional_object(1, _bridgeJSUnwrapProtocolExportable(result, \"\(protocolName)\").bridgeJSLowerAsProtocolReturn())"
)
}
printer.write("} else {")
printer.indent {
printer.write("_swift_js_return_optional_object(0, 0)")
}
printer.write("}")
default:
printer.write("return result.bridgeJSLowerReturn()")
}
}
}

let emitAsyncCallAndLower: (CodeFragmentPrinter) -> Void = { printer in
printer.write("let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure")
let resolveType = signature.returnType
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
let rejectName = "Promise_reject"
let closureHead: String
if signature.isThrows {
let returnSpelling = resolveType == .void ? "" : " -> \(resolveType.closureSwiftType)"
closureHead = " () async throws(JSException)\(returnSpelling) in"
} else {
closureHead = ""
}
printer.write("return _bjs_makePromise(resolve: \(resolveName), reject: \(rejectName)) {\(closureHead)")
printer.indent {
if resolveType == .void {
printer.write(asyncClosureCallExpr.description)
} else {
printer.write("return \(asyncClosureCallExpr)")
}
}
printer.write("}")
}

let catchPlaceholderStmt = abiReturnWasmType?.swiftReturnPlaceholderStmt

// Build function declaration using helper
let funcDecl = SwiftCodePattern.buildExposedFunctionDecl(
abiName: abiName,
signature: funcSignature
) { printer in
if signature.isAsync {
emitAsyncCallAndLower(printer)
} else if signature.isThrows {
printer.write(
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
)
printer.write("do {")
printer.indent {
emitCallAndLower(printer)
}
printer.write("} catch let error {")
printer.indent {
printer.write("if let error = error.thrownValue.object {")
printer.indent {
printer.write("withExtendedLifetime(error) {")
printer.indent {
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
}
printer.write("}")
}
printer.write("} else {")
printer.indent {
printer.write("let jsError = JSError(message: error.description)")
printer.write("withExtendedLifetime(jsError.jsObject) {")
printer.indent {
printer.write("_swift_js_throw(Int32(bitPattern: $0.id))")
}
printer.write("}")
}
printer.write("}")
if let catchPlaceholderStmt {
printer.write(catchPlaceholderStmt)
}
}
printer.write("}")
} else {
printer.write(
"let closure = Unmanaged<\(boxType)>.fromOpaque(boxPtr).takeUnretainedValue().closure"
)
emitCallAndLower(printer)
}
for (index, paramType) in signature.parameters.enumerated() {
try builder.liftParameter(
param: Parameter(label: nil, name: "param\(index)", type: paramType)
)
}

return DeclSyntax(funcDecl)
builder.call(name: "closure", returnType: signature.returnType)
try builder.lowerReturnValue(returnType: signature.returnType)
return builder.render(abiName: abiName)
}

public func renderSupport(for skeleton: BridgeJSSkeleton) throws -> String? {
Expand Down
70 changes: 13 additions & 57 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ public class ExportSwift {
/// helper. Set for every `async` thunk.
var asyncResolveReturnType: BridgeType?

/// Stack-using parameter lifts hoisted ahead of the deferred async closure.
var asyncHoistedBindings: [CodeBlockItemSyntax] = []
var parameterBindings: [CodeBlockItemSyntax] = []

init(effects: Effects, returnType: BridgeType) throws {
self.effects = effects
Expand Down Expand Up @@ -254,7 +253,10 @@ public class ExportSwift {
)
}

liftedParameterExprs.append(liftingExpr)
let varName = "_tmp_\(param.name)"
let binding: CodeBlockItemSyntax = "let \(raw: varName) = \(liftingExpr)"
parameterBindings.append(binding)
liftedParameterExprs.append(ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varName))))
for (name, type) in zip(argumentsToLift, liftingInfo.parameters.map { $0.type }) {
abiParameterSignatures.append((name, type))
}
Expand Down Expand Up @@ -310,7 +312,6 @@ public class ExportSwift {
}

func call(name: String, returnType: BridgeType) {
generateParameterLifting()
let item = renderCallStatement(callee: "\(raw: name)", returnType: returnType)
append(item)
}
Expand All @@ -326,48 +327,13 @@ public class ExportSwift {

func callMethod(methodName: String, returnType: BridgeType) {
let (_, selfExpr) = removeFirstLiftedParameter()
generateParameterLifting()
let item = renderCallStatement(
callee: "\(raw: selfExpr).\(raw: methodName)",
returnType: returnType
)
append(item)
}

/// Generates intermediate variables for stack-using parameters if needed for LIFO compatibility
private func generateParameterLifting() {
let stackParamIndices = parameters.enumerated().compactMap { index, param -> Int? in
param.type.isStackUsingParameter ? index : nil
}

if effects.isAsync {
// Drain stack parameters before the deferred `Task` or the shared stack is corrupted.
for index in stackParamIndices.reversed() {
let param = parameters[index]
let expr = liftedParameterExprs[index]
let varName = "_tmp_\(param.name)"
var binding: CodeBlockItemSyntax = "let \(raw: varName) = \(expr)"
if !asyncHoistedBindings.isEmpty {
binding = binding.with(\.leadingTrivia, .newline)
}
asyncHoistedBindings.append(binding)
liftedParameterExprs[index] = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varName)))
}
return
}

guard stackParamIndices.count > 1 else { return }

for index in stackParamIndices.reversed() {
let param = parameters[index]
let expr = liftedParameterExprs[index]
let varName = "_tmp_\(param.name)"

append("let \(raw: varName) = \(expr)")
liftedParameterExprs[index] = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varName)))
}
}

func callPropertyGetter(propertyName: String, returnType: BridgeType) {
let (_, selfExpr) = removeFirstLiftedParameter()
if returnType == .void {
Expand Down Expand Up @@ -456,10 +422,11 @@ public class ExportSwift {
/// so the body must also read the captured value.
/// See: https://github.com/swiftlang/swift/issues/89320
private var asyncThrowsBodyForcesCapture: Bool {
effects.isThrows && abiParameterSignatures.isEmpty && asyncHoistedBindings.isEmpty
effects.isThrows && parameterBindings.isEmpty
}

func render(abiName: String) -> DeclSyntax {
var bindings = Array(parameterBindings.reversed())
let body: CodeBlockItemListSyntax
if effects.isAsync, let resolveType = asyncResolveReturnType {
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
Expand All @@ -468,17 +435,15 @@ public class ExportSwift {
returnSpelling: resolveType.swiftType,
forcesCapture: forcesCapture
)
var hoistedBindings = asyncHoistedBindings
var bodyItems = self.body
if forcesCapture {
hoistedBindings.append("let __bjs_capture = 0")
bindings.append("let __bjs_capture = 0")
if !bodyItems.isEmpty {
bodyItems[0] = bodyItems[0].with(\.leadingTrivia, .newline)
}
bodyItems.insert("_ = __bjs_capture", at: 0)
}
body = """
\(CodeBlockItemListSyntax(hoistedBindings))
return _bjs_makePromise(resolve: \(raw: resolveName), reject: Promise_reject) {\(raw: closureHead)
\(CodeBlockItemListSyntax(bodyItems))
}
Expand All @@ -504,6 +469,10 @@ public class ExportSwift {
} else {
body = CodeBlockItemListSyntax(self.body)
}
let preparedBody: CodeBlockItemListSyntax = """
\(CodeBlockItemListSyntax(bindings.map { $0.with(\.leadingTrivia, .newline) }))
\(body)
"""
// Build function signature using SwiftSignatureBuilder
let signature = SwiftSignatureBuilder.buildABIFunctionSignature(
abiParameters: abiParameterSignatures,
Expand All @@ -515,7 +484,7 @@ public class ExportSwift {
abiName: abiName,
signature: signature
) { printer in
printer.write(multilineString: body.description)
printer.write(multilineString: preparedBody.description)
}

return DeclSyntax(funcDecl)
Expand Down Expand Up @@ -1744,19 +1713,6 @@ extension BridgeType {
return false
}

var isStackUsingParameter: Bool {
switch self {
case .swiftStruct, .array, .dictionary, .associatedValueEnum:
return true
case .nullable(let wrapped, _):
return wrapped.isStackUsingParameter
case .alias(_, let underlying):
return underlying.isStackUsingParameter
default:
return false
}
}

struct LiftingIntrinsicInfo: Sendable {
let parameters: [(name: String, type: WasmCoreType)]

Expand Down
4 changes: 2 additions & 2 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ public struct ImportTS {
body = CodeFragmentPrinter()

let tryKeyword = effects.isThrows ? "try" : "try!"
let rejectFactory = "makeRejectClosure: { JSTypedClosure<(sending JSValue) -> Void>($0) }"
let rejectFactory = "makeRejectClosure: { JSTypedClosure<(sending JSValue) -> Void>.sending($0) }"
if returnType == .void {
let resolveFactory = "makeResolveClosure: { JSTypedClosure<() -> Void>($0) }"
body.write(
Expand All @@ -348,7 +348,7 @@ public struct ImportTS {
} else {
let resolveSwiftType = returnType.closureSwiftType
let resolveFactory =
"makeResolveClosure: { JSTypedClosure<(sending \(resolveSwiftType)) -> Void>($0) }"
"makeResolveClosure: { JSTypedClosure<(sending \(resolveSwiftType)) -> Void>.sending($0) }"
body.write(
"let resolved = \(tryKeyword) await _bjs_awaitPromise(\(resolveFactory), \(rejectFactory)) { resolveRef, rejectRef in"
)
Expand Down
Loading
Loading