UNPKG

@thi.ng/wasm-api

Version:

Generic, modular, extensible API bridge and infrastructure for hybrid JS & WebAssembly projects

470 lines (469 loc) 13.3 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; import { INotifyMixin } from "@thi.ng/api/mixins/inotify"; import { topoSort } from "@thi.ng/arrays/topo-sort"; import { assert } from "@thi.ng/errors/assert"; import { defError } from "@thi.ng/errors/deferror"; import { U16, U32, U64BIG, U8, hexdumpLines } from "@thi.ng/hex"; import { ConsoleLogger } from "@thi.ng/logger/console"; import { EVENT_MEMORY_CHANGED, EVENT_PANIC } from "./api.js"; const Panic = defError(() => "Panic"); const OutOfMemoryError = defError(() => "Out of memory"); let WasmBridge = class { constructor(modules = [], logger = new ConsoleLogger("wasm")) { this.logger = logger; const logN = (x) => this.logger.debug(x); const logA = (method) => (addr, len) => this.logger.debug(() => method(addr, len).join(", ")); this.api = { printI8: logN, printU8: logN, printI16: logN, printU16: logN, printI32: logN, printU32: (x) => this.logger.debug(x >>> 0), printI64: (x) => this.logger.debug(x), printU64: (x) => this.logger.debug(x), printF32: logN, printF64: logN, printU8Hex: (x) => this.logger.debug(() => `0x${U8(x)}`), printU16Hex: (x) => this.logger.debug(() => `0x${U16(x)}`), printU32Hex: (x) => this.logger.debug(() => `0x${U32(x)}`), printU64Hex: (x) => this.logger.debug(() => `0x${U64BIG(x)}`), printHexdump: (addr, len) => { this.ensureMemory(); for (let line of hexdumpLines(this.u8, addr, len)) { this.logger.debug(line); } }, _printI8Array: logA(this.getI8Array.bind(this)), _printU8Array: logA(this.getU8Array.bind(this)), _printI16Array: logA(this.getI16Array.bind(this)), _printU16Array: logA(this.getU16Array.bind(this)), _printI32Array: logA(this.getI32Array.bind(this)), _printU32Array: logA(this.getU32Array.bind(this)), _printI64Array: logA(this.getI64Array.bind(this)), _printU64Array: logA(this.getU64Array.bind(this)), _printF32Array: logA(this.getF32Array.bind(this)), _printF64Array: logA(this.getF64Array.bind(this)), printStrZ: (addr) => this.logger.debug(() => this.getString(addr, 0)), _printStr: (addr, len) => this.logger.debug(() => this.getString(addr, len)), debug: () => { debugger; }, _panic: (addr, len) => { const msg = this.getString(addr, len); if (!this.notify({ id: EVENT_PANIC, value: msg })) { throw new Panic(msg); } }, timer: () => performance.now(), epoch: () => BigInt(Date.now()) }; this._buildModuleGraph(modules); } id = "wasmapi"; i8; u8; i16; u16; i32; u32; i64; u64; f32; f64; utf8Decoder = new TextDecoder(); utf8Encoder = new TextEncoder(); imports; exports; api; modules; order; /** * Takes array of root module specs, extracts all transitive dependencies, * pre-computes their topological order, then calls * {@link WasmModuleSpec.factory} for each module and stores all modules for * future reference. * * @remarks * Note: The pre-instantiated modules will only be fully initialized later * via {@link WasmBridge.instantiate} or {@link WasmBridge.init}. * * @param specs */ _buildModuleGraph(specs) { const unique = /* @__PURE__ */ new Set(); const queue = [...specs]; while (queue.length) { const mod = queue.shift(); unique.add(mod); if (!mod.deps) continue; for (let d of mod.deps) { if (!unique.has(d)) queue.push(d); } } const graph = [...unique].reduce((acc, mod) => { assert( (acc[mod.id] === void 0 || acc[mod.id] === mod) && mod.id !== this.id, `duplicate API module ID: ${mod.id}` ); acc[mod.id] = mod; return acc; }, {}); this.order = topoSort(graph, (mod) => mod.deps?.map((x) => x.id)); this.modules = this.order.reduce((acc, id) => { acc[id] = graph[id].factory(this); return acc; }, {}); } /** * Instantiates WASM module from given `src` (and optional provided extra * imports), then automatically calls {@link WasmBridge.init} with the * modules exports. * * @remarks * If the given `src` is a `Response` or `Promise<Response>`, the module * will be instantiated via `WebAssembly.instantiateStreaming()`, otherwise * the non-streaming version will be used. * * @param src * @param imports */ async instantiate(src, imports) { const $src = await src; const $imports = { ...this.getImports(), ...imports }; const wasm = await ($src instanceof Response ? WebAssembly.instantiateStreaming($src, $imports) : WebAssembly.instantiate($src, $imports)); return this.init(wasm.instance.exports); } /** * Receives the WASM module's combined exports, stores them for future * reference and then initializes all declared bridge child API modules in * their stated dependency order. Returns false if any of the module * initializations failed. * * @remarks * Emits the {@link EVENT_MEMORY_CHANGED} event just before returning (and * AFTER all child API modules have been initialized). * * @param exports */ async init(exports) { this.exports = exports; this.ensureMemory(false); for (let id of this.order) { this.logger.debug(`initializing API module: ${id}`); const status = await this.modules[id].init(this); if (!status) return false; } this.notify({ id: EVENT_MEMORY_CHANGED, value: this.exports.memory }); return true; } /** * Called automatically during initialization and from other memory * accessors. Initializes and/or updates the various typed WASM memory views * (e.g. after growing the WASM memory and the previous buffer becoming * detached). Unless `notify` is false, the {@link EVENT_MEMORY_CHANGED} * event will be emitted if the memory views had to be updated. * * @param notify */ ensureMemory(notify = true) { const buf = this.exports.memory.buffer; if (this.u8?.buffer === buf) return; this.i8 = new Int8Array(buf); this.u8 = new Uint8Array(buf); this.i16 = new Int16Array(buf); this.u16 = new Uint16Array(buf); this.i32 = new Int32Array(buf); this.u32 = new Uint32Array(buf); this.i64 = new BigInt64Array(buf); this.u64 = new BigUint64Array(buf); this.f32 = new Float32Array(buf); this.f64 = new Float64Array(buf); notify && this.notify({ id: EVENT_MEMORY_CHANGED, value: this.exports.memory }); } /** * Required use for WASM module instantiation to provide JS imports to the * module. Returns an object of all WASM imports declared by the bridge core * API and any provided bridge API modules. * * @remarks * Each API module's imports will be in their own WASM import object/table, * named using the same key which is defined by the JS side of the module * via {@link WasmModuleSpec.id}. The bridge's core API is named `wasmapi` * and is reserved. * * @example * The following creates a bridge with a fictional `custom` API module: * * ```ts * import { WasmBridge } from "@thi.ng/wasm-api"; * * const bridge = new WasmBridge([new CustomAPI()]); * * // get combined imports object * bridge.getImports(); * { * // imports defined by the core API of the bridge itself * wasmapi: { ... }, * // imports defined by the CustomAPI module * custom: { ... } * } * ``` * * Any related API bindings on the WASM (Zig) side then also need to refer * to these custom import sections (also see `/zig/core.zig`): * * ```zig * pub export "custom" fn foo(x: u32) void; * ``` */ getImports() { if (!this.imports) { this.imports = { [this.id]: this.api }; for (let id in this.modules) { this.imports[id] = this.modules[id].getImports(); } } return this.imports; } growMemory(numPages) { this.exports.memory.grow(numPages); this.ensureMemory(); } allocate(numBytes, clear = false) { const addr = this.exports._wasm_allocate(numBytes); if (!addr) throw new OutOfMemoryError(`unable to allocate: ${numBytes}`); this.logger.fine( () => `allocated ${numBytes} bytes @ 0x${U32(addr)} .. 0x${U32( addr + numBytes - 1 )}` ); this.ensureMemory(); clear && this.u8.fill(0, addr, addr + numBytes); return [addr, numBytes]; } free([addr, numBytes]) { this.logger.fine( () => `freeing memory @ 0x${U32(addr)} .. 0x${U32( addr + numBytes - 1 )}` ); this.exports._wasm_free(addr, numBytes); } getI8(addr) { return this.i8[addr]; } getU8(addr) { return this.u8[addr]; } getI16(addr) { return this.i16[addr >> 1]; } getU16(addr) { return this.u16[addr >> 1]; } getI32(addr) { return this.i32[addr >> 2]; } getU32(addr) { return this.u32[addr >> 2]; } getI64(addr) { return this.i64[addr >> 3]; } getU64(addr) { return this.u64[addr >> 3]; } getF32(addr) { return this.f32[addr >> 2]; } getF64(addr) { return this.f64[addr >> 3]; } setI8(addr, x) { this.i8[addr] = x; return this; } setU8(addr, x) { this.u8[addr] = x; return this; } setI16(addr, x) { this.i16[addr >> 1] = x; return this; } setU16(addr, x) { this.u16[addr >> 1] = x; return this; } setI32(addr, x) { this.i32[addr >> 2] = x; return this; } setU32(addr, x) { this.u32[addr >> 2] = x; return this; } setI64(addr, x) { this.i64[addr >> 3] = x; return this; } setU64(addr, x) { this.u64[addr >> 3] = x; return this; } setF32(addr, x) { this.f32[addr >> 2] = x; return this; } setF64(addr, x) { this.f64[addr >> 3] = x; return this; } getI8Array(addr, len) { return this.i8.subarray(addr, addr + len); } getU8Array(addr, len) { return this.u8.subarray(addr, addr + len); } getI16Array(addr, len) { addr >>= 1; return this.i16.subarray(addr, addr + len); } getU16Array(addr, len) { addr >>= 1; return this.u16.subarray(addr, addr + len); } getI32Array(addr, len) { addr >>= 2; return this.i32.subarray(addr, addr + len); } getU32Array(addr, len) { addr >>= 2; return this.u32.subarray(addr, addr + len); } getI64Array(addr, len) { addr >>= 3; return this.i64.subarray(addr, addr + len); } getU64Array(addr, len) { addr >>= 3; return this.u64.subarray(addr, addr + len); } getF32Array(addr, len) { addr >>= 2; return this.f32.subarray(addr, addr + len); } getF64Array(addr, len) { addr >>= 3; return this.f64.subarray(addr, addr + len); } setI8Array(addr, buf) { this.i8.set(buf, addr); return this; } setU8Array(addr, buf) { this.u8.set(buf, addr); return this; } setI16Array(addr, buf) { this.i16.set(buf, addr >> 1); return this; } setU16Array(addr, buf) { this.u16.set(buf, addr >> 1); return this; } setI32Array(addr, buf) { this.i32.set(buf, addr >> 2); return this; } setU32Array(addr, buf) { this.u32.set(buf, addr >> 2); return this; } setI64Array(addr, buf) { this.i64.set(buf, addr >> 3); return this; } setU64Array(addr, buf) { this.u64.set(buf, addr >> 3); return this; } setF32Array(addr, buf) { this.f32.set(buf, addr >> 2); return this; } setF64Array(addr, buf) { this.f64.set(buf, addr >> 3); return this; } getString(addr, len = 0) { this.ensureMemory(); return this.utf8Decoder.decode( this.u8.subarray( addr, len > 0 ? addr + len : this.u8.indexOf(0, addr) ) ); } setString(str, addr, maxBytes, terminate = true) { this.ensureMemory(); maxBytes = Math.min(maxBytes, this.u8.length - addr); const len = this.utf8Encoder.encodeInto( str, this.u8.subarray(addr, addr + maxBytes) ).written; assert( len != null && len < maxBytes + (terminate ? 0 : 1), `error writing string to 0x${U32( addr )} (max. ${maxBytes} bytes, got at least ${str.length})` ); if (terminate) { this.u8[addr + len] = 0; } return len; } getElementById(addr, len = 0) { const id = this.getString(addr, len); const el = document.getElementById(id); assert(!!el, `missing DOM element #${id}`); return el; } // @ts-ignore: mixin // prettier-ignore addListener(id, fn, scope) { } // @ts-ignore: mixin // prettier-ignore removeListener(id, fn, scope) { } // @ts-ignore: mixin notify(event) { } }; WasmBridge = __decorateClass([ INotifyMixin ], WasmBridge); export { OutOfMemoryError, Panic, WasmBridge };