Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
BridgeJS: Closure support review comments
  • Loading branch information
krodak committed Nov 14, 2025
commit d628d2d627b78a3d90813089215b193eef366a32
23 changes: 7 additions & 16 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class ExportSwift {
private var exportedProtocolNameByKey: [String: String] = [:]
private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver()
private let enumCodegen: EnumCodegen = EnumCodegen()
private lazy var closureCodegen = ClosureCodegen(moduleName: moduleName)
private let closureCodegen = ClosureCodegen()

public init(progress: ProgressReporting, moduleName: String) {
self.progress = progress
Expand Down Expand Up @@ -1354,6 +1354,7 @@ public class ExportSwift {
ClosureSignature(
parameters: parameters,
returnType: returnType,
moduleName: moduleName,
isAsync: isAsync,
isThrows: isThrows
)
Expand Down Expand Up @@ -1477,7 +1478,6 @@ public class ExportSwift {
}
decls.append(Self.prelude)

// Collect all unique closure signatures
var closureSignatures: Set<ClosureSignature> = []
for function in exportedFunctions {
collectClosureSignatures(from: function.parameters, into: &closureSignatures)
Expand Down Expand Up @@ -1817,20 +1817,14 @@ public class ExportSwift {
}

private struct ClosureCodegen {
let moduleName: String

func generateOptionalParameterLowering(signature: ClosureSignature) throws -> String {
var lines: [String] = []

for (index, paramType) in signature.parameters.enumerated() {
guard case .optional(let wrappedType) = paramType else {
continue
}

let paramName = "param\(index)"

// Use bridgeJSLowerParameterWithRetain for heap objects in escaping closures
// to ensure proper ownership transfer
if case .swiftHeapObject = wrappedType {
lines.append(
"let (\(paramName)IsSome, \(paramName)Value) = \(paramName).bridgeJSLowerParameterWithRetain()"
Expand Down Expand Up @@ -1859,7 +1853,7 @@ public class ExportSwift {
let closureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"

var invokeParams: [(name: String, type: String)] = [("_", "Int32")]
var invokeCallArgs: [String] = ["owner.callbackId"]
var invokeCallArgs: [String] = ["callback.bridgeJSLowerParameter()"]

for (index, paramType) in signature.parameters.enumerated() {
let paramName = "param\(index)"
Expand Down Expand Up @@ -1920,7 +1914,7 @@ public class ExportSwift {
"""
}

let externName = "invoke_js_callback_\(mangledName.lowercased())"
let externName = "invoke_js_callback_\(signature.moduleName)_\(mangledName)"
let optionalLoweringCode = try generateOptionalParameterLowering(signature: signature)

return """
Expand All @@ -1938,8 +1932,8 @@ public class ExportSwift {
}

static func bridgeJSLift(_ callbackId: Int32) -> \(raw: closureType) {
let owner = _JSCallbackOwner(callbackId: callbackId)
return { [owner] \(raw: signature.parameters.indices.map { "param\($0)" }.joined(separator: ", ")) in
let callback = JSObject.bridgeJSLiftParameter(callbackId)
return { [callback] \(raw: signature.parameters.indices.map { "param\($0)" }.joined(separator: ", ")) in
#if arch(wasm32)
@_extern(wasm, module: "bjs", name: "\(raw: externName)")
func _invoke(\(raw: invokeSignature)) -> \(raw: invokeReturnType)
Expand All @@ -1955,7 +1949,7 @@ public class ExportSwift {

func renderClosureInvokeHandler(signature: ClosureSignature) throws -> DeclSyntax {
let boxClassName = "_BJS_ClosureBox_\(signature.mangleName)"
let abiName = "invoke_swift_closure_\(signature.mangleName.lowercased())"
let abiName = "invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)"

var abiParams: [(name: String, type: String)] = [("boxPtr", "UnsafeMutableRawPointer")]
var liftedParams: [String] = []
Expand Down Expand Up @@ -2471,14 +2465,12 @@ public class ExportSwift {
return decls
}

/// Collects all closure signatures from function parameters
private func collectClosureSignatures(from parameters: [Parameter], into signatures: inout Set<ClosureSignature>) {
for param in parameters {
collectClosureSignatures(from: param.type, into: &signatures)
}
}

/// Collects all closure signatures from a bridge type
private func collectClosureSignatures(from type: BridgeType, into signatures: inout Set<ClosureSignature>) {
switch type {
case .closure(let signature):
Expand Down Expand Up @@ -2933,7 +2925,6 @@ extension BridgeType {
case .namespaceEnum:
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
case .closure:
// Closures are returned as UnsafeMutableRawPointer (box pointer)
return .swiftHeapObject
}
}
Expand Down
57 changes: 16 additions & 41 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,53 +696,29 @@ struct BridgeJSLink {
}
printer.write("}")

var closureSignatures: Set<ClosureSignature> = []
for skeleton in exportedSkeletons {
var closureSignatures: Set<ClosureSignature> = []
collectClosureSignatures(from: skeleton, into: &closureSignatures)
}

var classToModule: [String: String] = [:]
for skeleton in exportedSkeletons {
for klass in skeleton.classes {
classToModule[klass.name] = skeleton.moduleName
}
}
guard !closureSignatures.isEmpty else { continue }

for signature in closureSignatures.sorted(by: { $0.mangleName < $1.mangleName }) {
let invokeFuncName = "invoke_js_callback_\(signature.mangleName.lowercased())"
printer.write(
lines: generateInvokeFunction(
signature: signature,
functionName: invokeFuncName,
classToModule: classToModule
for signature in closureSignatures.sorted(by: { $0.mangleName < $1.mangleName }) {
let invokeFuncName = "invoke_js_callback_\(skeleton.moduleName)_\(signature.mangleName)"
printer.write(
lines: generateInvokeFunction(
signature: signature,
functionName: invokeFuncName
)
)
)

let lowerFuncName = "lower_closure_\(signature.mangleName.lowercased())"
printer.write(
lines: generateLowerClosureFunction(
signature: signature,
functionName: lowerFuncName
)
)
}

if !closureSignatures.isEmpty {
printer.nextLine()
printer.write("bjs[\"release_js_callback\"] = function(id) {")
printer.indent {
printer.write("\(JSGlueVariableScope.reservedSwift).memory.release(id);")
}
printer.write("};")

printer.nextLine()
printer.write("bjs[\"release_swift_closure\"] = function(boxPtr) {")
printer.indent {
let lowerFuncName = "lower_closure_\(skeleton.moduleName)_\(signature.mangleName)"
printer.write(
"\(JSGlueVariableScope.reservedInstance).exports._release_swift_closure(boxPtr);"
lines: generateLowerClosureFunction(
signature: signature,
functionName: lowerFuncName
)
)
}
printer.write("};")
}
}
}
Expand Down Expand Up @@ -793,8 +769,7 @@ struct BridgeJSLink {

private func generateInvokeFunction(
signature: ClosureSignature,
functionName: String,
classToModule: [String: String]
functionName: String
) -> [String] {
let printer = CodeFragmentPrinter()
let scope = JSGlueVariableScope()
Expand Down Expand Up @@ -889,7 +864,7 @@ struct BridgeJSLink {

// Call the Swift invoke function
let invokeCall =
"\(JSGlueVariableScope.reservedInstance).exports.invoke_swift_closure_\(signature.mangleName.lowercased())(\(invokeArgs.joined(separator: ", ")))"
"\(JSGlueVariableScope.reservedInstance).exports.invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)(\(invokeArgs.joined(separator: ", ")))"

let returnFragment = try! IntrinsicJSFragment.closureLiftReturn(type: signature.returnType)
_ = returnFragment.printCode([invokeCall], scope, printer, cleanupCode)
Expand Down
11 changes: 5 additions & 6 deletions Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1298,14 +1298,13 @@ struct IntrinsicJSFragment: Sendable {
let base = fullName.components(separatedBy: ".").last ?? fullName
return .associatedEnumLiftReturn(enumBase: base)
case .closure(let signature):
let lowerFuncName = "lower_closure_\(signature.moduleName)_\(signature.mangleName)"
return IntrinsicJSFragment(
parameters: ["closurePtr"],
parameters: ["boxPtr"],
printCode: { arguments, scope, printer, cleanupCode in
let closurePtr = arguments[0]
let lowerFuncName = "lower_closure_\(signature.mangleName.lowercased())"
let resultVar = scope.variable("closure")
printer.write("const \(resultVar) = bjs[\"\(lowerFuncName)\"](\(closurePtr));")
return [resultVar]
let boxPtr = arguments[0]
printer.write("return bjs[\"\(lowerFuncName)\"](\(boxPtr));")
return []
}
)
case .namespaceEnum(let string):
Expand Down
59 changes: 38 additions & 21 deletions Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,34 @@ public enum BridgeContext: Sendable {
public struct ClosureSignature: Codable, Equatable, Hashable, Sendable {
public let parameters: [BridgeType]
public let returnType: BridgeType
/// Simplified Swift ABI-style mangling with module prefix
// <moduleLength><module> + params + _ + return
// Examples:
// - 4MainSS_Si (Main module, String->Int)
// - 6MyAppSiSi_y (MyApp module, Int,Int->Void)
public let mangleName: String
public let isAsync: Bool
public let isThrows: Bool
public let mangleName: String
public let moduleName: String

public init(parameters: [BridgeType], returnType: BridgeType, isAsync: Bool = false, isThrows: Bool = false) {
public init(
parameters: [BridgeType],
returnType: BridgeType,
moduleName: String,
isAsync: Bool = false,
isThrows: Bool = false
) {
self.parameters = parameters
self.returnType = returnType
self.moduleName = moduleName
self.isAsync = isAsync
self.isThrows = isThrows

let paramPart =
parameters.isEmpty
? "Void"
: parameters.map { $0.mangleTypeName }.joined(separator: "_")
let returnPart = returnType.mangleTypeName
self.mangleName = "\(paramPart)_To_\(returnPart)"
? "y"
: parameters.map { $0.mangleTypeName }.joined()
let signaturePart = "\(paramPart)_\(returnType.mangleTypeName)"
self.mangleName = "\(moduleName.count)\(moduleName)\(signaturePart)"
}
}

Expand Down Expand Up @@ -602,31 +614,36 @@ extension BridgeType {
return false
}

/// Generates a mangled name for use in closure type names
/// Examples: "String", "Int", "MyClass", "Bool"
/// Simplified Swift ABI-style mangled name
/// https://github.com/swiftlang/swift/blob/main/docs/ABI/Mangling.rst#types
public var mangleTypeName: String {
switch self {
case .int: return "Int"
case .float: return "Float"
case .double: return "Double"
case .string: return "String"
case .bool: return "Bool"
case .void: return "Void"
case .int: return "Si"
case .float: return "Sf"
case .double: return "Sd"
case .string: return "SS"
case .bool: return "Sb"
case .void: return "y"
case .jsObject(let name):
return name ?? "JSObject"
let typeName = name ?? "JSObject"
return "\(typeName.count)\(typeName)C"
case .swiftHeapObject(let name):
return name
return "\(name.count)\(name)C"
case .optional(let wrapped):
return "Optional\(wrapped.mangleTypeName)"
return "Sq\(wrapped.mangleTypeName)"
case .caseEnum(let name),
.rawValueEnum(let name, _),
.associatedValueEnum(let name),
.namespaceEnum(let name):
return name
return "\(name.count)\(name)O"
case .swiftProtocol(let name):
return name
return "\(name.count)\(name)P"
case .closure(let signature):
return "Closure_\(signature.mangleName)"
let params =
signature.parameters.isEmpty
? "y"
: signature.parameters.map { $0.mangleTypeName }.joined()
return "K\(params)_\(signature.returnType.mangleTypeName)"
}
}

Expand Down
Loading