diff --git a/packages/wasm-api/src/api.ts b/packages/wasm-api/src/api.ts index 0a992a5cb6..abddafc850 100644 --- a/packages/wasm-api/src/api.ts +++ b/packages/wasm-api/src/api.ts @@ -4,8 +4,12 @@ import type { WasmBridge } from "./bridge.js"; /** * Common interface for WASM/JS child APIs which will be used in combination * with a parent {@link WasmBridge}. + * + * @remarks + * The generic type param is optional and only used if the API is requiring + * certain exports declared by WASM module. */ -export interface IWasmAPI { +export interface IWasmAPI { /** * Called by {@link WasmBridge.init} to initialize all child APIs (async) * after the WASM module has been instantiated. If the method returns false @@ -13,7 +17,7 @@ export interface IWasmAPI { * * @param parent */ - init(parent: WasmBridge): Promise; + init(parent: WasmBridge): Promise; /** * Returns an object of this child API's declared WASM imports. Be aware * imports from all child APIs will be merged into a single flat namespace, @@ -22,6 +26,30 @@ export interface IWasmAPI { getImports(): WebAssembly.ModuleImports; } +/** + * Base interface of exports declared by the WASM module. At the very least, the + * module needs to export its memory. + * + * @remarks + * This interface is supposed to be extended with the concrete exports defined + * by your WASM module and is used as generic type param for {@link WasmBridge} + * and any {@link IWasmAPI} bridge modules. These exports can obtained via + * {@link WasmBridge.exports} where they will be stored during the execution of + * {@link WasmBridge.init}. + */ +export interface WasmExports { + /** + * The WASM module's linear memory buffer. The `WasmBridge` automatically + * creates various typed views of that memory. + */ + memory: WebAssembly.Memory; +} + +/** + * Core API of WASM imports defined by the {@link WasmBridge}. The same + * functions are declared as bindings in `/zig/core.zig`. Also see this file for + * documentation of each function... + */ export interface CoreAPI { printI8: Fn; printU8: Fn; diff --git a/packages/wasm-api/src/bridge.ts b/packages/wasm-api/src/bridge.ts index 98cf302c04..4fedba969c 100644 --- a/packages/wasm-api/src/bridge.ts +++ b/packages/wasm-api/src/bridge.ts @@ -1,11 +1,15 @@ import type { FnU2, TypedArray } from "@thi.ng/api"; -import { assert } from "@thi.ng/errors/assert"; +import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; import { U16, U32, U8 } from "@thi.ng/hex"; import type { ILogger } from "@thi.ng/logger"; import { ConsoleLogger } from "@thi.ng/logger/console"; -import type { CoreAPI, IWasmAPI } from "./api.js"; +import type { CoreAPI, IWasmAPI, WasmExports } from "./api.js"; -export class WasmBridge { +/** + * The main interop API bridge between the JS host environment and a WebAssembly + * module. This class provides a small core API + */ +export class WasmBridge { i8!: Int8Array; u8!: Uint8Array; i16!: Int16Array; @@ -17,9 +21,10 @@ export class WasmBridge { utf8Decoder: TextDecoder = new TextDecoder(); utf8Encoder: TextEncoder = new TextEncoder(); core: CoreAPI; + exports!: T; constructor( - public modules: Record = {}, + public modules: Record> = {}, public logger: ILogger = new ConsoleLogger("wasm") ) { const logN = (x: number) => this.logger.debug(x); @@ -56,15 +61,24 @@ export class WasmBridge { }; } - async init(mem: WebAssembly.Memory) { - this.i8 = new Int8Array(mem.buffer); - this.u8 = new Uint8Array(mem.buffer); - this.i16 = new Int16Array(mem.buffer); - this.u16 = new Uint16Array(mem.buffer); - this.i32 = new Int32Array(mem.buffer); - this.u32 = new Uint32Array(mem.buffer); - this.f32 = new Float32Array(mem.buffer); - this.f64 = new Float64Array(mem.buffer); + /** + * Receives the WASM module's exports, stores the for future reference and + * then initializes all declared bridge child API modules. Returns false if + * any of the module initializations failed. + * + * @param exports + */ + async init(exports: T) { + this.exports = exports; + const buf = exports.memory.buffer; + 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.f32 = new Float32Array(buf); + this.f64 = new Float64Array(buf); for (let id in this.modules) { this.logger.debug(`initializing API module: ${id}`); const status = await this.modules[id].init(this); @@ -74,53 +88,68 @@ export class WasmBridge { } /** - * Returns object of all WASM imports declared in the bridge core API and - * any provided child APIs. + * 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 + * Since all declared imports will be merged into a single flat namespace, + * it's recommended to use per-module naming prefixes to avoid clashes. If + * there're any naming clashes, this function will throw an error. */ getImports() { - const env = { ...this.core }; + const env: WebAssembly.ModuleImports = { ...this.core }; for (let id in this.modules) { - Object.assign(env, this.modules[id].getImports()); + const imports = this.modules[id].getImports(); + // check for naming clashes + for (let k in imports) { + if (env[k] !== undefined) { + illegalArgs( + `attempt to redeclare import: ${k} by API module ${id}` + ); + } + } + Object.assign(env, imports); } return { env }; } - getI8Array(ptr: number, len: number): Int8Array { - return this.i8.subarray(ptr, ptr + len); + getI8Array(addr: number, len: number): Int8Array { + return this.i8.subarray(addr, addr + len); } - getU8Array(ptr: number, len: number): Uint8Array { - return this.u8.subarray(ptr, ptr + len); + getU8Array(addr: number, len: number): Uint8Array { + return this.u8.subarray(addr, addr + len); } - getI16Array(ptr: number, len: number): Int16Array { - ptr >>= 1; - return this.i16.subarray(ptr, ptr + len); + getI16Array(addr: number, len: number): Int16Array { + addr >>= 1; + return this.i16.subarray(addr, addr + len); } - getU16Array(ptr: number, len: number): Uint16Array { - ptr >>= 1; - return this.u16.subarray(ptr, ptr + len); + getU16Array(addr: number, len: number): Uint16Array { + addr >>= 1; + return this.u16.subarray(addr, addr + len); } - getI32Array(ptr: number, len: number): Int32Array { - ptr >>= 2; - return this.i32.subarray(ptr, ptr + len); + getI32Array(addr: number, len: number): Int32Array { + addr >>= 2; + return this.i32.subarray(addr, addr + len); } - getU32Array(ptr: number, len: number): Uint32Array { - ptr >>= 2; - return this.u32.subarray(ptr, ptr + len); + getU32Array(addr: number, len: number): Uint32Array { + addr >>= 2; + return this.u32.subarray(addr, addr + len); } - getF32Array(ptr: number, len: number): Float32Array { - ptr >>= 2; - return this.f32.subarray(ptr, ptr + len); + getF32Array(addr: number, len: number): Float32Array { + addr >>= 2; + return this.f32.subarray(addr, addr + len); } - getF64Array(ptr: number, len: number): Float64Array { - ptr >>= 3; - return this.f64.subarray(ptr, ptr + len); + getF64Array(addr: number, len: number): Float64Array { + addr >>= 3; + return this.f64.subarray(addr, addr + len); } derefI8(ptr: number) { @@ -167,7 +196,7 @@ export class WasmBridge { getElementById(addr: number, len = 0) { const id = this.getString(addr, len); const el = document.getElementById(id); - assert(!!el, `missing DOM element #${id}`); + el == null && illegalArgs(`missing DOM element #${id}`); return el; } @@ -182,10 +211,9 @@ export class WasmBridge { str, this.u8.subarray(addr, addr + maxBytes) ).written!; - assert( - len != null && len < maxBytes + (terminate ? 0 : 1), - `error writing string to 0x${U32(addr)}` - ); + if (len != null && len < maxBytes + (terminate ? 0 : 1)) { + illegalArgs(`error writing string to 0x${U32(addr)}`); + } if (terminate) { this.u8[addr + len!] = 0; return len! + 1;