Skip to content

Commit d4fbbbf

Browse files
committed
BridgeJS: Add opt-in TypedArray bridging for numeric arrays
Add typedArrayBridging config option ('always'/'never', default 'never') and per-declaration @js(typedArray: true) override 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 memory.buffer.slice(). Uses .slice() (copy) instead of a zero-copy view because TypedArray views into memory.buffer are invalidated when WASM memory grows. JS->Swift direction falls back to element-by-element stack protocol. - Add TypedArrayKind enum and BridgeType.typedArray case - Add typedArrayBridging config to BridgeJSConfig - Add @js(typedArray:) per-declaration override parameter - Add _BridgeJSTypedArrayElement protocol and intrinsics - Generate TypedArray construction in JS glue code - Add UInt8 array benchmarks - Update Documentation.docc for new config and type mapping
1 parent 5e96639 commit d4fbbbf

83 files changed

Lines changed: 3314 additions & 21 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
@@ -1870,6 +1870,49 @@ public func _bjs_ArrayRoundtrip_makeIntArrayLarge(_ _self: UnsafeMutableRawPoint
18701870
#endif
18711871
}
18721872

1873+
@_expose(wasm, "bjs_ArrayRoundtrip_takeUInt8Array")
1874+
@_cdecl("bjs_ArrayRoundtrip_takeUInt8Array")
1875+
public func _bjs_ArrayRoundtrip_takeUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1876+
#if arch(wasm32)
1877+
ArrayRoundtrip.bridgeJSLiftParameter(_self).takeUInt8Array(_: [UInt8].bridgeJSStackPop())
1878+
#else
1879+
fatalError("Only available on WebAssembly")
1880+
#endif
1881+
}
1882+
1883+
@_expose(wasm, "bjs_ArrayRoundtrip_makeUInt8Array")
1884+
@_cdecl("bjs_ArrayRoundtrip_makeUInt8Array")
1885+
public func _bjs_ArrayRoundtrip_makeUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1886+
#if arch(wasm32)
1887+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeUInt8Array()
1888+
ret.bridgeJSStackPush()
1889+
#else
1890+
fatalError("Only available on WebAssembly")
1891+
#endif
1892+
}
1893+
1894+
@_expose(wasm, "bjs_ArrayRoundtrip_roundtripUInt8Array")
1895+
@_cdecl("bjs_ArrayRoundtrip_roundtripUInt8Array")
1896+
public func _bjs_ArrayRoundtrip_roundtripUInt8Array(_ _self: UnsafeMutableRawPointer) -> Void {
1897+
#if arch(wasm32)
1898+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).roundtripUInt8Array(_: [UInt8].bridgeJSStackPop())
1899+
ret.bridgeJSStackPush()
1900+
#else
1901+
fatalError("Only available on WebAssembly")
1902+
#endif
1903+
}
1904+
1905+
@_expose(wasm, "bjs_ArrayRoundtrip_makeUInt8ArrayLarge")
1906+
@_cdecl("bjs_ArrayRoundtrip_makeUInt8ArrayLarge")
1907+
public func _bjs_ArrayRoundtrip_makeUInt8ArrayLarge(_ _self: UnsafeMutableRawPointer) -> Void {
1908+
#if arch(wasm32)
1909+
let ret = ArrayRoundtrip.bridgeJSLiftParameter(_self).makeUInt8ArrayLarge()
1910+
ret.bridgeJSStackPush()
1911+
#else
1912+
fatalError("Only available on WebAssembly")
1913+
#endif
1914+
}
1915+
18731916
@_expose(wasm, "bjs_ArrayRoundtrip_takeDoubleArray")
18741917
@_cdecl("bjs_ArrayRoundtrip_takeDoubleArray")
18751918
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
@@ -962,7 +962,7 @@ extension BridgeType {
962962
var params = [("isSome", WasmCoreType.i32)]
963963
params.append(contentsOf: wrappedInfo.loweredParameters)
964964
return LoweringParameterInfo(loweredParameters: params, useBorrowing: wrappedInfo.useBorrowing)
965-
case .array, .dictionary:
965+
case .array, .dictionary, .typedArray:
966966
return LoweringParameterInfo(loweredParameters: [])
967967
}
968968
}
@@ -1040,7 +1040,7 @@ extension BridgeType {
10401040
}
10411041
let wrappedInfo = try wrappedType.liftingReturnInfo(context: context)
10421042
return LiftingReturnInfo(valueToLift: wrappedInfo.valueToLift)
1043-
case .array, .dictionary:
1043+
case .array, .dictionary, .typedArray:
10441044
return LiftingReturnInfo(valueToLift: nil)
10451045
}
10461046
}

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)