Skip to content

Commit 648ab81

Browse files
committed
BridgeJS: Add opt-in TypedArray bridging for numeric arrays
Add typedArrayBridging config option ('always'/'never', default 'never') that bridges Swift numeric arrays as JavaScript TypedArrays instead of Array<number>. When enabled, [UInt8] becomes Uint8Array, [Float] becomes Float32Array, [Double] becomes Float64Array, etc. Non-numeric arrays are unaffected. Swift->JS direction uses bulk memory copy via withUnsafeBufferPointer. JS->Swift direction falls back to element-by-element (no WASM allocator available from JS). - Add TypedArrayKind enum and BridgeType.typedArray case - Add typedArrayBridging config to BridgeJSConfig - Add _BridgeJSTypedArrayElement protocol and intrinsics - Generate TypedArray construction in JS glue code - Handle typed arrays in all exhaustive BridgeType switches
1 parent 0f5c465 commit 648ab81

98 files changed

Lines changed: 13397 additions & 24 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.

Benchmarks/Sources/Benchmarks.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,20 @@ class IdentityCacheBenchmarkIdentity {
384384
return Array(1...10000)
385385
}
386386

387+
// MARK: Primitive Arrays - UInt8
388+
389+
@JS func takeUInt8Array(_ values: [UInt8]) {}
390+
@JS func makeUInt8Array() -> [UInt8] {
391+
return (0..<1000).map { UInt8($0 % 256) }
392+
}
393+
@JS func roundtripUInt8Array(_ values: [UInt8]) -> [UInt8] {
394+
return values
395+
}
396+
397+
@JS func makeUInt8ArrayLarge() -> [UInt8] {
398+
return (0..<10000).map { UInt8($0 % 256) }
399+
}
400+
387401
// MARK: Primitive Arrays - Double
388402

389403
@JS func takeDoubleArray(_ values: [Double]) {}

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,49 @@ public func _bjs_ArrayRoundtrip_makeIntArrayLarge(_ _self: UnsafeMutableRawPoint
18691869
#endif
18701870
}
18711871

