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