Skip to content

Commit bec6d83

Browse files
BridgeJS: Add JSTypedClosure API
1 parent 56aabfa commit bec6d83

Some content is hidden

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

51 files changed

+3296
-319
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public struct ClosureCodegen {
4040

4141
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
4242
let swiftReturnType = signature.returnType.swiftType
43-
let closureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
43+
let swiftClosureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
44+
let typedClosureType = "JSTypedClosure<\(swiftClosureType)>"
4445

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

@@ -65,14 +66,42 @@ 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 =
73+
closureParamsList.isEmpty ? "" : " \(closureParamsList) in\n"
74+
let wrapperCallArguments = closureParamsList.isEmpty ? "" : "\(closureParamsList)"
6875

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

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")
86+
}
87+
#endif
88+
"""
89+
90+
let releaseClosureExternDecl: DeclSyntax = """
91+
#if arch(wasm32)
92+
@_extern(wasm, module: "bjs", name: "release_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)")
93+
fileprivate func release_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer, _ funcRef: Int32)
94+
#else
95+
fileprivate func release_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer, _ funcRef: Int32) {
96+
fatalError("Only available on WebAssembly")
97+
}
98+
#endif
99+
"""
100+
72101
let boxClassDecl: DeclSyntax = """
73102
private final class \(raw: boxClassName): _BridgedSwiftClosureBox {
74-
let closure: \(raw: closureType)
75-
init(_ closure: @escaping \(raw: closureType)) {
103+
let closure: \(raw: swiftClosureType)
104+
init(_ closure: @escaping \(raw: swiftClosureType)) {
76105
self.closure = closure
77106
}
78107
}
@@ -96,17 +125,16 @@ public struct ClosureCodegen {
96125
firstName: .wildcardToken(),
97126
secondName: .identifier("closure"),
98127
colon: .colonToken(),
99-
type: TypeSyntax("@escaping \(raw: closureType)")
128+
type: TypeSyntax("\(raw: typedClosureType)")
100129
)
101130
},
102131
returnClause: ReturnClauseSyntax(
103132
arrow: .arrowToken(),
104-
type: IdentifierTypeSyntax(name: .identifier("UnsafeMutableRawPointer"))
133+
type: IdentifierTypeSyntax(name: .identifier("Int32"))
105134
)
106135
),
107136
body: CodeBlockSyntax {
108-
"let box = \(raw: boxClassName)(closure)"
109-
"return Unmanaged.passRetained(box).toOpaque()"
137+
"return closure._bridgeJSLowerParameter()"
110138
}
111139
)
112140
)
@@ -128,57 +156,59 @@ public struct ClosureCodegen {
128156
},
129157
returnClause: ReturnClauseSyntax(
130158
arrow: .arrowToken(),
131-
type: IdentifierTypeSyntax(name: .identifier(closureType))
159+
type: IdentifierTypeSyntax(name: .identifier(swiftClosureType))
132160
)
133161
),
134162
body: CodeBlockSyntax {
135163
"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-
)
164+
"""
165+
let callable: \(raw: swiftClosureType) = { [callback] \(raw: closureParamsList) in
166+
\(raw: wasmBody)
167+
}
168+
"""
169+
"return callable"
176170
}
177171
)
178172
)
179173
}
180174
)
181-
return [externDecl, boxClassDecl, DeclSyntax(helperEnumDecl)]
175+
let typedClosureExtension: DeclSyntax = """
176+
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
177+
convenience init(_ body: @escaping \(raw: swiftClosureType)) {
178+
let box = \(raw: boxClassName)(body)
179+
let pointer = Unmanaged.passRetained(box).toOpaque()
180+
let funcRefValue = make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(pointer)
181+
let funcRef = UInt32(bitPattern: funcRefValue)
182+
self.init(
183+
_boxPointer: pointer,
184+
callable: body,
185+
funcRef: funcRef,
186+
releaseHook: { pointer, funcRef in
187+
release_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(
188+
pointer,
189+
Int32(bitPattern: funcRef)
190+
)
191+
}
192+
)
193+
}
194+
195+
static func oneshot(_ body: @escaping \(raw: swiftClosureType)) -> JSTypedClosure<Signature> {
196+
var typedClosure: JSTypedClosure<Signature>?
197+
let wrapper: Signature = {
198+
\(raw: wrapperParamClause) defer { typedClosure?.release() }
199+
return body(\(raw: wrapperCallArguments))
200+
}
201+
let typed = JSTypedClosure(wrapper)
202+
typedClosure = typed
203+
return typed
204+
}
205+
}
206+
"""
207+
208+
return [
209+
externDecl, makeClosureExternDecl, releaseClosureExternDecl, boxClassDecl, DeclSyntax(helperEnumDecl),
210+
typedClosureExtension,
211+
]
182212
}
183213