1872+
@_expose(wasm, "bjs_ArrayRoundtrip_takeUInt8Array")
1873+
@_cdecl("bjs_ArrayRoundtrip_takeUInt8Array")
1874+
public func _bjs_ArrayRoundtrip_takeUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1875+
#if arch(wasm32)
1876+
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeUInt8Array(_: [UInt8].bridgeJSStackPop())
1877+
#else
1878+
fatalError("Only available on WebAssembly")
1879+
#endif
1880+
}
1881+
1882+
@_expose(wasm, "bjs_ArrayRoundtrip_makeUInt8Array")
1883+
@_cdecl("bjs_ArrayRoundtrip_makeUInt8Array")
1884+
public func _bjs_ArrayRoundtrip_makeUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1885+
#if arch(wasm32)
1886+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeUInt8Array()
1887+
ret.bridgeJSStackPush()
1888+
#else
1889+
fatalError("Only available on WebAssembly")
1890+
#endif
1891+
}
1892+
1893+
@_expose(wasm, "bjs_ArrayRoundtrip_roundtripUInt8Array")
1894+
@_cdecl("bjs_ArrayRoundtrip_roundtripUInt8Array")
1895+
public func _bjs_ArrayRoundtrip_roundtripUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1896+
#if arch(wasm32)
1897+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripUInt8Array(_: [UInt8].bridgeJSStackPop())
1898+
ret.bridgeJSStackPush()
1899+
#else
1900+
fatalError("Only available on WebAssembly")
1901+
#endif
1902+
}
1903+
1904+
@_expose(wasm, "bjs_ArrayRoundtrip_makeUInt8ArrayLarge")
1905+
@_cdecl("bjs_ArrayRoundtrip_makeUInt8ArrayLarge")
1906+
public func _bjs_ArrayRoundtrip_makeUInt8ArrayLarge(_ _self: UnsafeMutableRawPointer) -> Void {
1907+
#if arch(wasm32)
1908+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeUInt8ArrayLarge()
1909+
ret.bridgeJSStackPush()
1910+
#else
1911+
fatalError("Only available on WebAssembly")
1912+
#endif
1913+
}
1914+
18721915
@_expose(wasm, "bjs_ArrayRoundtrip_takeDoubleArray")
18731916
@_cdecl("bjs_ArrayRoundtrip_takeDoubleArray")
18741917
public func _bjs_ArrayRoundtrip_takeDoubleArray(_ _self: UnsafeMutableRawPointer) -> Void {

Benchmarks/Sources/Generated/JavaScript/BridgeJS.json

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,125 @@
19021902
}
19031903
}
19041904
},
1905+
{
1906+
"abiName" : "bjs_ArrayRoundtrip_takeUInt8Array",
1907+
"effects" : {
1908+
"isAsync" : false,
1909+
"isStatic" : false,
1910+
"isThrows" : false
1911+
},
1912+
"name" : "takeUInt8Array",
1913+
"parameters" : [
1914+
{
1915+
"label" : "_",
1916+
"name" : "values",
1917+
"type" : {
1918+
"array" : {
1919+
"_0" : {
1920+
"integer" : {
1921+
"_0" : {
1922+
"isSigned" : false,
1923+
"width" : "w8"
1924+
}
1925+
}
1926+
}
1927+
}
1928+
}
1929+
}
1930+
],
1931+
"returnType" : {
1932+
"void" : {
1933+
1934+
}
1935+
}
1936+
},
1937+
{
1938+
"abiName" : "bjs_ArrayRoundtrip_makeUInt8Array",
1939+
"effects" : {
1940+
"isAsync" : false,
1941+
"isStatic" : false,
1942+
"isThrows" : false
1943+
},
1944+
"name" : "makeUInt8Array",
1945+
"parameters" : [
1946+
1947+
],
1948+
"returnType" : {
1949+
"array" : {
1950+
"_0" : {
1951+
"integer" : {
1952+
"_0" : {
1953+
"isSigned" : false,
1954+
"width" : "w8"
1955+
}
1956+
}
1957+
}
1958+
}
1959+
}
1960+
},
1961+
{
1962+
"abiName" : "bjs_ArrayRoundtrip_roundtripUInt8Array",
1963+
"effects" : {
1964+
"isAsync" : false,
1965+
"isStatic" : false,
1966+
"isThrows" : false
1967+
},
1968+
"name" : "roundtripUInt8Array",
1969+
"parameters" : [
1970+
{
1971+
"label" : "_",
1972+
"name" : "values",
1973+
"type" : {
1974+
"array" : {
1975+
"_0" : {
1976+
"integer" : {
1977+
"_0" : {
1978+
"isSigned" : false,
1979+
"width" : "w8"
1980+
}
1981+
}
1982+
}
1983+
}
1984+
}
1985+
}
1986+
],
1987+
"returnType" : {
1988+
"array" : {
1989+
"_0" : {
1990+
"integer" : {
1991+
"_0" : {
1992+
"isSigned" : false,
1993+
"width" : "w8"
1994+
}
1995+
}
1996+
}
1997+
}
1998+
}
1999+
},
2000+
{
2001+
"abiName" : "bjs_ArrayRoundtrip_makeUInt8ArrayLarge",
2002+
"effects" : {
2003+
"isAsync" : false,
2004+
"isStatic" : false,
2005+
"isThrows" : false
2006+
},
2007+
"name" : "makeUInt8ArrayLarge",
2008+
"parameters" : [
2009+
2010+
],
2011+
"returnType" : {
2012+
"array" : {
2013+
"_0" : {
2014+
"integer" : {
2015+
"_0" : {
2016+
"isSigned" : false,
2017+
"width" : "w8"
2018+
}
2019+
}
2020+
}
2021+
}
2022+
}
2023+
},
19052024
{
19062025
"abiName" : "bjs_ArrayRoundtrip_takeDoubleArray",
19072026
"effects" : {

Benchmarks/run.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,30 @@ async function singleRun(results, nameFilter, iterations) {
668668
}
669669
})
670670

