Skip to content

Commit 16d3b1e

Browse files
committed
BridgeJS: Optimize numeric array transfer with bulk TypedArray copy
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: Array.bridgeJSTypedArrayPush() passes (ptr, count, kind) to swift_js_push_typed_array which copies into a JS TypedArray, then a for-loop converts to number[] for the caller. JS->Swift: Array.bridgeJSTypedArrayLiftParameter(id, count) receives a retained TypedArray ID, allocates via unsafeUninitializedCapacity, and calls swift_js_init_typed_array_memory to bulk copy. Non-numeric arrays (String, structs, classes, enums) are unaffected.
1 parent 5e96639 commit 16d3b1e

62 files changed

Lines changed: 1711 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(_: [Int].bridgeJSTypedArrayLiftParameter(valuesSourceId, valuesCount))
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+
ret.bridgeJSTypedArrayPush()
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(_: [Int].bridgeJSTypedArrayLiftParameter(valuesSourceId, valuesCount))
1856+
ret.bridgeJSTypedArrayPush()
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+
ret.bridgeJSTypedArrayPush()
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(_: [Double].bridgeJSTypedArrayLiftParameter(valuesSourceId, valuesCount))
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+
ret.bridgeJSTypedArrayPush()
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(_: [Double].bridgeJSTypedArrayLiftParameter(valuesSourceId, valuesCount))
1899+
ret.bridgeJSTypedArrayPush()
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+
"[\(elementType.swiftType)].bridgeJSTypedArrayLiftParameter(\(sourceIdName), \(countName))"
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+
"[\(raw: elementSwiftType)].bridgeJSTypedArrayLiftParameter(\(raw: param.name)SourceId, \(raw: param.name)Count)"
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("ret.bridgeJSTypedArrayPush()")
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("\(param.name).bridgeJSTypedArrayPush()", 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 = "[\(swiftElementType)].bridgeJSTypedArrayLiftParameter(_sourceId, _count)"
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: 78 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,72 @@ 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 srcVar = scope.variable("taSrc")
1897+
let resultVar = scope.variable("arrayResult")
1898+
let iVar = scope.variable("i")
1899+
printer.write("const \(srcVar) = \(JSGlueVariableScope.reservedTaStack).pop();")
1900+
printer.write("const \(resultVar) = new Array(\(srcVar).length);")
1901+
printer.write(
1902+
"for (let \(iVar) = 0; \(iVar) < \(srcVar).length; \(iVar)++) \(resultVar)[\(iVar)] = \(srcVar)[\(iVar)];"
1903+
)
1904+
return [resultVar]
1905+
}
1906+
)
1907+
}
1908+
18311909
/// Lowers an array from JS to Swift by iterating elements and pushing to stacks
18321910
static func arrayLower(elementType: BridgeType) throws -> IntrinsicJSFragment {
18331911
return IntrinsicJSFragment(

0 commit comments

Comments
 (0)