Skip to content

Commit c3c03ec

Browse files
committed
BridgeJS: Optimize numeric array bridging with bulk TypedArray transfer
Use bulk TypedArray memory copy instead of element-by-element stack serialization for numeric arrays ([Int], [UInt8], [Float], [Double], etc.). TypeScript types remain number[]/bigint[] — no API change. Swift->JS: withUnsafeBufferPointer passes (ptr, count, kind) to swift_js_push_typed_array which copies bytes into a JS TypedArray, then Array.from() converts to number[] for the caller. JS->Swift: retains the array as a TypedArray in the JS heap, passes (id, count) as WASM params, Swift allocates via Array(unsafeUninitializedCapacity:) and calls back to JS to bulk copy. Non-numeric arrays (String, structs, classes, enums) are unaffected and continue using the existing element-by-element stack protocol.
1 parent 5e96639 commit c3c03ec

62 files changed

Lines changed: 1694 additions & 203 deletions

Some content is hidden

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

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,9 +1829,9 @@ public func _bjs_ArrayRoundtrip_init() -> UnsafeMutableRawPointer {
18291829

18301830
@_expose(wasm, "bjs_ArrayRoundtrip_takeIntArray")
18311831
@_cdecl("bjs_ArrayRoundtrip_takeIntArray")
1832-
public func _bjs_ArrayRoundtrip_takeIntArray(_ _self: UnsafeMutableRawPointer) -> Void {
1832+
public func _bjs_ArrayRoundtrip_takeIntArray(_ _self: UnsafeMutableRawPointer, _ valuesSourceId: Int32, _ valuesCount: Int32) -> Void {
18331833
#if arch(wasm32)
1834-
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeIntArray(_: [Int].bridgeJSStackPop())
1834+
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeIntArray(_: _bridgeJS_typedArrayLiftParameter(valuesSourceId, valuesCount) as [Int])
18351835
#else
18361836
fatalError("Only available on WebAssembly")
18371837
#endif
@@ -1842,18 +1842,18 @@ public func _bjs_ArrayRoundtrip_takeIntArray(_ _self: UnsafeMutableRawPointer) -
18421842
public func _bjs_ArrayRoundtrip_makeIntArray(_ _self: UnsafeMutableRawPointer) -> Void {
18431843
#if arch(wasm32)
18441844
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeIntArray()
1845-
ret.bridgeJSStackPush()
1845+
_bridgeJS_typedArrayPush(ret)
18461846
#else
18471847
fatalError("Only available on WebAssembly")
18481848
#endif
18491849
}
18501850

18511851
@_expose(wasm, "bjs_ArrayRoundtrip_roundtripIntArray")
18521852
@_cdecl("bjs_ArrayRoundtrip_roundtripIntArray")
1853-
public func _bjs_ArrayRoundtrip_roundtripIntArray(_ _self: UnsafeMutableRawPointer) -> Void {
1853+
public func _bjs_ArrayRoundtrip_roundtripIntArray(_ _self: UnsafeMutableRawPointer, _ valuesSourceId: Int32, _ valuesCount: Int32) -> Void {
18541854
#if arch(wasm32)
1855-
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripIntArray(_: [Int].bridgeJSStackPop())
1856-
ret.bridgeJSStackPush()
1855+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripIntArray(_: _bridgeJS_typedArrayLiftParameter(valuesSourceId, valuesCount) as [Int])
1856+
_bridgeJS_typedArrayPush(ret)
18571857
#else
18581858
fatalError("Only available on WebAssembly")
18591859
#endif
@@ -1864,17 +1864,17 @@ public func _bjs_ArrayRoundtrip_roundtripIntArray(_ _self: UnsafeMutableRawPoint
18641864
public func _bjs_ArrayRoundtrip_makeIntArrayLarge(_ _self: UnsafeMutableRawPointer) -> Void {
18651865
#if arch(wasm32)
18661866
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeIntArrayLarge()
1867-
ret.bridgeJSStackPush()
1867+
_bridgeJS_typedArrayPush(ret)
18681868
#else
18691869
fatalError("Only available on WebAssembly")
18701870
#endif
18711871
}
18721872

18731873
@_expose(wasm, "bjs_ArrayRoundtrip_takeDoubleArray")
18741874
@_cdecl("bjs_ArrayRoundtrip_takeDoubleArray")
1875-
public func _bjs_ArrayRoundtrip_takeDoubleArray(_ _self: UnsafeMutableRawPointer) -> Void {
1875+
public func _bjs_ArrayRoundtrip_takeDoubleArray(_ _self: UnsafeMutableRawPointer, _ valuesSourceId: Int32, _ valuesCount: Int32) -> Void {
18761876
#if arch(wasm32)
1877-
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeDoubleArray(_: [Double].bridgeJSStackPop())
1877+
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeDoubleArray(_: _bridgeJS_typedArrayLiftParameter(valuesSourceId, valuesCount) as [Double])
18781878
#else
18791879
fatalError("Only available on WebAssembly")
18801880
#endif
@@ -1885,18 +1885,18 @@ public func _bjs_ArrayRoundtrip_takeDoubleArray(_ _self: UnsafeMutableRawPointer
18851885
public func _bjs_ArrayRoundtrip_makeDoubleArray(_ _self: UnsafeMutableRawPointer) -> Void {
18861886
#if arch(wasm32)
18871887
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeDoubleArray()
1888-
ret.bridgeJSStackPush()
1888+
_bridgeJS_typedArrayPush(ret)
18891889
#else
18901890
fatalError("Only available on WebAssembly")
18911891
#endif
18921892
}
18931893

18941894
@_expose(wasm, "bjs_ArrayRoundtrip_roundtripDoubleArray")
18951895
@_cdecl("bjs_ArrayRoundtrip_roundtripDoubleArray")
1896-
public func _bjs_ArrayRoundtrip_roundtripDoubleArray(_ _self: UnsafeMutableRawPointer) -> Void {
1896+
public func _bjs_ArrayRoundtrip_roundtripDoubleArray(_ _self: UnsafeMutableRawPointer, _ valuesSourceId: Int32, _ valuesCount: Int32) -> Void {
18971897
#if arch(wasm32)
1898-
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripDoubleArray(_: [Double].bridgeJSStackPop())
1899-
ret.bridgeJSStackPush()
1898+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripDoubleArray(_: _bridgeJS_typedArrayLiftParameter(valuesSourceId, valuesCount) as [Double])
1899+
_bridgeJS_typedArrayPush(ret)
19001900
#else
19011901
fatalError("Only available on WebAssembly")
19021902
#endif

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,19 @@ public struct ClosureCodegen {
132132

133133
for (index, paramType) in signature.parameters.enumerated() {
134134
let paramName = "param\(index)"
135+
136+
// Numeric arrays use bulk TypedArray transfer with (sourceId, count) WASM params
137+
if case .array(let elementType) = paramType, elementType.isNumericScalar {
138+
let sourceIdName = "\(paramName)SourceId"
139+
let countName = "\(paramName)Count"
140+
abiParams.append((sourceIdName, .i32))
141+
abiParams.append((countName, .i32))
142+
liftedParams.append(
143+
"_bridgeJS_typedArrayLiftParameter(\(sourceIdName), \(countName)) as [\(elementType.swiftType)]"
144+
)
145+
continue
146+
}
147+
135148
let liftInfo = try paramType.liftParameterInfo()
136149

137150
for (argName, wasmType) in liftInfo.parameters {

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ public class ExportSwift {
137137
case .swiftStruct(let structName):
138138
typeNameForIntrinsic = structName
139139
liftingExpr = ExprSyntax("\(raw: structName).bridgeJSLiftParameter()")
140+
case .array(let elementType) where elementType.isNumericScalar:
141+
// Numeric arrays use bulk TypedArray transfer with (sourceId, count) WASM params
142+
let elementSwiftType = elementType.swiftType
143+
typeNameForIntrinsic = param.type.swiftType
144+
liftingExpr = ExprSyntax(
145+
"_bridgeJS_typedArrayLiftParameter(\(raw: param.name)SourceId, \(raw: param.name)Count) as [\(raw: elementSwiftType)]"
146+
)
147+
// Override the ABI signatures: two i32 params instead of stack-based
148+
liftedParameterExprs.append(liftingExpr)
149+
abiParameterSignatures.append(("\(param.name)SourceId", .i32))
150+
abiParameterSignatures.append(("\(param.name)Count", .i32))
151+
return
140152
case .array:
141153
typeNameForIntrinsic = param.type.swiftType
142154
liftingExpr = StackCodegen().liftExpression(for: param.type)
@@ -301,6 +313,8 @@ public class ExportSwift {
301313
switch returnType {
302314
case .closure(_, useJSTypedClosure: false):
303315
append("return JSTypedClosure(ret).bridgeJSLowerReturn()")
316+
case .array(let elementType) where elementType.isNumericScalar:
317+
append("_bridgeJS_typedArrayPush(ret)")
304318
case .array, .nullable(.array, _):
305319
let stackCodegen = StackCodegen()
306320
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ public struct ImportTS {
152152
// The just created JSObject is not owned by the caller unlike those passed in parameters,
153153
// so we need to extend its lifetime during the call to ensure the JSObject.id is valid.
154154
valuesToExtendLifetimeDuringCall.append(param.name)
155+
case .array(let elementType) where elementType.isNumericScalar:
156+
// Numeric arrays use bulk TypedArray transfer instead of element-by-element
157+
stackLoweringStmts.insert("_bridgeJS_typedArrayPush(\(param.name))", at: 0)
158+
return
155159
default:
156160
break
157161
}
@@ -302,6 +306,12 @@ public struct ImportTS {
302306
switch returnType {
303307
case .closure(let signature, _):
304308
liftExpr = "_BJS_Closure_\(signature.mangleName).bridgeJSLift(ret)"
309+
case .array(let elementType) where elementType.isNumericScalar:
310+
// Numeric arrays: JS pushed (sourceId, count) onto i32 stack
311+
let swiftElementType = elementType.swiftType
312+
body.write("let _count = _swift_js_pop_i32()")
313+
body.write("let _sourceId = _swift_js_pop_i32()")
314+
liftExpr = "_bridgeJS_typedArrayLiftParameter(_sourceId, _count) as [\(swiftElementType)]"
305315
default:
306316
if liftingInfo.valueToLift != nil {
307317
liftExpr = "\(returnType.swiftType).bridgeJSLiftReturn(ret)"

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ public struct BridgeJSLink {
345345
"let \(JSGlueVariableScope.reservedF32Stack) = [];",
346346
"let \(JSGlueVariableScope.reservedF64Stack) = [];",
347347
"let \(JSGlueVariableScope.reservedPointerStack) = [];",
348+
"let \(JSGlueVariableScope.reservedTaStack) = [];",
349+
"const \(JSGlueVariableScope.reservedTypedArrayConstructors) = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, BigInt64Array, BigUint64Array, Float32Array, Float64Array];",
348350
"const \(JSGlueVariableScope.reservedEnumHelpers) = {};",
349351
"const \(JSGlueVariableScope.reservedStructHelpers) = {};",
350352
"",
@@ -487,6 +489,66 @@ public struct BridgeJSLink {
487489
printer.write("return \(JSGlueVariableScope.reservedI64Stack).pop();")
488490
}
489491
printer.write("}")
492+
printer.write("bjs[\"swift_js_push_typed_array\"] = function(ptr, count, kind) {")
493+
printer.indent {
494+
printer.write(
495+
"const Constructor = \(JSGlueVariableScope.reservedTypedArrayConstructors)[kind];"
496+
)
497+
printer.write(
498+
"const elemSize = Constructor.BYTES_PER_ELEMENT;"
499+
)
500+
printer.write(
501+
"const totalBytes = count * elemSize;"
502+
)
503+
printer.write(
504+
"const copy = new Uint8Array(totalBytes);"
505+
)
506+
printer.write(
507+
"copy.set(new Uint8Array(\(JSGlueVariableScope.reservedMemory).buffer, ptr, totalBytes));"
508+
)
509+
printer.write(
510+
"\(JSGlueVariableScope.reservedTaStack).push(new Constructor(copy.buffer));"
511+
)
512+
}
513+
printer.write("}")
514+
printer.write("bjs[\"swift_js_init_typed_array_memory\"] = function(sourceId, destPtr, count, kind) {")
515+
printer.indent {
516+
printer.write(
517+
"const source = \(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).getObject(sourceId);"
518+
)
519+
printer.write(
520+
"\(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).release(sourceId);"
521+
)
522+
printer.write(
523+
"const Constructor = \(JSGlueVariableScope.reservedTypedArrayConstructors)[kind];"
524+
)
525+
printer.write(
526+
"const elemSize = Constructor.BYTES_PER_ELEMENT;"
527+
)
528+
printer.write(
529+
"if (destPtr % elemSize === 0) {"
530+
)
531+
printer.indent {
532+
printer.write(
533+
"const dest = new Constructor(\(JSGlueVariableScope.reservedMemory).buffer, destPtr, count);"
534+
)
535+
printer.write("dest.set(source);")
536+
}
537+
printer.write(
538+
"} else {"
539+
)
540+
printer.indent {
541+
printer.write(
542+
"const dest = new Uint8Array(\(JSGlueVariableScope.reservedMemory).buffer, destPtr, count * elemSize);"
543+
)
544+
printer.write(
545+
"const srcBytes = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);"
546+
)
547+
printer.write("dest.set(srcBytes);")
548+
}
549+
printer.write("}")
550+
}
551+
printer.write("}")
490552
if !allStructs.isEmpty {
491553
for structDef in allStructs {
492554
printer.write("bjs[\"swift_js_struct_lower_\(structDef.abiName)\"] = function(objectId) {")

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ final class JSGlueVariableScope {
3434
static let reservedStructHelpers = "structHelpers"
3535
static let reservedSwiftClosureRegistry = "swiftClosureRegistry"
3636
static let reservedMakeSwiftClosure = "makeClosure"
37+
static let reservedTaStack = "taStack"
38+
static let reservedTypedArrayConstructors = "typedArrayConstructors"
3739

3840
private let intrinsicRegistry: JSIntrinsicRegistry
3941

@@ -63,6 +65,8 @@ final class JSGlueVariableScope {
6365
reservedStructHelpers,
6466
reservedSwiftClosureRegistry,
6567
reservedMakeSwiftClosure,
68+
reservedTaStack,
69+
reservedTypedArrayConstructors,
6670
]
6771

6872
init(intrinsicRegistry: JSIntrinsicRegistry) {
@@ -1317,6 +1321,8 @@ struct IntrinsicJSFragment: Sendable {
13171321
)
13181322
case .namespaceEnum(let string):
13191323
throw BridgeJSLinkError(message: "Namespace enums are not supported to be passed as parameters: \(string)")
1324+
case .array(let elementType) where elementType.isNumericScalar:
1325+
return numericArrayLower(elementType: elementType)
13201326
case .array(let elementType):
13211327
return try arrayLower(elementType: elementType)
13221328
case .dictionary(let valueType):
@@ -1374,6 +1380,8 @@ struct IntrinsicJSFragment: Sendable {
13741380
throw BridgeJSLinkError(
13751381
message: "Namespace enums are not supported to be returned from functions: \(string)"
13761382
)
1383+
case .array(let elementType) where elementType.isNumericScalar:
1384+
return numericArrayLift(elementType: elementType)
13771385
case .array(let elementType):
13781386
return try arrayLift(elementType: elementType)
13791387
case .dictionary(let valueType):
@@ -1472,6 +1480,8 @@ struct IntrinsicJSFragment: Sendable {
14721480
message:
14731481
"Namespace enums are not supported to be passed as parameters to imported JS functions: \(string)"
14741482
)
1483+
case .array(let elementType) where elementType.isNumericScalar:
1484+
return numericArrayLift(elementType: elementType)
14751485
case .array(let elementType):
14761486
return try arrayLift(elementType: elementType)
14771487
case .dictionary(let valueType):
@@ -1534,6 +1544,8 @@ struct IntrinsicJSFragment: Sendable {
15341544
throw BridgeJSLinkError(
15351545
message: "Namespace enums are not supported to be returned from imported JS functions: \(string)"
15361546
)
1547+
case .array(let elementType) where elementType.isNumericScalar:
1548+
return numericArrayLowerReturn(elementType: elementType)
15371549
case .array(let elementType):
15381550
return try arrayLower(elementType: elementType)
15391551
case .dictionary(let valueType):
@@ -1828,6 +1840,66 @@ struct IntrinsicJSFragment: Sendable {
18281840

18291841
// MARK: - Array Helpers
18301842

1843+
/// Lowers a numeric array from JS to Swift via bulk TypedArray transfer.
1844+
/// Converts the JS Array to a TypedArray, retains it, and passes (sourceId, count) as WASM params.
1845+
static func numericArrayLower(elementType: BridgeType) -> IntrinsicJSFragment {
1846+
let kind = elementType.typedArrayKind!
1847+
return IntrinsicJSFragment(
1848+
parameters: ["arr"],
1849+
printCode: { arguments, context in
1850+
let (scope, printer) = (context.scope, context.printer)
1851+
let arr = arguments[0]
1852+
let typedArrVar = scope.variable("typedArr")
1853+
let idVar = scope.variable("typedArrId")
1854+
printer.write(
1855+
"const \(typedArrVar) = new \(JSGlueVariableScope.reservedTypedArrayConstructors)[\(kind)](\(arr));"
1856+
)
1857+
printer.write(
1858+
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(typedArrVar));"
1859+
)
1860+
return [idVar, "\(typedArrVar).length"]
1861+
}
1862+
)
1863+
}
1864+
1865+
/// Lowers a numeric array from JS to Swift for return values (stack-based).
1866+
/// Pushes (sourceId, count) onto the i32 stack.
1867+
static func numericArrayLowerReturn(elementType: BridgeType) -> IntrinsicJSFragment {
1868+
let kind = elementType.typedArrayKind!
1869+
return IntrinsicJSFragment(
1870+
parameters: ["arr"],
1871+
printCode: { arguments, context in
1872+
let (scope, printer) = (context.scope, context.printer)
1873+
let arr = arguments[0]
1874+
let typedArrVar = scope.variable("typedArr")
1875+
let idVar = scope.variable("typedArrId")
1876+
printer.write(
1877+
"const \(typedArrVar) = new \(JSGlueVariableScope.reservedTypedArrayConstructors)[\(kind)](\(arr));"
1878+
)
1879+
printer.write(
1880+
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(typedArrVar));"
1881+
)
1882+
scope.emitPushI32Parameter(idVar, printer: printer)
1883+
scope.emitPushI32Parameter("\(typedArrVar).length", printer: printer)
1884+
return []
1885+
}
1886+
)
1887+
}
1888+
1889+
/// Lifts a numeric array from Swift to JS via bulk TypedArray transfer.
1890+
/// Swift side pushed the typed array view onto taStack; JS pops and converts to plain Array.
1891+
static func numericArrayLift(elementType: BridgeType) -> IntrinsicJSFragment {
1892+
return IntrinsicJSFragment(
1893+
parameters: [],
1894+
printCode: { arguments, context in
1895+
let (scope, printer) = (context.scope, context.printer)
1896+
let resultVar = scope.variable("arrayResult")
1897+
printer.write("const \(resultVar) = Array.from(\(JSGlueVariableScope.reservedTaStack).pop());")
1898+
return [resultVar]
1899+
}
1900+
)
1901+
}
1902+
18311903
/// Lowers an array from JS to Swift by iterating elements and pushing to stacks
18321904
static func arrayLower(elementType: BridgeType) throws -> IntrinsicJSFragment {
18331905
return IntrinsicJSFragment(

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,42 @@ public enum BridgeType: Codable, Equatable, Hashable, Sendable {
275275
indirect case closure(ClosureSignature, useJSTypedClosure: Bool)
276276
}
277277

278+
extension BridgeType {
279+
/// Returns true if this type is a numeric scalar eligible for bulk TypedArray transfer.
280+
/// Used by codegen to route `[NumericType]` through the optimized typed-array path
281+
/// instead of element-by-element stack operations.
282+
public var isNumericScalar: Bool {
283+
switch self {
284+
case .integer, .float, .double: return true
285+
default: return false
286+
}
287+
}
288+
289+
/// Returns the JavaScript TypedArray constructor kind index for numeric scalars.
290+
/// The index matches the `typedArrayConstructors` table in JS glue code.
291+
/// Returns nil for non-numeric types.
292+
public var typedArrayKind: Int? {
293+
switch self {
294+
case .integer(let t):
295+
switch (t.width, t.isSigned) {
296+
case (.w8, true): return 0 // Int8Array
297+
case (.w8, false): return 1 // Uint8Array
298+
case (.w16, true): return 2 // Int16Array
299+
case (.w16, false): return 3 // Uint16Array
300+
case (.w32, true): return 4 // Int32Array
301+
case (.w32, false): return 5 // Uint32Array
302+
case (.w64, true): return 6 // BigInt64Array
303+
case (.w64, false): return 7 // BigUint64Array
304+
case (.word, true): return 4 // Int (i32 on wasm32) → Int32Array
305+
case (.word, false): return 5 // UInt (u32 on wasm32) → Uint32Array
306+
}
307+
case .float: return 8 // Float32Array
308+
case .double: return 9 // Float64Array
309+
default: return nil
310+
}
311+
}
312+
}
313+
278314
public enum WasmCoreType: String, Codable, Sendable {
279315
case i32, i64, f32, f64, pointer
280316
}

0 commit comments

Comments
 (0)