UNPKG

@thi.ng/wasm-api

Version:

Generic, modular, extensible API bridge and infrastructure for hybrid JS & WebAssembly projects

217 lines (216 loc) 6.53 kB
import { isNumber } from "@thi.ng/checks/is-number"; import { unsupported } from "@thi.ng/errors/unsupported"; class WasmStringSlice { constructor(mem, base, isConst = true, terminated = true) { this.mem = mem; this.base = base; this.isConst = isConst; this.terminated = terminated; this.maxLen = this.length; } maxLen; /** * Returns string start address (deref'd pointer). */ get addr() { this.mem.ensureMemory(); return this.mem.u32[this.base >>> 2]; } /** * Returns string length (read from memory) */ get length() { this.mem.ensureMemory(); return this.mem.u32[this.base + 4 >>> 2]; } /** * Returns memory as JS string (aka wrapper for * {@link WasmBridge.getString}). */ deref() { return this.mem.getString(this.addr, this.length); } /** * If given a JS string as arg (and if **not** a const slice), attempts to * overwrite this wrapped string's memory with bytes from given string. If * given another {@link WasmStringSlice} as arg, only the slice pointer & * new length will be updated (always succeeds). * * @remarks * When copying bytes from a JS string, an error will be thrown if the new * string is longer than the _original_ length of the slice (i.e. from when * this `WasmStringSlice` wrapper instance was created). Also updates the * slice's length field to new string length. * * Passing a `WasmString` instance as arg is faster than JS string since * only the slice definition itself will be updated. * * @param str */ set(str) { this.mem.ensureMemory(); if (typeof str === "string") { if (this.isConst) unsupported("can't mutate const string"); this.mem.u32[this.base + 4 >>> 2] = this.mem.setString( str, this.addr, this.maxLen + ~~this.terminated, this.terminated ); } else { this.mem.u32[this.base >>> 2] = str.addr; this.mem.u32[this.base + 4 >>> 2] = str.length; } } setSlice(...args) { this.mem.ensureMemory(); const [slice, terminated] = isNumber(args[0]) ? [[args[0], args[1]], args[2]] : [args[0], args[1]]; this.mem.u32[this.base >>> 2] = slice[0]; this.mem.u32[this.base + 4 >>> 2] = slice[1]; this.terminated = terminated; return slice; } /** * Encodes given string to UTF-8 (by default zero terminated), allocates * memory for it, updates this slice and returns a {@link MemorySlice} of * the allocated region. * * @remarks * If `terminated` is true, the stored slice length will **NOT** include the * sentinel! E.g. the slice length of zero-terminated string `"abc"` is 3, * but the number of allocated bytes is 4. This is done for compatibility * with Zig's sentinel-terminated slice handling (e.g. `[:0]u8` slices). * * Regardless of `terminated` setting, the returned `MemorySlice` **always** * covers the entire allocated region! * * @param str * @param terminate */ setAlloc(str, terminate = true) { const slice = __alloc(this.mem, str, terminate); this.setSlice(terminate ? [slice[0], slice[1] - 1] : slice, terminate); return slice; } toJSON() { return this.deref(); } toString() { return this.deref(); } valueOf() { return this.deref(); } } class WasmStringPtr { constructor(mem, base, isConst = true) { this.mem = mem; this.base = base; this.isConst = isConst; } /** * Returns string start address (deref'd pointer). */ get addr() { this.mem.ensureMemory(); return this.mem.u32[this.base >>> 2]; } set addr(addr) { this.mem.ensureMemory(); this.mem.u32[this.base >>> 2] = addr; } get isNull() { return this.addr === 0; } /** * Returns computed string length (scanning memory for zero sentinel) * * @remarks * Always returns 0 if null pointer (i.e. if {@link WasmStringPtr.addr} is * zero). */ get length() { const addr = this.addr; if (!addr) return 0; const idx = this.mem.u8.indexOf(0, addr); return idx >= 0 ? idx - addr : 0; } /** * Returns memory as JS string (via {@link WasmBridge.getString}). Returns * empty string if null pointer (i.e. if {@link WasmStringPtr.addr} is * zero). */ deref() { const addr = this.addr; return addr ? this.mem.getString(addr, this.length) : ""; } /** * If given a JS string as arg (and if this `WasmStringPtr` instance itself * is not a `const` pointer), attempts to overwrite this wrapped string's * memory with bytes from given string. If given another * {@link WasmStringPtr}, it merely overrides the pointer to the new one * (always succeeds). * * @remarks * Unlike with {@link WasmStringSlice.set} this implementation which * performs bounds checking when copying bytes from a JS string, this method * only throws an error if the new string is longer than the available * memory (from the start address until the end of the WASM memory). * **Therefore, this is as (un)safe as a C pointer and should be used with * caution!** * * Passing a `WasmStringPtr` instance as arg is faster than JS string since * only the pointer itself will be updated. * * @param str */ set(str) { const addr = this.addr; if (!addr) unsupported("can't mutate null pointer"); if (typeof str === "string") { if (this.isConst) unsupported("can't mutate const string"); this.mem.ensureMemory(); this.mem.setString(str, addr, this.mem.u8.byteLength - addr, true); } else { this.addr = str.addr; this.isConst = str.isConst; } } /** * Encodes given string to UTF-8 (by default zero terminated), allocates * memory for it, updates this pointer to new address and returns allocated * {@link MemorySlice}. * * @remarks * See {@link WasmStringSlice.setAlloc} for important details. * * @param str */ setAlloc(str) { const slice = __alloc(this.mem, str, true); this.mem.u32[this.base >>> 2] = slice[0]; return slice; } toJSON() { return this.deref(); } toString() { return this.deref(); } valueOf() { return this.deref(); } } const __alloc = (mem, str, terminate) => { const buf = new TextEncoder().encode(str); const slice = mem.allocate(buf.length + ~~terminate); if (slice[1] > 0) { mem.u8.set(buf, slice[0]); terminate && (mem.u8[slice[0] + buf.length] = 0); } return slice; }; export { WasmStringPtr, WasmStringSlice };