Skip to content

Commit 8f7d9be

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 8f7d9be

64 files changed

Lines changed: 1774 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

DECISIONS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# TypedArray Bridging Rework — Design Decisions
2+
3+
## Decision 1: Internal optimization, not API change (per Yuta's feedback)
4+
5+
**Context:** Original implementation used `BridgeType.typedArray(TypedArrayKind)` with config and `@JS(typedArray:)` to change `[UInt8]``Uint8Array` in TypeScript. Yuta's feedback: "we can solve performance and API independently without introducing config."
6+
7+
**Decision:** Remove `BridgeType.typedArray`, config, and `@JS(typedArray:)`. Instead, optimize the internal transfer mechanism for `BridgeType.array(elementType)` when elementType is numeric. TS type stays `number[]` / `bigint[]`.
8+
9+
## Decision 2: Array.from(TypedArray) conversion is acceptable
10+
11+
**Context:** Bulk transfer produces a TypedArray view, but the user expects `number[]`. Converting via `Array.from()` adds a pure JS loop.
12+
13+
**Decision:** `Array.from()` for 1000 elements is <1µs in V8. Current element-by-element is ~20µs of WASM boundary crossing overhead. Still a ~20× improvement. Acceptable trade-off.
14+
15+
## Decision 3: JSTypedArray<T> as BridgeJS type is separate work
16+
17+
**Context:** Yuta said "for those who want raw TypedArray in JS side, use JSTypedArray rather than Swift.Array."
18+
19+
**Decision:** JSTypedArray<T> BridgeJS support is a separate PR. This PR focuses only on the internal performance optimization for `[T]`.
20+
21+
## Decision 4: Zero-copy view for internal transfer
22+
23+
**Context:** The TypedArray view created for `Array.from()` is consumed immediately in the same synchronous JS call. It never escapes to the user.
24+
25+
**Decision:** Use `new TypedArray(memory.buffer, ptr, count)` (zero-copy view) for the internal transfer, NOT `.slice()`. This is safe because the view is consumed by `Array.from()` before any WASM call can trigger `memory.grow()`. Same pattern as `swjs_decode_string` which uses `.subarray()` for `TextDecoder.decode()`.

PROGRESS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# TypedArray Bridging Rework — Progress
2+
3+
## Overview
4+
Rework typed array bridging per Yuta's feedback: internal performance optimization only, no API change. `[UInt8]` stays `number[]` in TS, but uses bulk TypedArray transfer internally.
5+
6+
## Key Files
7+
- `DECISIONS.md` — design rationale
8+
- Branch: `krodak/typed-array` (will be force-pushed after rework)
9+
- Draft PR: https://github.com/PassiveLogic/JavaScriptKit/pull/8
10+
11+
## Status: REWORKING
12+
13+
### Phase 1: Remove old approach
14+
- [ ] Remove `TypedArrayKind` enum and `BridgeType.typedArray` case
15+
- [ ] Remove `typedArrayBridging` config from Misc.swift, BridgeJSTool.swift, SwiftToSkeleton.swift
16+
- [ ] Remove `@JS(typedArray:)` parameter from Macros.swift and SwiftToSkeleton.swift
17+
- [ ] Remove all `.typedArray` switch cases across all files
18+
- [ ] Remove TypedArrayOverride test input and snapshots
19+
- [ ] Revert docs changes about config/`@JS(typedArray:)`
20+
21+
### Phase 2: Add internal optimization
22+
- [ ] Modify JS codegen for `.array(numericElement)` to use bulk TypedArray transfer
23+
- [ ] Swift→JS: `swift_js_push_typed_array` + JS does `Array.from(typedArrayView)`
24+
- [ ] JS→Swift: retain TypedArray + `swift_js_init_typed_array_memory` callback
25+
- [ ] Keep `_BridgeJSTypedArrayElement` protocol and intrinsics
26+
- [ ] Keep `swift_js_push_typed_array` and `swift_js_init_typed_array_memory` handlers
27+
28+
### Phase 3: Tests and verification
29+
- [ ] Update snapshot tests (TypedArrayTypes stays but outputs `number[]`)
30+
- [ ] Run full pipeline: format, snapshots, bridge-js-generate, unit tests, e2e tests
31+
- [ ] Update PR description

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) {")

0 commit comments

Comments
 (0)