Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WebAssembly

The WebAssembly target generates an npm package for a wasm32-unknown-unknown build of the library: an ES module with TypeScript declarations over glue that stages values in the module’s linear memory. The JavaScript API is the Node.js target’s, so the sections there on names, types, objects, errors, callbacks, and iterators apply here unchanged; this page covers what differs.

What gets generated

For a library named kvstore (the snapshot fixtures use kitchen_sink):

wasm/
├── package.json   ES module manifest
├── index.js       `init()`, the raw entry points, and the API
├── index.d.ts     TypeScript declarations
├── runtime.js     the shared runtime (codec, errors, wrappers)
├── linear.js      the transport: loading, staging, table functions
└── README.md

The package name defaults to the package identity’s name; set name under [generators.wasm] to override it. weaveffi package writes the npm tarball wasm/{package}-{version}.tgz with the module weaveffi build linked for wasm32 as {library}.wasm.

Build the module

The glue installs async completions and callback-interface methods in the module’s function table, so link the module with an exported, growable table:

RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table" \
  cargo build --release --target wasm32-unknown-unknown

weaveffi build --platforms wasm32 passes those flags itself and writes the module to target/weaveffi/wasm32/{library}.wasm.

Load

init() loads the module and checks the contract before any other export works: the module must implement ABI revision 4, and each top-level module’s contract table must hold every declaration the bindings were generated with (see the Node.js page). A failed check rejects with an Error naming the declaration (kvstore: kv.Store.put changed since these bindings were generated). Until the promise resolves, every other call throws.

import { init, kv } from 'kvstore';

await init();
const store = kv.Store.open('/tmp/data');

Without an argument, init() reads the file named by the {PREFIX}_LIBRARY environment variable (KVSTORE_LIBRARY) when it’s set on Node.js, and otherwise loads {library}.wasm from next to index.js (by fetch in a browser, from disk on Node.js). It also takes a URL, a path (resolved against index.js), bytes, a Response, or a compiled WebAssembly.Module:

export declare function init(
  source?: string | URL | BufferSource | WebAssembly.Module | Response,
): Promise<void>;

init() may be called more than once; later calls return the same promise, and a failed load can be retried.

Transport

Each C symbol gets one entry point in index.js that checks its arguments, copies strings and byte runs into linear memory for the call, and copies returned strings and buffers out before releasing them:

    kitchen_sink_kitchen_echo_string: (a0) => {
      let s0 = null;
      try {
        s0 = m.str(a0);
        const r = x.kitchen_sink_kitchen_echo_string(s0[0], s0[1], m.len, m.err);
        m.check();
        return m.takeStr(r, m.outLen());
      } finally {
        m.unstage(s0);
      }
    },

Staged arguments are runs from {prefix}_alloc, released with {prefix}_free_bytes once the call returns. Runs have alignment 1, so the glue reads and writes multi-byte values through a DataView. One error slot, one out_len slot, and one iterator item slot are reused by every call; they and the callback vtables live in 8-byte-aligned blocks carved from arenas the glue allocates once and never frees. __debugLive(4) leaves those arenas out of its count of byte runs. Object handles are linear-memory addresses.

Async completions and callback-interface methods are JavaScript functions installed in the module’s function table. The glue uses WebAssembly.Function where the engine provides it and otherwise compiles a tiny module that imports the function with the needed signature and re-exports it, so no experimental flag is needed.

Type mapping

The mapping is the Node.js target’s: 64-bit integers are bigint, bytes is Uint8Array, records are plain objects, rich enums are tagged unions, C-style enums are frozen objects, interfaces are classes with close() and [Symbol.dispose](), optionals are T | null, lists are arrays, maps are Record objects (a Map is accepted as an argument), and iter<T> is a lazy IterableIterator<T>.

Async and cancellation

Async functions return a Promise, and cancellable ones take an optional { signal } last argument, exactly as on Node.js. The module is single-threaded, and on wasm32 the default executor polls a future inline until it completes, inside its launcher, so an async call has completed by the time the Promise is returned; only a signal aborted before the call can cancel it. A future that’s still pending once nothing can wake it (it awaits something only another thread or a reactor would complete) fails with code -1 instead (async function suspended with no executor on wasm32). A cancelled call rejects with CancelledError (code -5).

Callbacks and errors

Callback interfaces work as on Node.js: returns of every family (a string, bytes, or buffer return is copied into a run from {prefix}_alloc that the producer adopts, and an object return is a new reference), domain errors from methods declared throws with their fields, and null for an optional callback (Cb?), which passes a null vtable. The vtable starts with its size, flags, and free entries, like any consumer’s.

There are no threads, so a callback runs only while a call into the module is on the stack, on the calling thread, and the Node.js addon’s threading rules don’t arise. An implementation that throws fails the call with code -4, as on Node.js. Because wasm32-unknown-unknown aborts on a panic, a producer panic traps the module; the trap surfaces as the package’s root error with code -2, and the module may be unusable afterward.

Emscripten

With emscripten = true under [generators.wasm], init instead takes an initialized Emscripten module (or the promise its MODULARIZE factory returns), binds its underscore-prefixed exports, reads memory through HEAPU8, and installs table functions with addFunction. Link the module with -sALLOW_TABLE_GROWTH, -sWASM_BIGINT, and -sEXPORTED_RUNTIME_METHODS=addFunction,HEAPU8, and export the library’s C symbols, including the runtime surface ({prefix}_alloc, {prefix}_free_bytes, {prefix}_error_set_payload, and the rest) and each {prefix}_{module}_contract. The packaged layout then ships glue only.

export declare function init(module: object | Promise<object>): Promise<void>;

Known limitations

  • The module must export its function table and allow it to grow.
  • An async call can’t be cancelled once launched, since it completes before its launcher returns, and a future that waits on another thread or an external event fails with code -1.
  • A producer panic traps rather than unwinding, which can leave the module’s state inconsistent.
  • Emscripten mode isn’t covered by the conformance suite.