Skip to content

Commit ac88754

Browse files
BridgeJS: Add JSTypedClosure API
```swift @JSFunction static func jsApplyInt( _ value: Int, _ transform: JSTypedClosure<(Int) -> Int> ) throws(JSException) -> Int ```
1 parent 444393f commit ac88754

File tree

24 files changed

+3525
-1470
lines changed

24 files changed

+3525
-1470
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: 44 additions & 41 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

@@ -69,13 +68,15 @@ public struct ClosureCodegen {
6968
// Generate extern declaration using CallJSEmission
7069
let externDecl = builder.renderImportDecl()
7170

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-
}
71+
let makeClosureExternDecl: DeclSyntax = """
72+
#if arch(wasm32)
73+
@_extern(wasm, module: "bjs", name: "make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)")
74+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32
75+
#else
76+
fileprivate func make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName)(_ boxPtr: UnsafeMutableRawPointer) -> Int32 {
77+
fatalError("Only available on WebAssembly")
7878
}
79+
#endif
7980
"""
8081

8182
let helperEnumDecl = EnumDeclSyntax(
@@ -84,33 +85,6 @@ public struct ClosureCodegen {
8485
},
8586
name: .identifier(helperName),
8687
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-
11488
DeclSyntax(
11589
FunctionDeclSyntax(
11690
modifiers: DeclModifierListSyntax {
@@ -128,7 +102,7 @@ public struct ClosureCodegen {
128102
},
129103
returnClause: ReturnClauseSyntax(
130104
arrow: .arrowToken(),
131-
type: IdentifierTypeSyntax(name: .identifier(closureType))
105+
type: IdentifierTypeSyntax(name: .identifier(swiftClosureType))
132106
)
133107
),
134108
body: CodeBlockSyntax {
@@ -178,11 +152,32 @@ public struct ClosureCodegen {
178152
)
179153
}
180154
)
181-
return [externDecl, boxClassDecl, DeclSyntax(helperEnumDecl)]
155+
let typedClosureExtension: DeclSyntax = """
156+
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
157+
init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
158+
self.init(
159+
makeClosure: make_swift_closure_\(raw: signature.moduleName)_\(raw: signature.mangleName),
160+
body: body,
161+
fileID: fileID,
162+
line: line
163+
)
164+
}
165+
}
166+
"""
167+
168+
return [
169+
externDecl, makeClosureExternDecl, DeclSyntax(helperEnumDecl), typedClosureExtension,
170+
]
182171
}
183172

184173
func renderClosureInvokeHandler(_ signature: ClosureSignature) throws -> DeclSyntax {
185-
let boxClassName = "_BJS_ClosureBox_\(signature.mangleName)"
174+
let closureParams = signature.parameters.enumerated().map { _, type in
175+
"\(type.swiftType)"
176+
}.joined(separator: ", ")
177+
let swiftEffects = (signature.isAsync ? " async" : "") + (signature.isThrows ? " throws" : "")
178+
let swiftReturnType = signature.returnType.swiftType
179+
let swiftClosureType = "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
180+
let boxType = "_BridgeJSTypedClosureBox<\(swiftClosureType)>"
186181
let abiName = "invoke_swift_closure_\(signature.moduleName)_\(signature.mangleName)"
187182

188183
// Build ABI parameters directly with WasmCoreType (no string conversion needed)
@@ -205,7 +200,7 @@ public struct ClosureCodegen {
205200
liftedParams.append("\(paramType.swiftType).bridgeJSLiftParameter(\(argNames.joined(separator: ", ")))")
206201
}
207202

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

210205
// Determine return type
211206
let abiReturnWasmType: WasmCoreType?
@@ -217,6 +212,8 @@ public struct ClosureCodegen {
217212
abiReturnWasmType = nil
218213
}
219214

215+
let throwReturn = abiReturnWasmType?.swiftReturnPlaceholderStmt ?? "return"
216+
220217
// Build signature using SwiftSignatureBuilder
221218
let funcSignature = SwiftSignatureBuilder.buildABIFunctionSignature(
222219
abiParameters: abiParams,
@@ -225,7 +222,13 @@ public struct ClosureCodegen {
225222

226223
// Build body
227224
let body = CodeBlockItemListSyntax {
228-
"let box = Unmanaged<\(raw: boxClassName)>.fromOpaque(boxPtr).takeUnretainedValue()"
225+
"let box = Unmanaged<\(raw: boxType)>.fromOpaque(boxPtr).takeUnretainedValue()"
226+
"""
227+
guard let closure = box.closure else {
228+
box._bridgeJSThrowReleasedClosure()
229+
\(raw: throwReturn)
230+
}
231+
"""
229232
if signature.returnType == .void {
230233
closureCallExpr
231234
} else {
@@ -315,7 +318,7 @@ public struct ClosureCodegen {
315318
for setter in type.setters {
316319
collectClosureSignatures(from: setter.type, into: &closureSignatures)
317320
}
318-
for method in type.methods {
321+
for method in type.methods + type.staticMethods {
319322
collectClosureSignatures(from: method.parameters, into: &closureSignatures)
320323
collectClosureSignatures(from: method.returnType, into: &closureSignatures)
321324
}

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):

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ 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 argument = genericArgs.firstAsTypeSyntax,
108+
let signatureType = lookupType(for: argument, errors: &errors),
109+
case .closure(let signature) = signatureType
110+
{
111+
return .closure(signature)
112+
}
113+
103114
// (T1, T2, ...) -> R
104115
if let functionType = type.as(FunctionTypeSyntax.self) {
105116
var parameters: [BridgeType] = []
@@ -2537,3 +2548,18 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25372548
}
25382549
}
25392550
}
2551+
2552+
extension GenericArgumentListSyntax {
2553+
/// Compatibility helper for accessing the first argument as a TypeSyntax
2554+
///
2555+
/// Note: SwiftSyntax 601 and later support InlineArrayTypeSyntax and
2556+
/// ``GenericArgumentSyntax/argument`` is now a ``TypeSyntax`` or ``ExprSyntax``.
2557+
fileprivate var firstAsTypeSyntax: TypeSyntax? {
2558+
guard let first = self.first else { return nil }
2559+
#if canImport(SwiftSyntax601)
2560+
return first.argument.as(TypeSyntax.self)
2561+
#else
2562+
return TypeSyntax(first.argument)
2563+
#endif
2564+
}
2565+
}

0 commit comments

Comments
 (0)