@thi.ng/wasm-api
Version:
Generic, modular, extensible API bridge and infrastructure for hybrid JS & WebAssembly projects
217 lines (216 loc) • 6.53 kB
JavaScript
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
};