Skip to content

Commit 4dffde8

Browse files
BridgeJS: Add JSTypedClosure API
1 parent 63b7b72 commit 4dffde8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3241
-860
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: 78 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ 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)"
43+
let typedClosureType = "JSTypedClosure<\(swiftClosureType)>"
44+
let boxType = "_BridgeJSTypedClosureBox<\(swiftClosureType)>"
4445

4546
let externName = "invoke_js_callback_\(signature.moduleName)_\(mangledName)"
4647

@@ -65,17 +66,25 @@ public struct ClosureCodegen {
6566

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

6976
// Generate extern declaration using CallJSEmission
7077
let externDecl = builder.renderImportDecl()
7178

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-
}
79+
let makeClosureExternDecl: DeclSyntax = """
80+
#if arch(wasm32)
81+
@_extern(wasm, module: "bjs", name: "make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)")
82+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32
83+
#else
84+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32 {
85+
fatalError("Only available on WebAssembly")
7886
}
87+
#endif
7988
"""
8089

8190
let helperEnumDecl = EnumDeclSyntax(
@@ -84,33 +93,6 @@ public struct ClosureCodegen {
8493
},
8594
name: .identifier(helperName),
8695
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-
11496
DeclSyntax(
11597
FunctionDeclSyntax(
11698
modifiers: DeclModifierListSyntax {
@@ -128,61 +110,60 @@ public struct ClosureCodegen {
128110
},
129111
returnClause: ReturnClauseSyntax(
130112
arrow: .arrowToken(),
131-
type: IdentifierTypeSyntax(name: .identifier(closureType))
113+
type: IdentifierTypeSyntax(name: .identifier(swiftClosureType))
132114
)
133115
),
134116
body: CodeBlockSyntax {
135117
"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-
)
118+
"""
119+
let callable: \(raw: swiftClosureType) = { [callback] \(raw: closureParamsList) in
120+
\(raw: wasmBody)
121+
}
122+
"""
123+
"return callable"
176124
}
177125
)
178126
)
179127
}
180128
)
181-
return [externDecl, boxClassDecl, DeclSyntax(helperEnumDecl)]
129+
let typedClosureExtension: DeclSyntax = """
130+
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
131+
init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
132+
let box = \(raw: boxType)(body, fileID: fileID, line: line)
133+
let pointer = Unmanaged.passRetained(box).toOpaque()
134+
let funcRefValue = make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(pointer)
135+
let jsObject = JSObject(id: UInt32(bitPattern: funcRefValue))
136+
self.init(
137+
_boxPointer: pointer,
138+
jsObject: jsObject
139+
)
140+
}
141+
142+
static func oneshot(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) -> JSTypedClosure<Signature> {
143+
var typedClosure: JSTypedClosure<Signature>!
144+
let wrapper: Signature = { \(raw: wrapperParamClause)\(raw: wrapperParamIn)
145+
defer { typedClosure.release() }
146+
return body(\(raw: wrapperCallArguments))
147+
}
148+
typedClosure = JSTypedClosure(fileID: fileID, line: line, wrapper)
149+
return typedClosure
150+
}
151+
}
152+
"""
153+
154+
return [
155+
externDecl, makeClosureExternDecl, DeclSyntax(helperEnumDecl), typedClosureExtension,
156+
]
182157
}
183158

184159
func renderClosureInvokeHandler(_ signature: ClosureSignature) throws -> DeclSyntax {
185-
let boxClassName = "_BJS_ClosureBox_\(signature.mangleName)"
160+
let closureParams = signature.parameters.enumerated().map { _, type in
161+
"\(type.swiftType)"
162+
}.joined(separator: ", ")
163+
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
164+
let swiftReturnType = signature.returnType.swiftType
165+
let swiftClosureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
166+
let boxType = "_BridgeJSTypedClosureBox<\(swiftClosureType)>"
186167
let abiName = "invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)"
187168

188169
// Build ABI parameters directly with WasmCoreType (no string conversion needed)
@@ -205,7 +186,7 @@ public struct ClosureCodegen {
205186
liftedParams.append("\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))")
206187
}
207188

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

210191
// Determine return type
211192
let abiReturnWasmType: WasmCoreType?
@@ -217,6 +198,19 @@ public struct ClosureCodegen {
217198
abiReturnWasmType = nil
218199
}
219200

201+
let throwReturn: String
202+
if let abiReturnWasmType {
203+
switch abiReturnWasmType {
204+
case .i32: throwReturn = "return 0"
205+
case .i64: throwReturn = "return 0"
206+
case .f32: throwReturn = "return 0"
207+
case .f64: throwReturn = "return 0"
208+
case .pointer: throwReturn = "return UnsafeMutableRawPointer(bitPattern: 0)!"
209+
}
210+
} else {
211+
throwReturn = "return"
212+
}
213+
220214
// Build signature using SwiftSignatureBuilder
221215
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
222216
abiParameters: abiParams,
@@ -225,7 +219,11 @@ public struct ClosureCodegen {
225219

226220
// Build body
227221
let body = CodeBlockItemListSyntax {
228-
"let box = Unmanaged<\(raw: boxClassName)>.fromOpaque(boxPtr).takeUnretainedValue()"
222+
"let box = Unmanaged<\(raw: boxType)>.fromOpaque(boxPtr).takeUnretainedValue()"
223+
"guard let closure = box.closure else {"
224+
" box._bridgeJSThrowReleasedClosure()"
225+
" \(raw: throwReturn)"
226+
"}"
229227
if signature.returnType == .void {
230228
closureCallExpr
231229
} else {

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ public class ExportSwift {
365365

366366
switch returnType {
367367
case .closure(let signature):
368-
append("return _BJS_Closure_\(raw: signature.mangleName).bridgeJSLower(ret)")
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") {
@@ -1628,6 +1628,35 @@ extension BridgeType {
16281628
}
16291629
}
16301630

1631+
/// Swift-side parameter type to use when sending a closure into JavaScript (ImportTS).
1632+
var swiftImportParameterType: String {
1633+
switch self {
1634+
case .closure(let signature):
1635+
let paramTypes = signature.parameters.map { $0.swiftType }.joined(separator: ", ")
1636+
let effectsStr = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
1637+
return "JSTypedClosure<(\(paramTypes))\(effectsStr) -> \(signature.returnType.swiftType)>"
1638+
case .nullable(let wrapped, let kind):
1639+
if wrapped.isClosureType {
1640+
let wrappedType = wrapped.swiftImportParameterType
1641+
switch kind {
1642+
case .null:
1643+
return "Optional<\(wrappedType)>"
1644+
case .undefined:
1645+
return "JSUndefinedOr<\(wrappedType)>"
1646+
}
1647+
} else {
1648+
return swiftType
1649+
}
1650+
default:
1651+
return swiftType
1652+
}
1653+
}
1654+
1655+
var isClosureType: Bool {
1656+
if case .closure = self { return true }
1657+
return false
1658+
}
1659+
16311660
struct LiftingIntrinsicInfo: Sendable {
16321661
let parameters: [(name: String, type: WasmCoreType)]
16331662

@@ -1725,7 +1754,7 @@ extension BridgeType {
17251754
case .namespaceEnum:
17261755
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
17271756
case .closure:
1728-
return .swiftHeapObject
1757+
return .jsObject
17291758
case .array:
17301759
return .array
17311760
}

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public struct ImportTS {
9999
switch param.type {
100100
case .closure(let signature):
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)