184214
func renderClosureInvokeHandler(_ signature: ClosureSignature) throws -> DeclSyntax {

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 _BJS_Closure_\(raw: signature.mangleName).bridgeJSLower(JSTypedClosure(ret))")
369369
case .array, .nullable(.array, _):
370370
let stackCodegen = StackCodegen()
371371
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {
@@ -1625,6 +1625,35 @@ extension BridgeType {
16251625
}
16261626
}
16271627

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

@@ -1718,7 +1747,7 @@ extension BridgeType {
17181747
case .namespaceEnum:
17191748
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
17201749
case .closure:
1721-
return .swiftHeapObject
1750+
return .jsObject
17221751
case .array:
17231752
return .array
17241753
}

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -694,12 +694,12 @@ struct SwiftSignatureBuilder {
694694
}
695695

696696
/// Builds a parameter type syntax from a BridgeType.
697-
///
698-
/// Swift closure parameters must be `@escaping` because they are boxed and can be invoked from JavaScript.
699697
static func buildParameterTypeSyntax(from type: BridgeType) -> TypeSyntax {
700698
switch type {
701699
case .closure:
702-
return TypeSyntax("@escaping \(raw: type.swiftType)")
700+
return TypeSyntax("\(raw: type.swiftImportParameterType)")
701+
case .nullable(let wrapped, _) where wrapped.isClosureType:
702+
return TypeSyntax("\(raw: type.swiftImportParameterType)")
703703
default:
704704
return buildTypeSyntax(from: type)
705705
}
@@ -896,8 +896,8 @@ extension BridgeType {
896896
case .jsObject: return .jsObject
897897
case .void: return .void
898898
case .closure:
899-
// Swift closure is boxed and passed to JS as a pointer.
900-
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
899+
// Swift closure is passed to JS as a JS function reference.
900+
return LoweringParameterInfo(loweredParameters: [("funcRef", .i32)])
901901
case .unsafePointer:
902902
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
903903
case .swiftHeapObject(let className):

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ public final class SwiftToSkeleton {
100100
return lookupType(for: attributedType.baseType, errors: &errors)
101101
}
102102

103+
if let identifierType = type.as(IdentifierTypeSyntax.self),
104+
identifierType.name.text == "JSTypedClosure",
105+
let genericArgs = identifierType.genericArgumentClause?.arguments,
106+
genericArgs.count == 1,
107+
let signatureType = lookupType(for: genericArgs.first!.argument, errors: &errors),
108+
case .closure(let signature) = signatureType
109+
{
110+
return .closure(signature)
111+
}
112+
103113
// (T1, T2, ...) -> R
104114
if let functionType = type.as(FunctionTypeSyntax.self) {
105115
var parameters: [BridgeType] = []

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -258,52 +258,37 @@ public struct BridgeJSLink {
258258
"let \(JSGlueVariableScope.reservedTmpStructCleanups) = [];",
259259
"const \(JSGlueVariableScope.reservedEnumHelpers) = {};",
260260
"const \(JSGlueVariableScope.reservedStructHelpers) = {};",
261+
"const \(JSGlueVariableScope.reservedSwiftClosureEntries) = new Map();",
262+
"const \(JSGlueVariableScope.reservedSwiftClosureRegistry) = (typeof FinalizationRegistry === \"undefined\") ? null : new FinalizationRegistry(({ pointer, funcRef }) => {",
263+
" \(JSGlueVariableScope.reservedSwiftClosureEntries).delete(funcRef);",
264+
" \(JSGlueVariableScope.reservedSwift).memory.release(funcRef);",
265+
" \(JSGlueVariableScope.reservedInstance)?.exports?.bjs_release_swift_closure(pointer);",
266+
"});",
267+
"const \(JSGlueVariableScope.reservedRegisterSwiftClosure) = (func, pointer) => {",
268+
" const funcRef = \(JSGlueVariableScope.reservedSwift).memory.retain(func);",
269+
" const token = \(JSGlueVariableScope.reservedSwiftClosureRegistry) ? {} : null;",
270+
" if (\(JSGlueVariableScope.reservedSwiftClosureRegistry)) {",
271+
" \(JSGlueVariableScope.reservedSwiftClosureRegistry).register(func, { pointer, funcRef }, token);",
272+
" }",
273+
" \(JSGlueVariableScope.reservedSwiftClosureEntries).set(funcRef, { pointer, token });",
274+
" return { func, funcRef };",
275+
"};",
276+
"const \(JSGlueVariableScope.reservedReleaseSwiftClosure) = (funcRef, pointer) => {",
277+
" const entry = \(JSGlueVariableScope.reservedSwiftClosureEntries).get(funcRef);",
278+
" if (!entry) { return; }",
279+
" \(JSGlueVariableScope.reservedSwiftClosureEntries).delete(funcRef);",
280+
" if (entry.token && \(JSGlueVariableScope.reservedSwiftClosureRegistry)) {",
281+
" \(JSGlueVariableScope.reservedSwiftClosureRegistry).unregister(entry.token);",
282+
" }",
283+
" \(JSGlueVariableScope.reservedSwift).memory.release(funcRef);",
284+
" (\(JSGlueVariableScope.reservedInstance)?.exports?.bjs_release_swift_closure(pointer ?? entry.pointer));",
285+
"};",
261286
"",
262287
"let _exports = null;",
263288
"let bjs = null;",
264289
]
265290
}
266291

267-
/// Checks if a skeleton contains any closure types
268-
private func hasClosureTypes(in skeleton: ExportedSkeleton) -> Bool {
269-
for function in skeleton.functions {
270-
if containsClosureType(in: function.parameters) || containsClosureType(in: function.returnType) {
271-
return true
272-
}
273-
}
274-
for klass in skeleton.classes {
275-
if let constructor = klass.constructor, containsClosureType(in: constructor.parameters) {
276-
return true
277-
}
278-
for method in klass.methods {
279-
if containsClosureType(in: method.parameters) || containsClosureType(in: method.returnType) {
280-
return true
281-
}
282-
}
283-
for property in klass.properties {
284-
if containsClosureType(in: property.type) {
285-
return true
286-
}
287-
}
288-
}
289-
return false
290-
}
291-
292-
private func containsClosureType(in parameters: [Parameter]) -> Bool {
293-
parameters.contains { containsClosureType(in: $0.type) }
294-
}
295-
296-
private func containsClosureType(in type: BridgeType) -> Bool {
297-
switch type {
298-
case .closure:
299-
return true
300-
case .nullable(let wrapped, _):
301-
return containsClosureType(in: wrapped)
302-
default:
303-
return false
304-
}
305-
}
306-
307292
private func generateAddImports(needsImportsObject: Bool) -> CodeFragmentPrinter {
308293
let printer = CodeFragmentPrinter()
309294
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
@@ -674,6 +659,23 @@ public struct BridgeJSLink {
674659
functionName: lowerFuncName
675660
)
676661
)
662+
663+
let makeFuncName = "make_swift_closure_\(moduleName)_\(signature.mangleName)"
664+
printer.write("bjs[\"\(makeFuncName)\"] = function(boxPtr) {")
665+
printer.indent {
666+
printer.write(
667+
"const { funcRef } = \(JSGlueVariableScope.reservedRegisterSwiftClosure)(bjs[\"\(lowerFuncName)\"](boxPtr), boxPtr);"
668+
)
669+
printer.write("return funcRef;")
670+
}
671+
printer.write("}")
672+
673+
let releaseFuncName = "release_swift_closure_\(moduleName)_\(signature.mangleName)"
674+
printer.write("bjs[\"\(releaseFuncName)\"] = function(boxPtr, funcRef) {")
675+
printer.indent {
676+
printer.write("\(JSGlueVariableScope.reservedReleaseSwiftClosure)(funcRef, boxPtr);")
677+
}
678+
printer.write("}")
677679
}
678680
}
679681
}

0 commit comments

Comments
 (0)