Skip to content

Commit

Permalink
feat(wasm-api): add events, update allocator handling
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
postspectacular committed Aug 24, 2022
1 parent be8423e commit 89416b1
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 44 deletions.
11 changes: 6 additions & 5 deletions packages/wasm-api/include/wasmapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdlib.h>
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);
Expand Down
35 changes: 16 additions & 19 deletions packages/wasm-api/include/wasmapi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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).*);
}
}
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 27 additions & 12 deletions packages/wasm-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -31,9 +33,9 @@ export interface IWasmAPI<T extends WasmExports = WasmExports> {
}

/**
* 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
Expand All @@ -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
Expand Down
61 changes: 53 additions & 8 deletions packages/wasm-api/src/bridge.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<T extends WasmExports = WasmExports>
implements IWasmMemoryAccess
implements IWasmMemoryAccess, INotify
{
i8!: Int8Array;
u8!: Uint8Array;
Expand Down Expand Up @@ -132,24 +142,34 @@ export class WasmBridge<T extends WasmExports = WasmExports>
* 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);
Expand All @@ -162,6 +182,11 @@ export class WasmBridge<T extends WasmExports = WasmExports>
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,
});
}

/**
Expand Down Expand Up @@ -240,7 +265,11 @@ export class WasmBridge<T extends WasmExports = WasmExports>
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;
Expand All @@ -252,6 +281,10 @@ export class WasmBridge<T extends WasmExports = WasmExports>
* `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
*/
Expand Down Expand Up @@ -514,4 +547,16 @@ export class WasmBridge<T extends WasmExports = WasmExports>
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 {}
}

0 comments on commit 89416b1

Please sign in to comment.