Skip to content

Commit 81d24bd

Browse files
committed
BridgeJS: Add bulk copy for JS-to-Swift typed array parameters
Use the string-lowering retain-allocate-callback pattern for typed array parameters: JS retains the TypedArray, passes (id, count) as WASM params, Swift allocates via Array(unsafeUninitializedCapacity:), then calls swift_js_init_typed_array_memory to bulk copy the data. This completes bidirectional typed array optimization. Both Swift->JS and JS->Swift now use bulk memory copy instead of element-by-element stack serialization for top-level typed array parameters and returns. Nested contexts (struct fields, dictionary values) continue using element-by-element as a safe fallback.
1 parent d4fbbbf commit 81d24bd

67 files changed

Lines changed: 787 additions & 124 deletions

File tree

Some content is hidden

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

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ public class ExportSwift {
137137
case .swiftStruct(let structName):
138138
typeNameForIntrinsic = structName
139139
liftingExpr = ExprSyntax("\(raw: structName).bridgeJSLiftParameter()")
140-
case .array, .typedArray:
140+
case .array:
141141
typeNameForIntrinsic = param.type.swiftType
142142
liftingExpr = StackCodegen().liftExpression(for: param.type)
143+
case .typedArray:
144+
typeNameForIntrinsic = param.type.swiftType
145+
liftingExpr = ExprSyntax(
146+
"_bridgeJS_typedArrayLiftParameter(\(raw: argumentsToLift.joined(separator: ", ")))"
147+
)
143148
case .nullable(let wrappedType, let kind):
144149
let optionalSwiftType: String
145150
if case .null = kind {
@@ -831,6 +836,8 @@ struct StackCodegen {
831836
case .uint32, .uintWord: elementType = .integer(.uint32)
832837
case .float32: elementType = .float
833838
case .float64: elementType = .double
839+
case .bigInt64: elementType = .integer(.int64)
840+
case .bigUint64: elementType = .integer(.uint64)
834841
}
835842
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
836843
case .dictionary(let valueType):
@@ -1517,7 +1524,7 @@ extension BridgeType {
15171524

15181525
var isStackUsingParameter: Bool {
15191526
switch self {
1520-
case .swiftStruct, .array, .dictionary, .associatedValueEnum, .typedArray:
1527+
case .swiftStruct, .array, .dictionary, .associatedValueEnum:
15211528
return true
15221529
case .nullable(let wrapped, _):
15231530
return wrapped.isStackUsingParameter
@@ -1577,8 +1584,10 @@ extension BridgeType {
15771584
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
15781585
case .closure:
15791586
return LiftingIntrinsicInfo(parameters: [("callbackId", .i32)])
1580-
case .array, .dictionary, .typedArray:
1587+
case .array, .dictionary:
15811588
return LiftingIntrinsicInfo(parameters: [])
1589+
case .typedArray:
1590+
return LiftingIntrinsicInfo(parameters: [("sourceId", .i32), ("count", .i32)])
15821591
}
15831592
}
15841593

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,22 @@ public struct BridgeJSLink {
397397
printer.write("bytes.set(source);")
398398
}
399399
printer.write("}")
400+
printer.write(
401+
"bjs[\"swift_js_init_typed_array_memory\"] = function(sourceId, destPtr, byteLength) {"
402+
)
403+
printer.indent {
404+
printer.write(
405+
"const source = \(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).getObject(sourceId);"
406+
)
407+
printer.write(
408+
"\(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).release(sourceId);"
409+
)
410+
printer.write(
411+
"const dest = new Uint8Array(\(JSGlueVariableScope.reservedMemory).buffer, destPtr, byteLength);"
412+
)
413+
printer.write("dest.set(new Uint8Array(source.buffer, source.byteOffset, source.byteLength));")
414+
}
415+
printer.write("}")
400416
printer.write("bjs[\"swift_js_make_js_string\"] = function(ptr, len) {")
401417
printer.indent {
402418
printer.write(
@@ -480,9 +496,9 @@ public struct BridgeJSLink {
480496
printer.write("}")
481497
// TypedArray push: immediately copies data from WASM memory into a JS TypedArray.
482498
// This is safe because the data is read before the Swift array can be freed.
483-
// kind: 0=Int8, 1=Uint8, 2=Int16, 3=Uint16, 4=Int32, 5=Uint32, 6=Float32, 7=Float64
499+
// kind: 0=Int8, 1=Uint8, 2=Int16, 3=Uint16, 4=Int32, 5=Uint32, 6=Float32, 7=Float64, 8=BigInt64, 9=BigUint64
484500
printer.write(
485-
"const typedArrayConstructors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array];"
501+
"const typedArrayConstructors = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array];"
486502
)
487503
printer.write("bjs[\"swift_js_push_typed_array\"] = function(ptr, count, kind) {")
488504
printer.indent {

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,7 +1325,7 @@ struct IntrinsicJSFragment: Sendable {
13251325
case .array(let elementType):
13261326
return try arrayLower(elementType: elementType)
13271327
case .typedArray(let kind):
1328-
return try typedArrayLower(kind: kind)
1328+
return try typedArrayLowerParameter(kind: kind)
13291329
case .dictionary(let valueType):
13301330
return try dictionaryLower(valueType: valueType)
13311331
default:
@@ -1498,6 +1498,8 @@ struct IntrinsicJSFragment: Sendable {
14981498
case .uint32, .uintWord: elementType = .integer(.uint32)
14991499
case .float32: elementType = .float
15001500
case .float64: elementType = .double
1501+
case .bigInt64: elementType = .integer(.int64)
1502+
case .bigUint64: elementType = .integer(.uint64)
15011503
}
15021504
return try arrayLift(elementType: elementType)
15031505
case .exportSwift:
@@ -1567,7 +1569,7 @@ struct IntrinsicJSFragment: Sendable {
15671569
case .array(let elementType):
15681570
return try arrayLower(elementType: elementType)
15691571
case .typedArray(let kind):
1570-
return try typedArrayLower(kind: kind)
1572+
return try typedArrayLowerElementByElement(kind: kind)
15711573
case .dictionary(let valueType):
15721574
return try dictionaryLower(valueType: valueType)
15731575
default:
@@ -1964,10 +1966,34 @@ struct IntrinsicJSFragment: Sendable {
19641966
)
19651967
}
19661968

1967-
/// Lowers a TypedArray from JS to Swift: fall back to array element-by-element protocol.
1968-
/// JS → Swift typed array optimization is deferred (no WASM allocator available).
1969-
static func typedArrayLower(kind: TypedArrayKind) throws -> IntrinsicJSFragment {
1970-
// Convert the TypedArrayKind back to a BridgeType element type and delegate to arrayLower
1969+
/// Lowers a TypedArray from JS to Swift by retaining it in the JS heap and passing
1970+
/// (retainedId, length) as two i32 WASM params. The Swift side then bulk-copies
1971+
/// via `_bridgeJS_typedArrayLiftParameter`. Mirrors the string lowering pattern.
1972+
/// Used for JS → Swift parameter lowering (export direction).
1973+
static func typedArrayLowerParameter(kind: TypedArrayKind) throws -> IntrinsicJSFragment {
1974+
let jsConstructorName = kind.jsConstructorName
1975+
return IntrinsicJSFragment(
1976+
parameters: ["arr"],
1977+
printCode: { arguments, context in
1978+
let (scope, printer) = (context.scope, context.printer)
1979+
let arr = arguments[0]
1980+
let typedVar = scope.variable("\(arr)Typed")
1981+
let retainedIdVar = scope.variable("\(arr)Id")
1982+
// Ensure input is a proper TypedArray (plain Array<number> must be converted)
1983+
printer.write(
1984+
"const \(typedVar) = \(arr) instanceof \(jsConstructorName) ? \(arr) : new \(jsConstructorName)(\(arr));"
1985+
)
1986+
printer.write(
1987+
"const \(retainedIdVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(typedVar));"
1988+
)
1989+
return [retainedIdVar, "\(typedVar).length"]
1990+
}
1991+
)
1992+
}
1993+
1994+
/// Lowers a TypedArray from JS to Swift using element-by-element stack protocol.
1995+
/// Used for return value lowering (import direction) and nested contexts (struct fields).
1996+
static func typedArrayLowerElementByElement(kind: TypedArrayKind) throws -> IntrinsicJSFragment {
19711997
let elementType: BridgeType
19721998
switch kind {
19731999
case .int8: elementType = .integer(.int8)
@@ -1978,6 +2004,8 @@ struct IntrinsicJSFragment: Sendable {
19782004
case .uint32, .uintWord: elementType = .integer(.uint32)
19792005
case .float32: elementType = .float
19802006
case .float64: elementType = .double
2007+
case .bigInt64: elementType = .integer(.int64)
2008+
case .bigUint64: elementType = .integer(.uint64)
19812009
}
19822010
return try arrayLower(elementType: elementType)
19832011
}
@@ -2146,6 +2174,8 @@ struct IntrinsicJSFragment: Sendable {
21462174
case .uint32, .uintWord: elementType = .integer(.uint32)
21472175
case .float32: elementType = .float
21482176
case .float64: elementType = .double
2177+
case .bigInt64: elementType = .integer(.int64)
2178+
case .bigUint64: elementType = .integer(.uint64)
21492179
}
21502180
return try arrayLift(elementType: elementType)
21512181
default:
@@ -2270,7 +2300,7 @@ struct IntrinsicJSFragment: Sendable {
22702300
case .dictionary(let valueType):
22712301
return try dictionaryLower(valueType: valueType)
22722302
case .typedArray(let kind):
2273-
return try typedArrayLower(kind: kind)
2303+
return try typedArrayLowerElementByElement(kind: kind)
22742304
default:
22752305
throw BridgeJSLinkError(message: "Unsupported array element type for lowering: \(elementType)")
22762306
}

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,16 @@ public enum TypedArrayKind: String, Codable, Equatable, Hashable, Sendable {
272272
case uintWord // Swift `UInt` (word-sized, 32-bit on wasm32)
273273
case float32
274274
case float64
275+
case bigInt64
276+
case bigUint64
275277

276278
/// Byte size of one element
277279
public var byteSize: Int {
278280
switch self {
279281
case .int8, .uint8: return 1
280282
case .int16, .uint16: return 2
281283
case .int32, .uint32, .intWord, .uintWord, .float32: return 4
282-
case .float64: return 8
284+
case .float64, .bigInt64, .bigUint64: return 8
283285
}
284286
}
285287

@@ -294,6 +296,8 @@ public enum TypedArrayKind: String, Codable, Equatable, Hashable, Sendable {
294296
case .uint32, .uintWord: return "Uint32Array"
295297
case .float32: return "Float32Array"
296298
case .float64: return "Float64Array"
299+
case .bigInt64: return "BigInt64Array"
300+
case .bigUint64: return "BigUint64Array"
297301
}
298302
}
299303

@@ -313,6 +317,8 @@ public enum TypedArrayKind: String, Codable, Equatable, Hashable, Sendable {
313317
case .uintWord: return "UInt"
314318
case .float32: return "Float"
315319
case .float64: return "Double"
320+
case .bigInt64: return "Int64"
321+
case .bigUint64: return "UInt64"
316322
}
317323
}
318324

@@ -327,7 +333,8 @@ public enum TypedArrayKind: String, Codable, Equatable, Hashable, Sendable {
327333
case (true, .word): return .intWord // Swift Int on wasm32
328334
case (false, .w32): return .uint32
329335
case (false, .word): return .uintWord // Swift UInt on wasm32
330-
case (_, .w64): return nil // BigInt interop deferred
336+
case (true, .w64): return .bigInt64
337+
case (false, .w64): return .bigUint64
331338
}
332339
}
333340

Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/TypedArrayTypes.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
return data
1919
}
2020

21+
@JS func processInt64Array(_ data: [Int64]) -> [Int64] {
22+
return data
23+
}
24+
25+
@JS func processUInt64Array(_ data: [UInt64]) -> [UInt64] {
26+
return data
27+
}
28+
2129
// This should NOT become a typed array — it should stay as string[]
2230
@JS func processStringArray(_ data: [String]) -> [String] {
2331
return data

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/TypedArrayOverride.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@_expose(wasm, "bjs_processData")
22
@_cdecl("bjs_processData")
3-
public func _bjs_processData() -> Void {
3+
public func _bjs_processData(_ dataSourceId: Int32, _ dataCount: Int32) -> Void {
44
#if arch(wasm32)
5-
let ret = processData(_: [UInt8].bridgeJSStackPop())
5+
let ret = processData(_: _bridgeJS_typedArrayLiftParameter(dataSourceId, dataCount))
66
_bridgeJS_typedArrayPush(ret)
77
#else
88
fatalError("Only available on WebAssembly")
@@ -11,9 +11,9 @@ public func _bjs_processData() -> Void {
1111

1212
@_expose(wasm, "bjs_processFloat32")
1313
@_cdecl("bjs_processFloat32")
14-
public func _bjs_processFloat32() -> Void {
14+
public func _bjs_processFloat32(_ dataSourceId: Int32, _ dataCount: Int32) -> Void {
1515
#if arch(wasm32)
16-
let ret = processFloat32(_: [Float].bridgeJSStackPop())
16+
let ret = processFloat32(_: _bridgeJS_typedArrayLiftParameter(dataSourceId, dataCount))
1717
_bridgeJS_typedArrayPush(ret)
1818
#else
1919
fatalError("Only available on WebAssembly")
@@ -55,9 +55,9 @@ public func _bjs_GPURenderer_init() -> UnsafeMutableRawPointer {
5555

5656
@_expose(wasm, "bjs_GPURenderer_writeVertexData")
5757
@_cdecl("bjs_GPURenderer_writeVertexData")
58-
public func _bjs_GPURenderer_writeVertexData(_ _self: UnsafeMutableRawPointer) -> Void {
58+
public func _bjs_GPURenderer_writeVertexData(_ _self: UnsafeMutableRawPointer, _ dataSourceId: Int32, _ dataCount: Int32) -> Void {
5959
#if arch(wasm32)
60-
let ret = GPURenderer.bridgeJSLiftParameter(_self).writeVertexData(_: [Float].bridgeJSStackPop())
60+
let ret = GPURenderer.bridgeJSLiftParameter(_self).writeVertexData(_: _bridgeJS_typedArrayLiftParameter(dataSourceId, dataCount))
6161
_bridgeJS_typedArrayPush(ret)
6262
#else
6363
fatalError("Only available on WebAssembly")

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/TypedArrayTypes.TypedArray.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,56 @@
133133
}
134134
}
135135
},
136+
{
137+
"abiName" : "bjs_processInt64Array",
138+
"effects" : {
139+
"isAsync" : false,
140+
"isStatic" : false,
141+
"isThrows" : false
142+
},
143+
"name" : "processInt64Array",
144+
"parameters" : [
145+
{
146+
"label" : "_",
147+
"name" : "data",
148+
"type" : {
149+
"typedArray" : {
150+
"_0" : "bigInt64"
151+
}
152+
}
153+
}
154+
],
155+
"returnType" : {
156+
"typedArray" : {
157+
"_0" : "bigInt64"
158+
}
159+
}
160+
},
161+
{
162+
"abiName" : "bjs_processUInt64Array",
163+
"effects" : {
164+
"isAsync" : false,
165+
"isStatic" : false,
166+
"isThrows" : false
167+
},
168+
"name" : "processUInt64Array",
169+
"parameters" : [
170+
{
171+
"label" : "_",
172+
"name" : "data",
173+
"type" : {
174+
"typedArray" : {
175+
"_0" : "bigUint64"
176+
}
177+
}
178+
}
179+
],
180+
"returnType" : {
181+
"typedArray" : {
182+
"_0" : "bigUint64"
183+
}
184+
}
185+
},
136186
{
137187
"abiName" : "bjs_processStringArray",
138188
"effects" : {

0 commit comments

Comments
 (0)