bridge-common.mjs raw
1 // bridge-common.mjs - shared wasm bridge infrastructure.
2 //
3 // Exports utility functions and a factory for the common bridge functions
4 // that appear identically in every wasm Worker host: SAB channel ops,
5 // spawn_domain, is_worker_context, subtle_random_bytes, core helpers, WASI.
6
7 // ── SAB ring buffer ────────────────────────────────────────────────────────
8
9 export const SAB_HDR = 32;
10 export const SAB_SLOT_HDR = 12;
11
12 export function sabCreate(slotSize, slotCount) {
13 const sab = new SharedArrayBuffer(SAB_HDR + slotCount * slotSize);
14 const u32 = new Uint32Array(sab);
15 u32[2] = slotSize;
16 u32[3] = slotCount;
17 return sab;
18 }
19
20 // forceClose sets the closed flag and wakes all blocked waiters.
21 // No sentinel slot needed: send/recv wait loops check bit0 on every wakeup.
22 export function sabForceClose(sab) {
23 const u32 = new Uint32Array(sab);
24 Atomics.or(u32, 4, 1);
25 Atomics.notify(u32, 0, Atomics.MAX_VALUE);
26 Atomics.notify(u32, 1, Atomics.MAX_VALUE);
27 }
28
29 export function sabSend(sab, bytes) {
30 const u32 = new Uint32Array(sab);
31 const slotSize = u32[2], slotCount = u32[3];
32 const payloadCap = slotSize - SAB_SLOT_HDR;
33 if (bytes.length > (slotCount - 1) * payloadCap)
34 throw new Error('channel_send: overflow');
35 let offset = 0, first = true;
36 while (offset <= bytes.length) {
37 const chunkLen = Math.min(bytes.length - offset, payloadCap);
38 const isFinal = (offset + chunkLen >= bytes.length) ? 1 : 0;
39 let wi = Atomics.load(u32, 0);
40 while (wi - Atomics.load(u32, 1) >= slotCount) {
41 Atomics.wait(u32, 1, Atomics.load(u32, 1));
42 wi = Atomics.load(u32, 0);
43 if (Atomics.load(u32, 4) & 1) throw new Error('channel_send: send on closed channel');
44 }
45 if (Atomics.load(u32, 4) & 1) throw new Error('channel_send: send on closed channel');
46 const base = SAB_HDR + (wi & (slotCount - 1)) * slotSize;
47 const dv = new DataView(sab, base, slotSize);
48 dv.setUint8(0, isFinal);
49 dv.setUint8(1, 0); dv.setUint8(2, 0); dv.setUint8(3, 0);
50 dv.setUint32(4, first ? bytes.length : 0, true);
51 dv.setUint32(8, chunkLen, true);
52 if (chunkLen > 0)
53 new Uint8Array(sab, base + SAB_SLOT_HDR, chunkLen)
54 .set(bytes.subarray(offset, offset + chunkLen));
55 Atomics.store(u32, 0, wi + 1);
56 Atomics.notify(u32, 0, 1);
57 offset += chunkLen;
58 first = false;
59 if (isFinal) break;
60 }
61 }
62
63 export function sabRecv(sab, dstBuf) {
64 const u32 = new Uint32Array(sab);
65 const slotSize = u32[2], slotCount = u32[3];
66 let totalLen = -1, dstOffset = 0, awaitingFirst = true;
67 for (;;) {
68 let ri = Atomics.load(u32, 1);
69 while (Atomics.load(u32, 0) === ri) {
70 Atomics.wait(u32, 0, ri);
71 ri = Atomics.load(u32, 1);
72 if (Atomics.load(u32, 4) & 1) return -1;
73 }
74 const base = SAB_HDR + (ri & (slotCount - 1)) * slotSize;
75 const dv = new DataView(sab, base, slotSize);
76 const isFinal = dv.getUint8(0);
77 const slotTotalLen = dv.getUint32(4, true);
78 const chunkLen = dv.getUint32(8, true);
79 Atomics.store(u32, 1, ri + 1);
80 Atomics.notify(u32, 1, 1);
81 if (awaitingFirst) {
82 if (slotTotalLen === 0)
83 return (Atomics.load(u32, 4) & 1) ? -1 : 0;
84 if (slotTotalLen > dstBuf.length)
85 throw new Error('channel_recv: overflow');
86 totalLen = slotTotalLen;
87 awaitingFirst = false;
88 }
89 if (chunkLen > 0) {
90 dstBuf.set(new Uint8Array(sab, base + SAB_SLOT_HDR, chunkLen), dstOffset);
91 dstOffset += chunkLen;
92 }
93 if (isFinal) return totalLen;
94 }
95 }
96
97 export function sabClose(sab) {
98 const u32 = new Uint32Array(sab);
99 Atomics.or(u32, 4, 1);
100 const slotSize = u32[2], slotCount = u32[3];
101 let wi = Atomics.load(u32, 0);
102 while (wi - Atomics.load(u32, 1) >= slotCount) {
103 Atomics.wait(u32, 1, Atomics.load(u32, 1));
104 wi = Atomics.load(u32, 0);
105 }
106 const base = SAB_HDR + (wi & (slotCount - 1)) * slotSize;
107 const dv = new DataView(sab, base, slotSize);
108 dv.setUint8(0, 1); dv.setUint32(4, 0, true); dv.setUint32(8, 0, true);
109 Atomics.store(u32, 0, wi + 1);
110 Atomics.notify(u32, 0, 1);
111 }
112
113 // ── fromSlice ──────────────────────────────────────────────────────────────
114
115 export function fromSlice(s) {
116 if (s instanceof Uint8Array) return s;
117 if (s && s.$array != null) {
118 const u = new Uint8Array(s.$length);
119 for (let i = 0; i < s.$length; i++) u[i] = s.$array[s.$offset + i];
120 return u;
121 }
122 if (s && typeof s === 'string') return new TextEncoder().encode(s);
123 return new Uint8Array(0);
124 }
125
126 // ── Core helpers factory ───────────────────────────────────────────────────
127
128 // makeCoreHelpers returns memory-access helpers that close over getMem/getXp.
129 // Call after building the bridge object; the getters provide the live wasm
130 // instance so helpers always see the current memory even after wasm growth.
131 export function makeCoreHelpers(getMem, getXp) {
132 const enc = new TextEncoder();
133 const dec = new TextDecoder();
134
135 function readStr(ptr, len) {
136 if (len <= 0) return '';
137 return dec.decode(new Uint8Array(getMem().buffer, ptr >>> 0, len));
138 }
139 function readBytes(ptr, len) {
140 if (len <= 0) return new Uint8Array(0);
141 return new Uint8Array(getMem().buffer, ptr >>> 0, len);
142 }
143 function writeStr(s) {
144 const bytes = enc.encode('' + s);
145 const ptr = getXp().__alloc(bytes.length) >>> 0;
146 new Uint8Array(getMem().buffer, ptr, bytes.length).set(bytes);
147 return [ptr, bytes.length];
148 }
149 function writeI32(addr, val) {
150 new DataView(getMem().buffer).setInt32(addr >>> 0, val, true);
151 }
152 function writeBytes(data) {
153 const u = fromSlice(data);
154 const ptr = getXp().__alloc(u.length) >>> 0;
155 new Uint8Array(getMem().buffer, ptr, u.length).set(u);
156 return [ptr, u.length];
157 }
158 function cb0(id) { getXp().__cb0(id); }
159 function cbs(id, s) {
160 const [ptr, len] = writeStr(s);
161 getXp().__cbs(id, ptr, len);
162 }
163 function cbdata(id, data) {
164 const [ptr, len] = writeBytes(data);
165 getXp().__cbdata(id, ptr, len);
166 }
167
168 return { readStr, readBytes, writeStr, writeI32, writeBytes, cb0, cbs, cbdata };
169 }
170
171 // ── WASI ───────────────────────────────────────────────────────────────────
172
173 export function makeWasi(getMem) {
174 const dec = new TextDecoder();
175 return {
176 fd_write(fd, iovs, iovs_len, nwritten_ptr) {
177 const dv = new DataView(getMem().buffer);
178 let total = 0;
179 const iovBase = iovs >>> 0;
180 for (let i = 0; i < iovs_len; i++) {
181 const ptr = dv.getUint32(iovBase + i * 8, true);
182 const len = dv.getUint32(iovBase + i * 8 + 4, true);
183 const s = dec.decode(new Uint8Array(getMem().buffer, ptr, len));
184 if (fd === 1) console.log(s);
185 else if (fd === 2) console.error(s);
186 total += len;
187 }
188 dv.setUint32(nwritten_ptr >>> 0, total, true);
189 return 0;
190 },
191 clock_time_get(clock_id, precision, time_ptr) {
192 const dv = new DataView(getMem().buffer);
193 const ms = (clock_id === 1) ? performance.now() : Date.now();
194 const ns = BigInt(Math.trunc(ms * 1e6));
195 dv.setBigUint64(time_ptr >>> 0, ns, true);
196 return 0;
197 },
198 };
199 }
200
201 // ── Build hash ─────────────────────────────────────────────────────────────
202
203 export async function computeBuildHash(wasmBytes) {
204 const buf = await crypto.subtle.digest('SHA-256', wasmBytes);
205 return Array.from(new Uint8Array(buf))
206 .map(b => b.toString(16).padStart(2, '0')).join('');
207 }
208
209 // ── Spawned Worker lifecycle ───────────────────────────────────────────────
210
211 export function createSpawnedWorker(wasmUrl, buildHash, fnIdx, argBytes, chanSabs, spawnedWorkerChans) {
212 const worker = new Worker(new URL(wasmUrl), { type: 'module' });
213 worker.postMessage({ type: 'init', mode: 'spawn', wasmUrl, buildHash, fnIdx, argBytes, chanSabs });
214 spawnedWorkerChans.set(worker, chanSabs);
215 worker.onmessage = function(e) {
216 if (e.data && e.data.type === 'exit') spawnedWorkerChans.delete(worker);
217 };
218 worker.onerror = function(e) {
219 const sabs = spawnedWorkerChans.get(worker);
220 spawnedWorkerChans.delete(worker);
221 console.error('[spawn] child Worker died:', e.message,
222 'at', e.filename + ':' + e.lineno,
223 e.error && e.error.stack ? '\n' + e.error.stack : '');
224 if (sabs) sabs.forEach(sabForceClose);
225 };
226 worker.onmessageerror = function() {
227 const sabs = spawnedWorkerChans.get(worker);
228 spawnedWorkerChans.delete(worker);
229 if (sabs) sabs.forEach(sabForceClose);
230 };
231 }
232
233 // ── Common bridge factory ──────────────────────────────────────────────────
234
235 // makeCommonBridge returns the bridge functions shared by all wasm Worker
236 // hosts. The host merges these with its own specific functions (ext_*, dom_*,
237 // etc.) to form the complete bridge object passed to WebAssembly.instantiate.
238 //
239 // h: the result of makeCoreHelpers(getMem, getXp)
240 // sabTable: Map<int32, SharedArrayBuffer> - handle table (mutated by channel_create)
241 // sabSeqRef: { value: int32 } - next handle sequence number (mutable)
242 // wasmUrlRef: { value: string|null } - set during wasm boot
243 // buildHashRef: { value: string|null } - set during wasm boot
244 // spawnedWorkerChans: Map<Worker, SAB[]> - dead-Worker cleanup tracking
245 export function makeCommonBridge(h, sabTable, sabSeqRef, wasmUrlRef, buildHashRef, spawnedWorkerChans) {
246 const { readStr, readBytes, writeBytes, writeI32, cbs, cbdata } = h;
247
248 return {
249 // --- runtime context ---
250 is_worker_context() { return 1; },
251
252 // --- SAB channels ---
253 channel_create(slotSize, slotCount) {
254 const sab = sabCreate(slotSize, slotCount);
255 const handle = sabSeqRef.value++;
256 sabTable.set(handle, sab);
257 return handle;
258 },
259 channel_send(handle, srcPtr, srcLen, srcCap) {
260 const sab = sabTable.get(handle);
261 if (!sab) throw new Error('channel_send: invalid handle ' + handle);
262 // .slice() copies bytes out before the Atomics.wait in sabSend can
263 // conflict with wasm advancing the memory pointer during a GC.
264 sabSend(sab, readBytes(srcPtr, srcLen).slice());
265 },
266 channel_recv(handle, dstPtr, dstCap, cap) {
267 const sab = sabTable.get(handle);
268 if (!sab) return -1;
269 const dst = new Uint8Array(dstCap);
270 const n = sabRecv(sab, dst);
271 if (n > 0) {
272 // readBytes returns a writable view into wasm linear memory at dstPtr.
273 // set() writes the received bytes directly to that address.
274 readBytes(dstPtr, n).set(dst.subarray(0, n));
275 }
276 return n;
277 },
278 channel_close(handle) {
279 const sab = sabTable.get(handle);
280 if (sab) sabClose(sab);
281 },
282
283 // --- spawn ---
284 spawn_domain(fnIdx, argPtr, argLen, chanHandlesPtr, nChans) {
285 if (!wasmUrlRef.value) throw new Error('spawn_domain: wasmUrl not set');
286 const chanSabs = [];
287 for (let i = 0; i < nChans; i++) {
288 // readBytes returns a view; DataView reads the int32 handle at each offset.
289 const handleData = readBytes(chanHandlesPtr + i * 4, 4);
290 const hh = new DataView(handleData.buffer, handleData.byteOffset, 4).getInt32(0, true);
291 const sab = sabTable.get(hh);
292 if (sab) chanSabs.push(sab);
293 }
294 const argBytes = argLen > 0 ? readBytes(argPtr, argLen).slice() : new Uint8Array(0);
295 createSpawnedWorker(wasmUrlRef.value, buildHashRef.value, fnIdx, argBytes, chanSabs, spawnedWorkerChans);
296 },
297
298 // --- time ---
299 timezone_offset_minutes() {
300 return new Date().getTimezoneOffset();
301 },
302
303 // --- subtle ---
304 subtle_random_bytes(ptr, len, cap) {
305 crypto.getRandomValues(readBytes(ptr, len));
306 },
307 };
308 }
309