Copy typed array sets of the same element kind on shared memory as bytes (#2094)

This commit is contained in:
Chris Tate
2026-10-24 09:07:57 -05:00
committed by GitHub
6 changed files with 238 additions and 2 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8106-shared-typed-array-set.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8107-shared-typed-array-set-workers.ts"
],
"diags": []
}
+19 -2
View File
@@ -755,12 +755,29 @@ void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) {
}
size_t width = scr_bytes_elem_size(dst->elem);
if (dst->shared || src->shared) {
/* Convert a private copy of the source, then publish the target range. */
SCR_BYTES_SNAPSHOT(src);
size_t bytes = src->len * width;
uint8_t *target = dst->data + (size_t)t * width;
if (dst->elem == src->elem) {
/* The same element kind copies the source's bytes, NaN payloads
* included. A source range that overlaps the target range (views of
* one buffer, where no snapshot was taken) is copied out first. */
uintptr_t to = (uintptr_t)target, from = (uintptr_t)src->data;
bool overlap = to <= from ? from - to < bytes : to - from < bytes;
if (!overlap) {
scr_bytes_raw_out(dst, target, src->data, bytes);
return;
}
uint8_t *copy = scr_bytes_scratch(bytes);
scr_bytes_raw_in(src, copy, src->data, bytes);
scr_bytes_raw_out(dst, target, copy, bytes);
free(copy);
return;
}
/* Convert a private copy of the source, then publish the target range. */
uint8_t *scratch = scr_bytes_scratch(bytes);
scr_bytes_convert_span(scratch, dst->elem, src->data, src->elem, src->len);
scr_bytes_raw_out(dst, dst->data + (size_t)t * width, scratch, bytes);
scr_bytes_raw_out(dst, target, scratch, bytes);
free(scratch);
return;
}
+47
View File
@@ -37,6 +37,52 @@ static void external_set(void) {
}
}
/* Same-kind sets into and out of shared storage copy raw bytes, NaN
* payloads included, also between overlapping views of one shared buffer. */
static void shared_set(void) {
enum { COUNT = 37 };
for (ScrBytesElem elem = SCR_BYTES_U8; elem <= SCR_BYTES_F16; elem++) {
size_t width = scr_bytes_elem_size(elem), bytes = COUNT * width;
uint8_t pattern[COUNT * 8], expected[COUNT * 8];
for (size_t i = 0; i < bytes; i++) pattern[i] = (uint8_t)(i * 37 + 0x7f);
/* Signaling NaNs, which a conversion through double would quiet. */
const uint16_t nan16 = 0x7d01;
const uint32_t nan32 = 0x7fa00001;
const uint64_t nan64 = UINT64_C(0x7ff4000000000001);
for (size_t i = 1; i < COUNT; i += 6) {
if (elem == SCR_BYTES_F16) memcpy(pattern + i * width, &nan16, width);
if (elem == SCR_BYTES_F32) memcpy(pattern + i * width, &nan32, width);
if (elem == SCR_BYTES_F64) memcpy(pattern + i * width, &nan64, width);
}
for (int direction = 0; direction < 2; direction++) {
ScrBytes *shared = scr_bytes_new(elem, COUNT);
scr_bytes_make_shared(shared);
memcpy(shared->data, pattern, bytes);
ScrBytes *src = direction ? scr_bytes_subarray(shared, 3, COUNT) : scr_bytes_subarray(shared, 0, COUNT - 3);
double offset = direction ? 0 : 3;
memcpy(expected, pattern, bytes);
memmove(expected + (size_t)offset * width, pattern + (direction ? 3 : 0) * width, (COUNT - 3) * width);
scr_bytes_set_from(shared, src, offset);
assert(!scr_exc_pending());
assert(memcmp(shared->data, expected, bytes) == 0);
scr_bytes_release(src);
ScrBytes *local = scr_bytes_new(elem, COUNT);
memcpy(local->data, pattern, bytes);
ScrBytes *head = scr_bytes_subarray(local, 0, COUNT - 1);
scr_bytes_set_from(shared, head, 1);
memcpy(expected + width, pattern, bytes - width);
assert(memcmp(shared->data, expected, bytes) == 0);
memset(local->data, 0, bytes);
scr_bytes_set_from(local, shared, 0);
assert(memcmp(local->data, expected, bytes) == 0);
scr_bytes_release(head);
scr_bytes_release(local);
scr_bytes_release(shared);
}
}
}
static void copied_storage(void) {
/* Same-kind copies preserve NaN payloads, signed zero and ownership. */
const uint64_t bits[] = {UINT64_C(0x7ff8000000000042), UINT64_C(0x8000000000000000),
@@ -79,6 +125,7 @@ static void filled_storage(void) {
void scr_bytes_test_bulk_storage(void) {
long before = scr_bytes_live_count();
external_set();
shared_set();
copied_storage();
filled_storage();
assert(scr_bytes_live_count() == before);
@@ -0,0 +1,97 @@
// TypedArray.prototype.set where the source, the target or both are views of
// a SharedArrayBuffer: same element kinds (bytes copied as is, NaN payloads
// included), different kinds (converted per element), offsets, overlapping
// ranges of one buffer, Buffer targets and out-of-bounds offsets.
function bits(label: string, buffer: ArrayBufferLike): void {
const words = new Uint32Array(buffer);
const out: string[] = [];
for (let i = 0; i < words.length; i++) out.push(words[i]!.toString(16));
console.log(label, out.join(","));
}
const shared = new SharedArrayBuffer(32);
const bytes = new Uint8Array(shared);
// Private source into a shared target, then shared into shared and shared into private.
bytes.set(new Uint8Array([1, 2, 3, 4, 5]), 2);
console.log("private into shared", bytes.subarray(0, 8).join(","));
const other = new Uint8Array(new SharedArrayBuffer(8));
other.set(bytes.subarray(2, 7));
console.log("shared into shared", other.join(","));
const local = new Uint8Array(12);
local.set(other, 3);
console.log("shared into private", local.join(","));
// Overlapping ranges of the same buffer, forward and backward.
const overlap = new Uint8Array(new SharedArrayBuffer(10));
overlap.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
overlap.set(overlap.subarray(0, 6), 3);
console.log("overlap forward", overlap.join(","));
overlap.set(overlap.subarray(4, 10), 1);
console.log("overlap backward", overlap.join(","));
// Same kind, wider elements: a signaling NaN keeps its bits.
const floatBits = new Uint32Array(new SharedArrayBuffer(8));
floatBits[0] = 0x7fa00001;
floatBits[1] = 0xffc00002;
const floatsShared = new Float32Array(floatBits.buffer);
const floatsLocal = new Float32Array(2);
floatsLocal.set(floatsShared);
bits("f32 shared into private", floatsLocal.buffer);
const floatsBack = new Float32Array(new SharedArrayBuffer(8));
floatsBack.set(floatsLocal);
bits("f32 private into shared", floatsBack.buffer);
const doubleBits = new Uint32Array(new SharedArrayBuffer(16));
doubleBits[0] = 5;
doubleBits[1] = 0x7ff40000;
doubleBits[2] = 0;
doubleBits[3] = 0xc0090000;
const doublesShared = new Float64Array(doubleBits.buffer);
const doublesCopy = new Float64Array(new SharedArrayBuffer(16));
doublesCopy.set(doublesShared);
bits("f64 shared into shared", doublesCopy.buffer);
console.log("f64 values", doublesCopy[1]);
const signed = new Int16Array(new SharedArrayBuffer(8));
signed.set(new Int16Array([-32768, -1, 32767]), 1);
console.log("i16 private into shared", signed.join(","));
// Different kinds convert each element.
const clamped = new Uint8ClampedArray(new SharedArrayBuffer(4));
clamped.set(new Int16Array([300, -5, 128, 255]));
console.log("i16 into shared clamped", clamped.join(","));
const wrapped = new Uint8Array(new SharedArrayBuffer(4));
wrapped.set(new Int16Array(signed.buffer, 2, 3));
console.log("shared i16 into shared u8", wrapped.join(","));
const narrowed = new Int8Array(4);
narrowed.set(new Float64Array(doublesShared.buffer, 8, 1));
narrowed.set(new Uint8Array([200, 127]), 2);
console.log("f64 and u8 into i8", narrowed.join(","));
const fromClamped = new Uint8Array(new SharedArrayBuffer(4));
fromClamped.set(clamped);
console.log("shared clamped into shared u8", fromClamped.join(","));
// Buffer views over shared memory take the same paths.
const buf = Buffer.from(shared, 16, 8);
buf.set(Buffer.from("scriptc"), 1);
console.log("buffer", buf.toString("latin1", 1, 8), bytes[17], bytes[23]);
const into = Buffer.alloc(4);
into.set(buf.subarray(1, 4));
console.log("buffer into private", into.toString("hex"));
// Offsets past the end throw before writing.
for (const offset of [-1, 30, 31]) {
try {
bytes.set(new Uint8Array([9, 9]), offset);
console.log("set at", offset, bytes[30], bytes[31]);
} catch (error) {
console.log("set at", offset, (error as Error).name, (error as Error).message);
}
}
try {
floatsBack.set(new Float32Array(3));
} catch (error) {
console.log("too long", (error as Error).name, (error as Error).message);
}
console.log("unchanged", floatsBack.join(","));
@@ -0,0 +1,63 @@
// TypedArray.prototype.set on SharedArrayBuffer views in a program with
// workers: the same cases on the main thread and in a worker (same element
// kinds with NaN payloads, overlapping ranges of one buffer, converted
// kinds), and a worker's set into a buffer that the main thread reads after
// a message.
import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads";
interface Job {
shared: SharedArrayBuffer;
}
function hex(buffer: ArrayBufferLike): string {
const words = new Uint32Array(buffer);
const out: string[] = [];
for (let i = 0; i < words.length; i++) out.push(words[i]!.toString(16));
return out.join(",");
}
function cases(where: string): string[] {
const lines: string[] = [];
const bytes = new Uint8Array(new SharedArrayBuffer(12));
bytes.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
bytes.set(bytes.subarray(0, 8), 4);
lines.push(`${where} overlap forward ${bytes.join(",")}`);
bytes.set(bytes.subarray(5, 12), 0);
lines.push(`${where} overlap backward ${bytes.join(",")}`);
const floatBits = new Uint32Array(new SharedArrayBuffer(8));
floatBits[0] = 0x7f800001;
floatBits[1] = 0x7fc12345;
const local = new Float32Array(2);
local.set(new Float32Array(floatBits.buffer));
const back = new Float32Array(new SharedArrayBuffer(12));
back.set(local, 1);
lines.push(`${where} f32 ${hex(local.buffer)} ${hex(back.buffer)}`);
const halves = new Uint16Array(new SharedArrayBuffer(8));
halves.set(new Uint16Array([65535, 1, 2]), 1);
const signed = new Int16Array(4);
signed.set(halves);
const clamped = new Uint8ClampedArray(new SharedArrayBuffer(4));
clamped.set(signed);
lines.push(`${where} kinds ${halves.join(",")} ${signed.join(",")} ${clamped.join(",")}`);
return lines;
}
if (isMainThread) {
for (const line of cases("main")) console.log(line);
const shared = new SharedArrayBuffer(16);
const job: Job = { shared };
const worker = new Worker(new URL(import.meta.url), { workerData: job });
worker.on("message", (message: string) => {
console.log(message);
console.log("worker wrote", new Uint8Array(shared).join(","));
void worker.terminate();
});
} else {
const { shared } = workerData as Job;
const target = new Uint8Array(shared);
target.set(new Uint8Array([9, 8, 7]), 2);
target.set(target.subarray(2, 5), 10);
parentPort!.postMessage(cases("worker").join("\n"));
}