// A small reader for the parts of a WebAssembly binary the reach build needs: the // function signatures of every import and export. It reads the type, import, // function and export sections and skips the rest, so the build does not depend on // any tool's text format. // // import { readModule } from './wasm.mjs'; // const m = readModule(bytes); // m.imports [{ module, name, kind, params?, results? }] // m.exports [{ name, kind, params?, results? }] kind: func | table | memory | global | tag const VALTYPE = { 0x7f: 'i32', 0x7e: 'i64', 0x7d: 'f32', 0x7c: 'f64', 0x7b: 'v128', 0x70: 'funcref', 0x6f: 'externref' }; const KIND = ['func', 'table', 'memory', 'global', 'tag']; export function readModule(bytes) { const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); let p = 0; const fail = (msg) => { throw new Error(`wasm: ${msg} at byte ${p}`); }; const u8 = () => (p < b.length ? b[p++] : fail('unexpected end')); const leb = () => { let result = 0, shift = 0, byte; do { byte = u8(); result += (byte & 0x7f) * 2 ** shift; shift += 7; if (shift > 35) fail('LEB128 too long'); } while (byte & 0x80); return result; }; const name = () => { const n = leb(); if (p + n > b.length) fail('name runs past the end'); const s = new TextDecoder('utf-8', { fatal: true }).decode(b.subarray(p, p + n)); p += n; return s; }; const valtype = () => { const t = u8(); return VALTYPE[t] ?? fail(`value type 0x${t.toString(16)} not read here`); }; const limits = () => { const flag = u8(); if (flag > 7) fail(`limits flag 0x${flag.toString(16)}`); leb(); if (flag & 1) leb(); }; if (b.length < 8 || b[0] !== 0 || b[1] !== 0x61 || b[2] !== 0x73 || b[3] !== 0x6d) fail('not a WebAssembly binary'); if (b[4] !== 1 || b[5] !== 0 || b[6] !== 0 || b[7] !== 0) fail('not version 1'); p = 8; const types = []; const imports = []; const funcTypes = []; // type index of every function, imported ones first const exports = []; while (p < b.length) { const id = u8(); const size = leb(); const end = p + size; if (end > b.length) fail(`section ${id} runs past the end`); if (id === 1) { const n = leb(); for (let i = 0; i < n; i++) { const form = u8(); if (form !== 0x60) fail(`type form 0x${form.toString(16)} not read here`); const params = Array.from({ length: leb() }, valtype); const results = Array.from({ length: leb() }, valtype); types.push({ params, results }); } } else if (id === 2) { const n = leb(); for (let i = 0; i < n; i++) { const module = name(), field = name(), kind = u8(); if (kind === 0) { const t = leb(); funcTypes.push(t); imports.push({ module, name: field, kind: 'func', ...types[t] }); } else if (kind === 1) { u8(); limits(); imports.push({ module, name: field, kind: 'table' }); } else if (kind === 2) { limits(); imports.push({ module, name: field, kind: 'memory' }); } else if (kind === 3) { valtype(); u8(); imports.push({ module, name: field, kind: 'global' }); } else if (kind === 4) { u8(); leb(); imports.push({ module, name: field, kind: 'tag' }); } else fail(`import kind ${kind}`); } } else if (id === 3) { const n = leb(); for (let i = 0; i < n; i++) funcTypes.push(leb()); } else if (id === 7) { const n = leb(); for (let i = 0; i < n; i++) { const field = name(), kind = u8(), index = leb(); const e = { name: field, kind: KIND[kind] ?? fail(`export kind ${kind}`), index }; if (kind === 0) Object.assign(e, types[funcTypes[index]] ?? fail(`export ${field}: no type for function ${index}`)); exports.push(e); } } p = end; } return { types, imports, exports }; } // The i64 boundary of a module: every function import and export with an i64 // parameter or result. The reach runtime splits and joins these values at the JS // boundary (an i64 crosses as a (low, high) pair of i32 values, and a result's high // half travels through env.setTempRet0). A multi-value result is refused: neither // legalisation nor the runtime handles one. export function i64Boundary(m) { const sig = (f) => { if (f.results.length > 1) throw new Error(`${f.name}: a multi-value result is not supported`); return { params: f.params, result: f.results[0] ?? null }; }; const has64 = (f) => f.params.includes('i64') || f.results.includes('i64'); const exports = {}, imports = {}; for (const e of m.exports) if (e.kind === 'func' && has64(e)) exports[e.name] = sig(e); for (const i of m.imports) { if (i.kind !== 'func' || !has64(i)) continue; if (imports[i.name]) throw new Error(`import ${i.name} appears under two modules`); imports[i.name] = { module: i.module, ...sig(i) }; } return { exports, imports }; }