Skip to content

Commit 6589dbb

Browse files
committed
BridgeJS: Use direct string retain + encodeInto() for stack ABI paths
For arrays, struct fields, enum payloads, and dictionary entries, the stack ABI now retains the JS string directly (instead of encoding to a Uint8Array first) and passes the worst-case UTF-8 byte length via _maxUTF8Len() as buffer capacity. A new dedicated swift_js_init_memory_from_string import handles the string path - it encodes UTF-8 directly into the WASM buffer via encodeInto() and returns the actual byte count written. This avoids modifying the existing swift_js_init_memory contract. This eliminates the intermediate Uint8Array allocation for every string element in arrays and struct fields.
1 parent 33fa793 commit 6589dbb

55 files changed

Lines changed: 506 additions & 143 deletions

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/BridgeJSLink/BridgeJSLink.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,10 @@ public struct BridgeJSLink {
356356
" \(JSGlueVariableScope.reservedStrEncCache).set(str, encoded);",
357357
" return encoded;",
358358
"}",
359+
// Worst-case UTF-8 byte count: each UTF-16 code unit can expand
360+
// to at most 3 bytes. Surrogate pairs (2 units) produce 4 bytes,
361+
// so the per-unit ratio stays <= 3.
362+
"function \(JSGlueVariableScope.reservedMaxUTF8Len)(str) { return str.length * 3; }",
359363
"let \(JSGlueVariableScope.reservedStringStack) = [];",
360364
"let \(JSGlueVariableScope.reservedI32Stack) = [];",
361365
"let \(JSGlueVariableScope.reservedI64Stack) = [];",
@@ -413,6 +417,25 @@ public struct BridgeJSLink {
413417
printer.write("bytes.set(source);")
414418
}
415419
printer.write("}")
420+
// Dedicated string-to-WASM-memory path used by the stack ABI.
421+
// Encodes UTF-8 directly into the target buffer - no intermediate
422+
// Uint8Array allocation. Returns actual byte count written.
423+
printer.write("bjs[\"swift_js_init_memory_from_string\"] = function(stringId, bytesPtr) {")
424+
printer.indent {
425+
printer.write(
426+
"const str = \(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).getObject(stringId);"
427+
)
428+
printer.write(
429+
"\(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).release(stringId);"
430+
)
431+
printer.write(
432+
"const target = new Uint8Array(\(JSGlueVariableScope.reservedMemory).buffer, bytesPtr);"
433+
)
434+
printer.write(
435+
"return \(JSGlueVariableScope.reservedTextEncoder).encodeInto(str, target).written;"
436+
)
437+
}
438+
printer.write("}")
416439
printer.write("bjs[\"swift_js_make_js_string\"] = function(ptr, len) {")
417440
printer.indent {
418441
printer.write(

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ final class JSGlueVariableScope {
2727
static let reservedCachedEncode = "_cachedEncode"
2828
static let reservedStrEncCache = "_strEncCache"
2929
static let reservedStrEncCacheMax = "_strEncCacheMax"
30+
static let reservedMaxUTF8Len = "_maxUTF8Len"
3031
static let reservedStringStack = "strStack"
3132
static let reservedI32Stack = "i32Stack"
3233
static let reservedI64Stack = "i64Stack"
@@ -59,6 +60,7 @@ final class JSGlueVariableScope {
5960
reservedCachedEncode,
6061
reservedStrEncCache,
6162
reservedStrEncCacheMax,
63+
reservedMaxUTF8Len,
6264
reservedStringStack,
6365
reservedI32Stack,
6466
reservedI64Stack,
@@ -2122,15 +2124,17 @@ struct IntrinsicJSFragment: Sendable {
21222124
printCode: { arguments, context in
21232125
let (scope, printer) = (context.scope, context.printer)
21242126
let value = arguments[0]
2125-
let bytesVar = scope.variable("bytes")
21262127
let idVar = scope.variable("id")
2128+
// Retain the JS string directly - no textEncoder.encode() allocation.
2129+
// swift_js_init_memory_from_string uses encodeInto() to write
2130+
// UTF-8 directly into the WASM buffer on the Swift side.
21272131
printer.write(
2128-
"const \(bytesVar) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(value));"
2132+
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(value));"
21292133
)
2130-
printer.write(
2131-
"const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesVar));"
2134+
scope.emitPushI32Parameter(
2135+
"\(JSGlueVariableScope.reservedMaxUTF8Len)(\(value))",
2136+
printer: printer
21322137
)
2133-
scope.emitPushI32Parameter("\(bytesVar).length", printer: printer)
21342138
scope.emitPushI32Parameter(idVar, printer: printer)
21352139
return []
21362140
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayTypes.js

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export async function createInstantiator(options, swift) {
4848
_strEncCache.set(str, encoded);
4949
return encoded;
5050
}
51+
function _maxUTF8Len(str) { return str.length * 3; }
5152
let strStack = [];
5253
let i32Stack = [];
5354
let i64Stack = [];
@@ -88,6 +89,12 @@ export async function createInstantiator(options, swift) {
8889
const bytes = new Uint8Array(memory.buffer, bytesPtr);
8990
bytes.set(source);
9091
}
92+
bjs["swift_js_init_memory_from_string"] = function(stringId, bytesPtr) {
93+
const str = swift.memory.getObject(stringId);
94+
swift.memory.release(stringId);
95+
const target = new Uint8Array(memory.buffer, bytesPtr);
96+
return textEncoder.encodeInto(str, target).written;
97+
}
9198
bjs["swift_js_make_js_string"] = function(ptr, len) {
9299
return swift.memory.retain(decodeString(ptr, len));
93100
}
@@ -317,9 +324,8 @@ export async function createInstantiator(options, swift) {
317324
arrayResult.reverse();
318325
let ret = imports.importProcessStrings(arrayResult);
319326
for (const elem of ret) {
320-
const bytes = textEncoder.encode(elem);
321-
const id = swift.memory.retain(bytes);
322-
i32Stack.push(bytes.length);
327+
const id = swift.memory.retain(elem);
328+
i32Stack.push(_maxUTF8Len(elem));
323329
i32Stack.push(id);
324330
}
325331
i32Stack.push(ret.length);
@@ -427,9 +433,8 @@ export async function createInstantiator(options, swift) {
427433
}
428434
i32Stack.push(nums.length);
429435
for (const elem1 of strs) {
430-
const bytes = textEncoder.encode(elem1);
431-
const id = swift.memory.retain(bytes);
432-
i32Stack.push(bytes.length);
436+
const id = swift.memory.retain(elem1);
437+
i32Stack.push(_maxUTF8Len(elem1));
433438
i32Stack.push(id);
434439
}
435440
i32Stack.push(strs.length);
@@ -482,9 +487,8 @@ export async function createInstantiator(options, swift) {
482487
},
483488
processStringArray: function bjs_processStringArray(values) {
484489
for (const elem of values) {
485-
const bytes = textEncoder.encode(elem);
486-
const id = swift.memory.retain(bytes);
487-
i32Stack.push(bytes.length);
490+
const id = swift.memory.retain(elem);
491+
i32Stack.push(_maxUTF8Len(elem));
488492
i32Stack.push(id);
489493
}
490494
i32Stack.push(values.length);
@@ -667,9 +671,8 @@ export async function createInstantiator(options, swift) {
667671
for (const elem of values) {
668672
const isSome = elem != null ? 1 : 0;
669673
if (isSome) {
670-
const bytes = textEncoder.encode(elem);
671-
const id = swift.memory.retain(bytes);
672-
i32Stack.push(bytes.length);
674+
const id = swift.memory.retain(elem);
675+
i32Stack.push(_maxUTF8Len(elem));
673676
i32Stack.push(id);
674677
}
675678
i32Stack.push(isSome);
@@ -823,9 +826,8 @@ export async function createInstantiator(options, swift) {
823826
processNestedStringArray: function bjs_processNestedStringArray(values) {
824827
for (const elem of values) {
825828
for (const elem1 of elem) {
826-
const bytes = textEncoder.encode(elem1);
827-
const id = swift.memory.retain(bytes);
828-
i32Stack.push(bytes.length);
829+
const id = swift.memory.retain(elem1);
830+
i32Stack.push(_maxUTF8Len(elem1));
829831
i32Stack.push(id);
830832
}
831833
i32Stack.push(elem.length);
@@ -992,9 +994,8 @@ export async function createInstantiator(options, swift) {
992994
}
993995
i32Stack.push(nums.length);
994996
for (const elem1 of strs) {
995-
const bytes = textEncoder.encode(elem1);
996-
const id = swift.memory.retain(bytes);
997-
i32Stack.push(bytes.length);
997+
const id = swift.memory.retain(elem1);
998+
i32Stack.push(_maxUTF8Len(elem1));
998999
i32Stack.push(id);
9991000
}
10001001
i32Stack.push(strs.length);
@@ -1013,9 +1014,8 @@ export async function createInstantiator(options, swift) {
10131014
const isSome1 = b != null;
10141015
if (isSome1) {
10151016
for (const elem1 of b) {
1016-
const bytes = textEncoder.encode(elem1);
1017-
const id = swift.memory.retain(bytes);
1018-
i32Stack.push(bytes.length);
1017+
const id = swift.memory.retain(elem1);
1018+
i32Stack.push(_maxUTF8Len(elem1));
10191019
i32Stack.push(id);
10201020
}
10211021
i32Stack.push(b.length);

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function createInstantiator(options, swift) {
3535
_strEncCache.set(str, encoded);
3636
return encoded;
3737
}
38+
function _maxUTF8Len(str) { return str.length * 3; }
3839
let strStack = [];
3940
let i32Stack = [];
4041
let i64Stack = [];
@@ -63,6 +64,12 @@ export async function createInstantiator(options, swift) {
6364
const bytes = new Uint8Array(memory.buffer, bytesPtr);
6465
bytes.set(source);
6566
}
67+
bjs["swift_js_init_memory_from_string"] = function(stringId, bytesPtr) {
68+
const str = swift.memory.getObject(stringId);
69+
swift.memory.release(stringId);
70+
const target = new Uint8Array(memory.buffer, bytesPtr);
71+
return textEncoder.encodeInto(str, target).written;
72+
}
6673
bjs["swift_js_make_js_string"] = function(ptr, len) {
6774
return swift.memory.retain(decodeString(ptr, len));
6875
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncImport.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function createInstantiator(options, swift) {
3535
_strEncCache.set(str, encoded);
3636
return encoded;
3737
}
38+
function _maxUTF8Len(str) { return str.length * 3; }
3839
let strStack = [];
3940
let i32Stack = [];
4041
let i64Stack = [];
@@ -178,6 +179,12 @@ export async function createInstantiator(options, swift) {
178179
const bytes = new Uint8Array(memory.buffer, bytesPtr);
179180
bytes.set(source);
180181
}
182+
bjs["swift_js_init_memory_from_string"] = function(stringId, bytesPtr) {
183+
const str = swift.memory.getObject(stringId);
184+
swift.memory.release(stringId);
185+
const target = new Uint8Array(memory.buffer, bytesPtr);
186+
return textEncoder.encodeInto(str, target).written;
187+
}
181188
bjs["swift_js_make_js_string"] = function(ptr, len) {
182189
return swift.memory.retain(decodeString(ptr, len));
183190
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/AsyncStaticImport.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function createInstantiator(options, swift) {
3535
_strEncCache.set(str, encoded);
3636
return encoded;
3737
}
38+
function _maxUTF8Len(str) { return str.length * 3; }
3839
let strStack = [];
3940
let i32Stack = [];
4041
let i64Stack = [];
@@ -177,6 +178,12 @@ export async function createInstantiator(options, swift) {
177178
const bytes = new Uint8Array(memory.buffer, bytesPtr);
178179
bytes.set(source);
179180
}
181+
bjs["swift_js_init_memory_from_string"] = function(stringId, bytesPtr) {
182+
const str = swift.memory.getObject(stringId);
183+
swift.memory.release(stringId);
184+
const target = new Uint8Array(memory.buffer, bytesPtr);
185+
return textEncoder.encodeInto(str, target).written;
186+
}
180187
bjs["swift_js_make_js_string"] = function(ptr, len) {
181188
return swift.memory.retain(decodeString(ptr, len));
182189
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DefaultParameters.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function createInstantiator(options, swift) {
4141
_strEncCache.set(str, encoded);
4242
return encoded;
4343
}
44+
function _maxUTF8Len(str) { return str.length * 3; }
4445
let strStack = [];
4546
let i32Stack = [];
4647
let i64Stack = [];
@@ -54,9 +55,8 @@ export async function createInstantiator(options, swift) {
5455
let bjs = null;
5556
const __bjs_createConfigHelpers = () => ({
5657
lower: (value) => {
57-
const bytes = textEncoder.encode(value.name);
58-
const id = swift.memory.retain(bytes);
59-
i32Stack.push(bytes.length);
58+
const id = swift.memory.retain(value.name);
59+
i32Stack.push(_maxUTF8Len(value.name));
6060
i32Stack.push(id);
6161
i32Stack.push((value.value | 0));
6262
i32Stack.push(value.enabled ? 1 : 0);
@@ -105,6 +105,12 @@ export async function createInstantiator(options, swift) {
105105
const bytes = new Uint8Array(memory.buffer, bytesPtr);
106106
bytes.set(source);
107107
}
108+
bjs["swift_js_init_memory_from_string"] = function(stringId, bytesPtr) {
109+
const str = swift.memory.getObject(stringId);
110+
swift.memory.release(stringId);
111+
const target = new Uint8Array(memory.buffer, bytesPtr);
112+
return textEncoder.encodeInto(str, target).written;
113+
}
108114
bjs["swift_js_make_js_string"] = function(ptr, len) {
109115
return swift.memory.retain(decodeString(ptr, len));
110116
}
@@ -576,9 +582,8 @@ export async function createInstantiator(options, swift) {
576582
},
577583
testStringArrayDefault: function bjs_testStringArrayDefault(names = ["a", "b", "c"]) {
578584
for (const elem of names) {
579-
const bytes = textEncoder.encode(elem);
580-
const id = swift.memory.retain(bytes);
581-
i32Stack.push(bytes.length);
585+
const id = swift.memory.retain(elem);
586+
i32Stack.push(_maxUTF8Len(elem));
582587
i32Stack.push(id);
583588
}
584589
i32Stack.push(names.length);

0 commit comments

Comments
 (0)