Skip to content

Commit

Permalink
feat(wasm-api): major update WasmBridge, add types
Browse files Browse the repository at this point in the history
- add WasmExports base interface
- add generics for WasmBridge & IWasmAPI
- update WasmBridge.init() arg (full WASM exports, not just mem)
- add WasmBridge.exports field to store WASM module exports
- add naming conflict check in WasmBridge.getImports()
  • Loading branch information
postspectacular committed Aug 3, 2022
1 parent ef46381 commit 47aa222
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 46 deletions.
32 changes: 30 additions & 2 deletions packages/wasm-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ 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<T extends WasmExports = WasmExports> {
/**
* Called by {@link WasmBridge.init} to initialize all child APIs (async)
* after the WASM module has been instantiated. If the method returns false
* the overall initialization process will be stopped/terminated.
*
* @param parent
*/
init(parent: WasmBridge): Promise<boolean>;
init(parent: WasmBridge<T>): Promise<boolean>;
/**
* 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,
Expand All @@ -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<number, void>;
printU8: Fn<number, void>;
Expand Down
116 changes: 72 additions & 44 deletions packages/wasm-api/src/bridge.ts
Original file line number Diff line number Diff line change
@@ -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<T extends WasmExports = WasmExports> {
i8!: Int8Array;
u8!: Uint8Array;
i16!: Int16Array;
Expand All @@ -17,9 +21,10 @@ export class WasmBridge {
utf8Decoder: TextDecoder = new TextDecoder();
utf8Encoder: TextEncoder = new TextEncoder();
core: CoreAPI;
exports!: T;

constructor(
public modules: Record<string, IWasmAPI> = {},
public modules: Record<string, IWasmAPI<T>> = {},
public logger: ILogger = new ConsoleLogger("wasm")
) {
const logN = (x: number) => this.logger.debug(x);
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down

0 comments on commit 47aa222

Please sign in to comment.