From 89416b142a8ac43af49e5098bf88c29f4939f118 Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Wed, 24 Aug 2022 11:40:00 +0200 Subject: [PATCH] feat(wasm-api): add events, update allocator handling - add INotify impl for WasmBridge - emit event when WASM memory has changed (e.g. to recreate user views) - reverse logic so that NO allocator is used by default and instead must be explicitly enabled (rather than disabled) - update Zig & C bindings - add/update docstrings --- packages/wasm-api/include/wasmapi.h | 11 ++--- packages/wasm-api/include/wasmapi.zig | 35 +++++++-------- packages/wasm-api/src/api.ts | 39 +++++++++++------ packages/wasm-api/src/bridge.ts | 61 +++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 44 deletions(-) diff --git a/packages/wasm-api/include/wasmapi.h b/packages/wasm-api/include/wasmapi.h index 78115eeea0..a97877a9eb 100644 --- a/packages/wasm-api/include/wasmapi.h +++ b/packages/wasm-api/include/wasmapi.h @@ -16,16 +16,17 @@ extern "C" { // Same as EMSCRIPTEN_KEEP_ALIVE, ensures symbol will be exported #define WASM_KEEP __attribute__((used)) -// Generate stubs only if explicitly disabled by defining this symbol -#ifdef WASMAPI_NO_MALLOC -size_t WASM_KEEP _wasm_allocate(size_t num_bytes) { return 0; } -void WASM_KEEP _wasm_free(size_t addr) {} -#else +// Generate malloc/free wrappers only if explicitly enabled by defining this +// symbol. If undefined some function stubs are exported. +#ifdef WASMAPI_MALLOC #include size_t WASM_KEEP _wasm_allocate(size_t numBytes) { return (size_t)malloc(numBytes); } void WASM_KEEP _wasm_free(size_t addr) { free((void*)addr); } +#else +size_t WASM_KEEP _wasm_allocate(size_t num_bytes) { return 0; } +void WASM_KEEP _wasm_free(size_t addr) {} #endif WASM_IMPORT("wasmapi", void, printI8, wasm_)(int8_t x); diff --git a/packages/wasm-api/include/wasmapi.zig b/packages/wasm-api/include/wasmapi.zig index 2873dbabc4..b84df87d6e 100644 --- a/packages/wasm-api/include/wasmapi.zig +++ b/packages/wasm-api/include/wasmapi.zig @@ -3,28 +3,26 @@ const std = @import("std"); const root = @import("root"); -/// Initialize the allocator to be exposed to the WASM host env +/// Obtains the allocator to be exposed to the WASM host env /// (via `_wasm_allocate()` and `_wasm_free()`). /// If the user defines a public `WASM_ALLOCATOR` in their root file -/// then this allocator will be used, otherwise the implementation -/// falls back to using GPA. +/// then this allocator will be used, otherwise the implementations +/// of the two mentioned functions are no-ops. +/// The `WASM_ALLOCATOR` can be changed and/or enabled/disabled dynamically +/// This helper function here is used to always lookup the current value/impl. +/// /// Note: The type for this var is purposefully chosen as an optional, -/// effectively disabling allocations from the WASM host side if -/// `WASM_ALLOCATOR` is set to null. -pub const allocator: ?std.mem.Allocator = alloc: { - if (@hasDecl(root, "WASM_ALLOCATOR")) { - break :alloc root.WASM_ALLOCATOR; - } else { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - break :alloc gpa.allocator(); - } -}; +/// effectively disabling allocations from the WASM host side if no +/// `WASM_ALLOCATOR` is set (or set to null). +pub fn allocator() ?std.mem.Allocator { + return if (@hasDecl(root, "WASM_ALLOCATOR")) root.WASM_ALLOCATOR else null; +} -/// Attempts to allocate memory using configured `allocator` and if +/// Attempts to allocate memory using configured `allocator()` and if /// successful returns address of new chunk or zero if failed /// Note: For SIMD compatibility all allocations are aligned to 16 bytes pub export fn _wasm_allocate(numBytes: usize) usize { - if (allocator) |alloc| { + if (allocator()) |alloc| { var mem = alloc.alignedAlloc(u8, 16, numBytes) catch return 0; return @ptrToInt(mem.ptr); } @@ -33,11 +31,10 @@ pub export fn _wasm_allocate(numBytes: usize) usize { /// Frees chunk of heap memory (previously allocated using `_wasm_allocate()`) /// starting at given address and of given byte length. -/// Note: This is a no-op if the allocator is explicitly disabled (see `setAllocator()`), +/// Note: This is a no-op if no allocator is configured (see `allocator()`) pub export fn _wasm_free(addr: usize, numBytes: usize) void { - if (allocator) |alloc| { + if (allocator()) |alloc| { var mem = [2]usize{ addr, numBytes }; - printFmt("{d}", .{@ptrCast(*[]u8, &mem).*}); alloc.free(@ptrCast(*[]u8, &mem).*); } } @@ -169,7 +166,7 @@ pub fn printStr(msg: []const u8) void { /// to output it via JS, then frees string's memory again /// (Only available if the allocator used by `_wasm_allocate()` hasn't been disabled.) pub fn printFmt(comptime fmt: []const u8, args: anytype) void { - if (allocator) |alloc| { + if (allocator()) |alloc| { const res = std.fmt.allocPrint(alloc, fmt, args) catch return; defer alloc.free(res); printStr(res); diff --git a/packages/wasm-api/src/api.ts b/packages/wasm-api/src/api.ts index 73ea1a9551..482590239a 100644 --- a/packages/wasm-api/src/api.ts +++ b/packages/wasm-api/src/api.ts @@ -3,6 +3,8 @@ import type { WasmBridge } from "./bridge.js"; export const PKG_NAME = "@thi.ng/wasm-api"; +export const EVENT_MEMORY_CHANGED = "memory-changed"; + export type BigIntArray = bigint[] | BigInt64Array | BigUint64Array; /** @@ -31,9 +33,9 @@ export interface IWasmAPI { } /** - * Base interface of exports declared by the WASM module. At the very least, the - * module needs to export its memory and the functions defined in this - * interface. + * Base interface of exports declared by the WASM module. At the very least, a + * compatible module needs to export its memory and the functions defined in + * this interface. * * @remarks * This interface is supposed to be extended with the concrete exports defined @@ -49,21 +51,34 @@ export interface WasmExports { */ memory: WebAssembly.Memory; /** - * Implementation specific memory allocation function (likely heap-based). - * If successful returns address of new memory block, or zero if - * unsuccessful. + * Implementation specific WASM memory allocation function. If successful + * returns address of new memory block, or zero if unsuccessful. * * @remarks - * In the supplied Zig bindings (see `/zig/core.zig`), by default this is - * using the `std.heap.GeneralPurposeAllocator` (which also automatically - * handles growing the WASM memory), however as mentioned the underlying - * mechanism is purposefully left to the actual WASM-side implementation. In - * a C program, this would likely use `malloc()` or similar... + * #### Zig + * + * Using the supplied Zig bindings (see `/include/wasmapi.zig`), it's the + * user's responsibility to define a public `WASM_ALLOCATOR` in the root + * source file to enable allocations, e.g. using the + * [`std.heap.GeneralPurposeAllocator`](https://ziglang.org/documentation/master/#Choosing-an-Allocator) + * (which also automatically handles growing the WASM memory). However, as + * mentioned, the underlying mechanism is purposefully left to the actual + * WASM-side implementation. If no allocator is defined this function + * returns zero, which in turn will cause {@link WasmBridge.allocate} to + * throw an error. + * + * #### C/C++ + * + * Using the supplied C bindings (see `/include/wasmapi.h`), it's the user's + * responsibility to enable allocation support by defining the + * `WASMAPI_MALLOC` symbol (and compiling the WASM module with a malloc + * implementation). */ _wasm_allocate(numBytes: number): number; /** * Implementation specific function to free a previously allocated chunk of - * of WASM memory (allocated via {@link WasmExports._wasm_allocate}). + * of WASM memory (allocated via {@link WasmExports._wasm_allocate}, also + * see remarks for that function). * * @param addr * @param numBytes diff --git a/packages/wasm-api/src/bridge.ts b/packages/wasm-api/src/bridge.ts index d967998902..7a2d8d2a43 100644 --- a/packages/wasm-api/src/bridge.ts +++ b/packages/wasm-api/src/bridge.ts @@ -1,15 +1,24 @@ -import type { FnU2, NumericArray, TypedArray } from "@thi.ng/api"; +import type { + Event, + FnU2, + INotify, + Listener, + NumericArray, + TypedArray, +} from "@thi.ng/api"; +import { INotifyMixin } from "@thi.ng/api/mixins/inotify"; import { defError } from "@thi.ng/errors/deferror"; import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; import { U16, U32, U64HL, U8 } from "@thi.ng/hex"; import type { ILogger } from "@thi.ng/logger"; import { ConsoleLogger } from "@thi.ng/logger/console"; -import type { +import { BigIntArray, CoreAPI, IWasmAPI, WasmExports, IWasmMemoryAccess, + EVENT_MEMORY_CHANGED, } from "./api.js"; const B32 = BigInt(32); @@ -32,8 +41,9 @@ export const OutOfMemoryError = defError(() => "Out of memory"); * 64bit integers are handled via JS `BigInt` and hence require the host env to * support it. No polyfill is provided. */ +@INotifyMixin export class WasmBridge - implements IWasmMemoryAccess + implements IWasmMemoryAccess, INotify { i8!: Int8Array; u8!: Uint8Array; @@ -132,24 +142,34 @@ export class WasmBridge * then initializes all declared bridge child API modules. 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: T) { this.exports = exports; - this.ensureMemory(); + this.ensureMemory(false); for (let id in this.modules) { 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. Initializes and/or updates the various typed WASM - * memory views (e.g. after growing the WASM memory). + * Called automatically during initialization. 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() { + ensureMemory(notify = true) { const buf = this.exports.memory.buffer; if (this.u8 && this.u8.buffer === buf) return; this.i8 = new Int8Array(buf); @@ -162,6 +182,11 @@ export class WasmBridge 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, + }); } /** @@ -240,7 +265,11 @@ export class WasmBridge const addr = this.exports._wasm_allocate(numBytes); if (!addr) throw new OutOfMemoryError(`unable to allocate: ${numBytes}`); - this.logger.debug(`allocated ${numBytes} bytes @ 0x${U32(addr)}`); + this.logger.debug( + `allocated ${numBytes} bytes @ 0x${U32(addr)} .. 0x${U32( + addr + numBytes - 1 + )}` + ); this.ensureMemory(); clear && this.u8.fill(0, addr, addr + numBytes); return addr; @@ -252,6 +281,10 @@ export class WasmBridge * `numBytes` value must be the same as previously given to * {@link WasmBridge.allocate}. * + * @remarks + * This function always succeeds, regardless of presence of an active + * allocator on the WASM side or validity of given arguments. + * * @param addr * @param numBytes */ @@ -514,4 +547,16 @@ export class WasmBridge el == null && illegalArgs(`missing DOM element #${id}`); return el!; } + + /** {@inheritDoc @thi.ng/api#INotify.addListener} */ + // @ts-ignore: mixin + addListener(id: string, fn: Listener, scope?: any): boolean {} + + /** {@inheritDoc @thi.ng/api#INotify.removeListener} */ + // @ts-ignore: mixin + removeListener(id: string, fn: Listener, scope?: any): boolean {} + + /** {@inheritDoc @thi.ng/api#INotify.notify} */ + // @ts-ignore: mixin + notify(event: Event): void {} }