671+
// Primitive Arrays - UInt8
672+
benchmarkRunner("ArrayRoundtrip/takeUInt8Array", () => {
673+
const arr = Array.from({length: 1000}, (_, i) => i % 256)
674+
for (let i = 0; i < iterations; i++) {
675+
arrayRoundtrip.takeUInt8Array(arr)
676+
}
677+
})
678+
benchmarkRunner("ArrayRoundtrip/makeUInt8Array", () => {
679+
for (let i = 0; i < iterations; i++) {
680+
arrayRoundtrip.makeUInt8Array()
681+
}
682+
})
683+
benchmarkRunner("ArrayRoundtrip/roundtripUInt8Array", () => {
684+
const arr = Array.from({length: 1000}, (_, i) => i % 256)
685+
for (let i = 0; i < iterations; i++) {
686+
arrayRoundtrip.roundtripUInt8Array(arr)
687+
}
688+
})
689+
benchmarkRunner("ArrayRoundtrip/makeUInt8ArrayLarge", () => {
690+
for (let i = 0; i < iterations; i++) {
691+
arrayRoundtrip.makeUInt8ArrayLarge()
692+
}
693+
})
694+
671695
// Primitive Arrays - Double
672696
benchmarkRunner("ArrayRoundtrip/takeDoubleArray", () => {
673697
const arr = Array.from({length: 1000}, (_, i) => (i + 1) * 1.1)

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public class ExportSwift {
137137
case .swiftStruct(let structName):
138138
typeNameForIntrinsic = structName
139139
liftingExpr = ExprSyntax("\(raw: structName).bridgeJSLiftParameter()")
140-
case .array:
140+
case .array, .typedArray:
141141
typeNameForIntrinsic = param.type.swiftType
142142
liftingExpr = StackCodegen().liftExpression(for: param.type)
143143
case .nullable(let wrappedType, let kind):
@@ -306,6 +306,16 @@ public class ExportSwift {
306306
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {
307307
append(stmt)
308308
}
309+
case .typedArray:
310+
// Top-level typed array return: use immediate copy via _bridgeJS_typedArrayPush
311+
append("_bridgeJS_typedArrayPush(ret)")
312+
case .nullable(.typedArray, _):
313+
// Optional typed arrays in return position: use stack-based optional protocol
314+
// (element-by-element) to match JS liftReturn's optional handling.
315+
let stackCodegen = StackCodegen()
316+
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {
317+
append(stmt)
318+
}
309319
case .dictionary(.swiftProtocol):
310320
let stackCodegen = StackCodegen()
311321
for stmt in stackCodegen.lowerStatements(for: returnType, accessor: "ret", varPrefix: "ret") {
@@ -754,7 +764,8 @@ struct StackCodegen {
754764
switch type {
755765
case .string, .integer, .bool, .float, .double,
756766
.jsObject(nil), .jsValue, .swiftStruct, .swiftHeapObject, .unsafePointer,
757-
.swiftProtocol, .caseEnum, .associatedValueEnum, .rawValueEnum, .array, .dictionary:
767+
.swiftProtocol, .caseEnum, .associatedValueEnum, .rawValueEnum, .array, .dictionary,
768+
.typedArray:
758769
return "\(raw: type.swiftType).bridgeJSStackPop()"
759770
case .jsObject(let className?):
760771
return "\(raw: className)(unsafelyWrapping: JSObject.bridgeJSStackPop())"
@@ -772,7 +783,7 @@ struct StackCodegen {
772783
switch wrappedType {
773784
case .string, .integer, .bool, .float, .double, .jsObject(nil), .jsValue,
774785
.swiftStruct, .swiftHeapObject, .caseEnum, .associatedValueEnum, .rawValueEnum,
775-
.array, .dictionary:
786+
.array, .dictionary, .typedArray:
776787
return "\(raw: typeName)<\(raw: wrappedType.swiftType)>.bridgeJSStackPop()"
777788
case .jsObject(let className?):
778789
return "\(raw: typeName)<JSObject>.bridgeJSStackPop().map { \(raw: className)(unsafelyWrapping: $0) }"
@@ -807,6 +818,21 @@ struct StackCodegen {
807818
return []
808819
case .array(let elementType):
809820
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
821+
case .typedArray(let kind):
822+
// In stack context (struct fields), use element-by-element protocol
823+
// to match JS stackLiftFragment which also falls back to arrayLift.
824+
let elementType: BridgeType
825+
switch kind {
826+
case .int8: elementType = .integer(.int8)
827+
case .uint8: elementType = .integer(.uint8)
828+
case .int16: elementType = .integer(.int16)
829+
case .uint16: elementType = .integer(.uint16)
830+
case .int32, .intWord: elementType = .integer(.int32)
831+
case .uint32, .uintWord: elementType = .integer(.uint32)
832+
case .float32: elementType = .float
833+
case .float64: elementType = .double
834+
}
835+
return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix)
810836
case .dictionary(let valueType):
811837
return lowerDictionaryStatements(valueType: valueType, accessor: accessor, varPrefix: varPrefix)
812838
}
@@ -1459,6 +1485,7 @@ extension BridgeType {
14591485
case .nullable(let wrappedType, let kind):
14601486
return kind == .null ? "Optional<\(wrappedType.swiftType)>" : "JSUndefinedOr<\(wrappedType.swiftType)>"
14611487
case .array(let elementType): return "[\(elementType.swiftType)]"
1488+
case .typedArray(let kind): return "[\(kind.swiftElementType)]"
14621489
case .dictionary(let valueType): return "[String: \(valueType.swiftType)]"
14631490
case .caseEnum(let name): return name
14641491
case .rawValueEnum(let name, _): return name
@@ -1490,7 +1517,7 @@ extension BridgeType {
14901517

14911518
var isStackUsingParameter: Bool {
14921519
switch self {
1493-
case .swiftStruct, .array, .dictionary, .associatedValueEnum:
1520+
case .swiftStruct, .array, .dictionary, .associatedValueEnum, .typedArray:
14941521
return true
14951522
case .nullable(let wrapped, _):
14961523
return wrapped.isStackUsingParameter
@@ -1550,7 +1577,7 @@ extension BridgeType {
15501577
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
15511578
case .closure:
15521579
return LiftingIntrinsicInfo(parameters: [("callbackId", .i32)])
1553-
case .array, .dictionary:
1580+
case .array, .dictionary, .typedArray:
15541581
return LiftingIntrinsicInfo(parameters: [])
15551582
}
15561583
}
@@ -1601,7 +1628,7 @@ extension BridgeType {
16011628
throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters")
16021629
case .closure:
16031630
return .jsObject
1604-
case .array, .dictionary:
1631+
case .array, .dictionary, .typedArray:
16051632
return .array
16061633
}
16071634
}

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ extension BridgeType {
960960
var params = [("isSome", WasmCoreType.i32)]
961961
params.append(contentsOf: wrappedInfo.loweredParameters)
962962
return LoweringParameterInfo(loweredParameters: params, useBorrowing: wrappedInfo.useBorrowing)
963-
case .array, .dictionary:
963+
case .array, .dictionary, .typedArray:
964964
return LoweringParameterInfo(loweredParameters: [])
965965
}
966966
}
@@ -1034,7 +1034,7 @@ extension BridgeType {
10341034
case .nullable(let wrappedType, _):
10351035
let wrappedInfo = try wrappedType.liftingReturnInfo(context: context)
10361036
return LiftingReturnInfo(valueToLift: wrappedInfo.valueToLift)
1037-
case .array, .dictionary:
1037+
case .array, .dictionary, .typedArray:
10381038
return LiftingReturnInfo(valueToLift: nil)
10391039
}
10401040
}

Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,23 +351,40 @@ public struct BridgeJSConfig: Codable {
351351
/// Default: `nil` (treated as `"none"`)
352352
public var identityMode: String?
353353

354-
public init(tools: [String: String]? = nil, exposeToGlobal: Bool = false, identityMode: String? = nil) {
354+
/// Controls whether numeric Swift arrays are bridged as JavaScript TypedArrays.
355+
///
356+
/// - `"always"`: Numeric arrays (`[UInt8]`, `[Float]`, `[Double]`, etc.)
357+
/// are bridged as TypedArrays; non-numeric arrays remain `Array<T>`.
358+
/// - `"never"` (default): All arrays use the element-by-element stack protocol (backward compat).
359+
///
360+
/// Default: `nil` (treated as `"never"`)
361+
public var typedArrayBridging: String?
362+
363+
public init(
364+
tools: [String: String]? = nil,
365+
exposeToGlobal: Bool = false,
366+
identityMode: String? = nil,
367+
typedArrayBridging: String? = nil
368+
) {
355369
self.tools = tools
356370
self.exposeToGlobal = exposeToGlobal
357371
self.identityMode = identityMode
372+
self.typedArrayBridging = typedArrayBridging
358373
}
359374

360375
enum CodingKeys: String, CodingKey {
361376
case tools
362377
case exposeToGlobal
363378
case identityMode
379+
case typedArrayBridging
364380
}
365381

366382
public init(from decoder: Decoder) throws {
367383
let container = try decoder.container(keyedBy: CodingKeys.self)
368384
tools = try container.decodeIfPresent([String: String].self, forKey: .tools)
369385
exposeToGlobal = try container.decodeIfPresent(Bool.self, forKey: .exposeToGlobal) ?? false
370386
identityMode = try container.decodeIfPresent(String.self, forKey: .identityMode)
387+
typedArrayBridging = try container.decodeIfPresent(String.self, forKey: .typedArrayBridging)
371388
}
372389

373390
/// Load the configuration file from the SwiftPM package target directory.
@@ -411,7 +428,8 @@ public struct BridgeJSConfig: Codable {
411428
return BridgeJSConfig(
412429
tools: (tools ?? [:]).merging(overrides.tools ?? [:], uniquingKeysWith: { $1 }),
413430
exposeToGlobal: overrides.exposeToGlobal,
414-
identityMode: overrides.identityMode ?? identityMode
431+
identityMode: overrides.identityMode ?? identityMode,
432+
typedArrayBridging: overrides.typedArrayBridging ?? typedArrayBridging
415433
)
416434
}
417435
}

0 commit comments

Comments
 (0)