Skip to content

Commit 27dab51

Browse files
BridgeJS: Add JSTypedClosure API
1 parent 6b3a697 commit 27dab51

File tree

24 files changed

+3672
-1143
lines changed

24 files changed

+3672
-1143
lines changed

Package.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ let useLegacyResourceBundling =
99
Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false
1010

1111
let testingLinkerFlags: [LinkerSetting] = [
12-
.unsafeFlags([
13-
"-Xlinker", "--stack-first",
14-
"-Xlinker", "--global-base=524288",
15-
"-Xlinker", "-z",
16-
"-Xlinker", "stack-size=524288",
17-
])
12+
.unsafeFlags(
13+
[
14+
"-Xlinker", "--stack-first",
15+
"-Xlinker", "--global-base=524288",
16+
"-Xlinker", "-z",
17+
"-Xlinker", "stack-size=524288",
18+
],
19+
.when(platforms: [.wasi])
20+
)
1821
]
1922

2023
let package = Package(

Package@swift-6.2.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ let tracingTrait = Trait(
1616
)
1717

1818
let testingLinkerFlags: [LinkerSetting] = [
19-
.unsafeFlags([
20-
"-Xlinker", "--stack-first",
21-
"-Xlinker", "--global-base=524288",
22-
"-Xlinker", "-z",
23-
"-Xlinker", "stack-size=524288",
24-
])
19+
.unsafeFlags(
20+
[
21+
"-Xlinker", "--stack-first",
22+
"-Xlinker", "--global-base=524288",
23+
"-Xlinker", "-z",
24+
"-Xlinker", "stack-size=524288",
25+
],
26+
.when(platforms: [.wasi])
27+
)
2528
]
2629

2730
let package = Package(

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 66 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@ public struct ClosureCodegen {
3232
func renderClosureHelpers(_ signature: ClosureSignature) throws -> [DeclSyntax] {
3333
let mangledName = signature.mangleName
3434
let helperName = "_BJS_Closure_\(mangledName)"
35-
let boxClassName = "_BJS_ClosureBox_\(mangledName)"
3635

3736
let closureParams = signature.parameters.enumerated().map { _, type in
3837
"\(type.swiftType)"
3938
}.joined(separator: ", ")
4039

4140
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
4241
let swiftReturnType = signature.returnType.swiftType
43-
let closureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
42+
let swiftClosureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
4443

4544
let externName = "invoke_js_callback_\(signature.moduleName)_\(mangledName)"
4645

@@ -65,17 +64,25 @@ public struct ClosureCodegen {
6564

6665
// Get the body code
6766
let bodyCode = builder.getBody()
67+
let wasmBody = SwiftCodePattern.buildWasmConditionalCompilation(wasmBody: bodyCode.statements)
68+
.formatted(using: BasicFormat()).description
69+
let closureParamsList = signature.parameters.enumerated().map { "param\($0.offset)" }.joined(separator: ", ")
70+
let wrapperParamClause = closureParamsList.isEmpty ? "" : "\(closureParamsList) "
71+
let wrapperParamIn = closureParamsList.isEmpty ? "" : "in "
72+
let wrapperCallArguments = closureParamsList.isEmpty ? "" : "\(closureParamsList)"
6873

6974
// Generate extern declaration using CallJSEmission
7075
let externDecl = builder.renderImportDecl()
7176

72-
let boxClassDecl: DeclSyntax = """
73-
private final class \(raw: boxClassName): _BridgedSwiftClosureBox {
74-
let closure: \(raw: closureType)
75-
init(_ closure: @escaping \(raw: closureType)) {
76-
self.closure = closure
77-
}
77+
let makeClosureExternDecl: DeclSyntax = """
78+
#if arch(wasm32)
79+
@_extern(wasm, module: "bjs", name: "make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)")
80+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32
81+
#else
82+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32 {
83+
fatalError("Only available on WebAssembly")
7884
}
85+
#endif
7986
"""
8087

8188
let helperEnumDecl = EnumDeclSyntax(
@@ -84,33 +91,6 @@ public struct ClosureCodegen {
8491
},
8592
name: .identifier(helperName),
8693
memberBlockBuilder: {
87-
DeclSyntax(
88-
FunctionDeclSyntax(
89-
modifiers: DeclModifierListSyntax {
90-
DeclModifierSyntax(name: .keyword(.static))
91-
},
92-
name: .identifier("bridgeJSLower"),
93-
signature: FunctionSignatureSyntax(
94-
parameterClause: FunctionParameterClauseSyntax {
95-
FunctionParameterSyntax(
96-
firstName: .wildcardToken(),
97-
secondName: .identifier("closure"),
98-
colon: .colonToken(),
99-
type: TypeSyntax("@escaping \(raw: closureType)")
100-
)
101-
},
102-
returnClause: ReturnClauseSyntax(
103-
arrow: .arrowToken(),
104-
type: IdentifierTypeSyntax(name: .identifier("UnsafeMutableRawPointer"))
105-
)
106-
),
107-
body: CodeBlockSyntax {
108-
"let box = \(raw: boxClassName)(closure)"
109-
"return Unmanaged.passRetained(box).toOpaque()"
110-
}
111-
)
112-
)
113-
11494
DeclSyntax(
11595
FunctionDeclSyntax(
11696
modifiers: DeclModifierListSyntax {
@@ -128,61 +108,58 @@ public struct ClosureCodegen {
128108
},
129109
returnClause: ReturnClauseSyntax(
130110
arrow: .arrowToken(),
131-
type: IdentifierTypeSyntax(name: .identifier(closureType))
111+
type: IdentifierTypeSyntax(name: .identifier(swiftClosureType))
132112
)
133113
),
134114
body: CodeBlockSyntax {
135115
"let callback = JSObject.bridgeJSLiftParameter(callbackId)"
136-
ReturnStmtSyntax(
137-
expression: ClosureExprSyntax(
138-
leftBrace: .leftBraceToken(),
139-
signature: ClosureSignatureSyntax(
140-
capture: ClosureCaptureClauseSyntax(
141-
leftSquare: .leftSquareToken(),
142-
items: ClosureCaptureListSyntax {
143-
#if canImport(SwiftSyntax602)
144-
ClosureCaptureSyntax(
145-
name: .identifier("", presence: .missing),
146-
initializer: InitializerClauseSyntax(
147-
equal: .equalToken(presence: .missing),
148-
nil,
149-
value: ExprSyntax("callback")
150-
),
151-
trailingTrivia: nil
152-
)
153-
#else
154-
ClosureCaptureSyntax(
155-
expression: ExprSyntax("callback")
156-
)
157-
#endif
158-
},
159-
rightSquare: .rightSquareToken()
160-
),
161-
parameterClause: .simpleInput(
162-
ClosureShorthandParameterListSyntax {
163-
for (index, _) in signature.parameters.enumerated() {
164-
ClosureShorthandParameterSyntax(name: .identifier("param\(index)"))
165-
}
166-
}
167-
),
168-
inKeyword: .keyword(.in)
169-
),
170-
statements: CodeBlockItemListSyntax {
171-
SwiftCodePattern.buildWasmConditionalCompilation(wasmBody: bodyCode.statements)
172-
},
173-
rightBrace: .rightBraceToken()
174-
)
175-
)
116+
"""
117+
let callable: \(raw: swiftClosureType) = { [callback] \(raw: closureParamsList) in
118+
\(raw: wasmBody)
119+
}
120+
"""
121+
"return callable"
176122
}
177123
)
178124
)
179125
}
180126
)
181-
return [externDecl, boxClassDecl, DeclSyntax(helperEnumDecl)]
127+
let typedClosureExtension: DeclSyntax = """
128+
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
129+
init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
130+
self.init(
131+
makeClosure: make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName),
132+
body: body,
133+
fileID: fileID,
134+
line: line
135+
)
136+
}
137+
138+
static func oneshot(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) -> JSTypedClosure<Signature> {
139+
var typedClosure: JSTypedClosure<Signature>!
140+
let wrapper: Signature = { \(raw: wrapperParamClause)\(raw: wrapperParamIn)
141+
defer { typedClosure.release() }
142+
return body(\(raw: wrapperCallArguments))
143+
}
144+
typedClosure = JSTypedClosure(fileID: fileID, line: line, wrapper)
145+
return typedClosure
146+
}
147+
}
148+
"""
149+
150+
return [
151+
externDecl, makeClosureExternDecl, DeclSyntax(helperEnumDecl), typedClosureExtension,
152+
]
182153
}
183154

184155
func renderClosureInvokeHandler(_ signature: ClosureSignature) throws -> DeclSyntax {
185-
let boxClassName = "_BJS_ClosureBox_\(signature.mangleName)"
156+
let closureParams = signature.parameters.enumerated().map { _, type in
157+
"\(type.swiftType)"
158+
}.joined(separator: ", ")
159+
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
160+
let swiftReturnType = signature.returnType.swiftType
161+
let swiftClosureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
162+
let boxType = "_BridgeJSTypedClosureBox<\(swiftClosureType)>"
186163
let abiName = "invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)"
187164

188165
// Build ABI parameters directly with WasmCoreType (no string conversion needed)
@@ -205,7 +182,7 @@ public struct ClosureCodegen {
205182
liftedParams.append("\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))")
206183
}
207184

208-
let closureCallExpr = ExprSyntax("box.closure(\(raw: liftedParams.joined(separator: ", ")))")
185+
let closureCallExpr = ExprSyntax("closure(\(raw: liftedParams.joined(separator: ", ")))")
209186

210187
// Determine return type
211188
let abiReturnWasmType: WasmCoreType?
@@ -217,6 +194,8 @@ public struct ClosureCodegen {
217194
abiReturnWasmType = nil
218195
}
219196

197+
let throwReturn = abiReturnWasmType?.swiftReturnPlaceholderStmt ?? "return"
198+
220199
// Build signature using SwiftSignatureBuilder
221200
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
222201
abiParameters: abiParams,
@@ -225,7 +204,13 @@ public struct ClosureCodegen {
225204

226205
// Build body
227206
let body = CodeBlockItemListSyntax {
228-
"let box = Unmanaged<\(raw: boxClassName)>.fromOpaque(boxPtr).takeUnretainedValue()"
207+
"let box = Unmanaged<\(raw: boxType)>.fromOpaque(boxPtr).takeUnretainedValue()"
208+
"""
209+
guard let closure = box.closure else {
210+
box._bridgeJSThrowReleasedClosure()
211+
\(raw: throwReturn)
212+
}
213+
"""
229214
if signature.returnType == .void {
230215
closureCallExpr
231216
} else {
@@ -315,7 +300,7 @@ public struct ClosureCodegen {
315300
for setter in type.setters {
316301
collectClosureSignatures(from: setter.type, into: &closureSignatures)
317302
}
318-
for method in type.methods {
303+
for method in type.methods + type.staticMethods {
319304
collectClosureSignatures(from: method.parameters, into: &closureSignatures)
320305
collectClosureSignatures(from: method.returnType, into: &closureSignatures)
321306
}

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ public class ExportSwift {
364364
}
365365

366366
switch returnType {
367-
case .closure(let signature):
368-
append("return _BJS_Closure_\(raw: signature.mangleName).bridgeJSLower(ret)")
367+
case .closure:
368+
append("return JSTypedClosure(ret)._bridgeJSLowerReturn()")
369369
case .array, .nullable(.array, _):
370370
let stackCodegen = StackCodegen()
371371
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {
@@ -423,14 +423,7 @@ public class ExportSwift {
423423
}
424424

425425
private func returnPlaceholderStmt() -> String {
426-
switch abiReturnType {
427-
case .i32: return "return 0"
428-
case .i64: return "return 0"
429-
case .f32: return "return 0.0"
430-
case .f64: return "return 0.0"
431-
case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped"
432-
case .none: return "return"
433-
}
426+
return abiReturnType?.swiftReturnPlaceholderStmt ?? "return"
434427
}
435428
}
436429

@@ -1756,6 +1749,35 @@ extension BridgeType {
17561749
}
17571750
}
17581751

1752+
/// Swift-side parameter type to use when sending a closure into JavaScript (ImportTS).
1753+
var swiftImportParameterType: String {
1754+
switch self {
1755+
case .closure(let signature):
1756+
let paramTypes = signature.parameters.map { $0.swiftType }.joined(separator: ", ")
1757+
let effectsStr = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
1758+
return "JSTypedClosure<(\(paramTypes))\(effectsStr) -> \(signature.returnType.swiftType)>"
1759+
case .nullable(let wrapped, let kind):
1760+
if wrapped.isClosureType {
1761+
let wrappedType = wrapped.swiftImportParameterType
1762+
switch kind {
1763+
case .null:
1764+
return "Optional<\(wrappedType)>"
1765+
case .undefined:
1766+
return "JSUndefinedOr<\(wrappedType)>"
1767+
}
1768+
} else {
1769+
return swiftType
1770+
}
1771+
default:
1772+
return swiftType
1773+
}
1774+
}
1775+
1776+
var isClosureType: Bool {
1777+
if case .closure = self { return true }
1778+
return false
1779+
}
1780+
17591781
struct LiftingIntrinsicInfo: Sendable {
17601782
let parameters: [(name: String, type: WasmCoreType)]
17611783

@@ -1853,7 +1875,7 @@ extension BridgeType {
18531875
case .namespaceEnum:
18541876
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
18551877
case .closure:
1856-
return .swiftHeapObject
1878+
return .jsObject
18571879
case .array, .dictionary:
18581880
return .array
18591881
}

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,9 @@ public struct ImportTS {
9797

9898
let initializerExpr: ExprSyntax
9999
switch param.type {
100-
case .closure(let signature):
100+
case .closure:
101101
initializerExpr = ExprSyntax(
102-
"_BJS_Closure_\(raw: signature.mangleName).bridgeJSLower(\(raw: param.name))"
102+
"\(raw: param.name)._bridgeJSLowerParameter()"
103103
)
104104
default:
105105
initializerExpr = ExprSyntax("\(raw: param.name).bridgeJSLowerParameter()")
@@ -722,12 +722,12 @@ struct SwiftSignatureBuilder {
722722
}
723723

724724
/// Builds a parameter type syntax from a BridgeType.
725-
///
726-
/// Swift closure parameters must be `@escaping` because they are boxed and can be invoked from JavaScript.
727725
static func buildParameterTypeSyntax(from type: BridgeType) -> TypeSyntax {
728726
switch type {
729727
case .closure:
730-
return TypeSyntax("@escaping \(raw: type.swiftType)")
728+
return TypeSyntax("\(raw: type.swiftImportParameterType)")
729+
case .nullable(let wrapped, _) where wrapped.isClosureType:
730+
return TypeSyntax("\(raw: type.swiftImportParameterType)")
731731
default:
732732
return buildTypeSyntax(from: type)
733733
}
@@ -930,8 +930,8 @@ extension BridgeType {
930930
case .jsValue: return .jsValue
931931
case .void: return .void
932932
case .closure:
933-
// Swift closure is boxed and passed to JS as a pointer.
934-
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
933+
// Swift closure is passed to JS as a JS function reference.
934+
return LoweringParameterInfo(loweredParameters: [("funcRef", .i32)])
935935
case .unsafePointer:
936936
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
937937
case .swiftHeapObject(let className):

0 commit comments

Comments
 (0)