@thi.ng/wasm-api
Version:
Generic, modular, extensible API bridge and infrastructure for hybrid JS & WebAssembly projects
470 lines (469 loc) • 13.3 kB
JavaScript
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 (const 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 (const 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 (const 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 (const 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
};