Exu is a high-performance library designed for both browser and Deno environments, facilitating seamless integration of WebAssembly modules. It adopts a unique sandbox/worker approach to ensure safe and efficient execution of tasks.
- Sandbox/Worker Model: Exu leverages a sandboxed environment for executing tasks, ensuring isolation and security while maintaining high performance.
- Shared Memory Access: Offers two modes of memory access - direct shared memory access for efficient data manipulation and a
memcpyfunction for copying data between contexts. - Task-Based Execution: Simplifies the execution of bulk actions and complex tasks by encapsulating them in easily manageable units.
To include Exu in your project, import the module directly from the provided URL:
import { Module } from "jsr:@dusk/exu";First, create a new Module instance by specifying the path to your WebAssembly module:
const module = new Module(
new URL("path/to/your/wasm/module.wasm", import.meta.url),
);Optionally, set default imports if your WebAssembly module requires them:
module.defaultImports = new URL("./path/to/custom-imports.js", import.meta.url);Perform single API calls straightforwardly:
const result = await module.api().yourFunction(args);Handle errors gracefully using try-catch blocks for API calls that might fail:
try {
const result = await module.api().nonExistentFunction();
} catch (error) {
console.error("Function does not exist", error);
}To efficiently handle bulk actions, such as when multiple functions need to access the same memory, or to manage more complex tasks, it's recommended to encapsulate these operations within a defined task:
let task = module.task(async ({ yourFunctionA, yourFunctionB }) => {
let result1 = await yourFunctionA(arg1);
let result2 = await yourFunctionB(arg2);
return { result1, result2 };
});
let results = await task();Directly manipulate shared memory for high-performance scenarios:
const task = module.task(async ({ malloc, free, byte, set_byte }, { memory }) => {
let ptr = await malloc(size);
let buffer = new Uint8Array(memory.buffer, ptr, size);
// Perform operations on the buffer
await free(ptr, size);
});Use the memcpy function to copy data between JavaScript and the WebAssembly environment, useful for scenarios where direct memory sharing is not feasible:
const task = module.task(async ({ malloc, free }, { memcpy }) => {
let ptr = await malloc(size);
let data = new Uint8Array(size);
// Set data values...
// Copy data to WebAssembly memory
data = await memcpy(ptr, data, size);
// Retrieve updated data from WebAssembly memory
data = await memcpy(null, ptr, size);
await free(ptr, size);
});Tasks run in a pool of workers: at most workers at once per module, whatever
their imports (by default one less than the logical cores), with other tasks
waiting their turn. Each task gets a
fresh instance of the module. A worker that finished its task cleanly serves
the next one, and ends after idleTimeout ms without work (10 seconds by
default; 0 ends it as soon as its task does, unless a task is waiting).
Workers whose task was aborted, that still had calls running, or that loaded
custom imports are never reused.
const module = new Module({ source, workers: 4, idleTimeout: 30_000 });
// ...
module.close(); // ends idle workers nowIdle workers hold no ports or timers on the caller's side; each one counts
down its own idle time. In Deno, though, an idle worker keeps the process
alive until it times out: call close() when a script is done with a module.
A task may wait on another task of the same module. When every worker is
taken and has run no call for 100 ms (each task is waiting on something else),
the next waiting task gets an extra worker, which ends with its task. So
nested tasks cannot deadlock the pool; they may wait up to about 100 ms. While
workers are busy with calls, the workers bound holds.
In browsers, Exu starts each sandbox worker from src/sandbox/worker-entry.js,
next to its own module. Bundlers such as Vite and webpack recognise this and
emit the worker as a file of their own, which pages limited to
worker-src 'self' (browser extensions, for example) can start. If that file
does not start (Exu was loaded from another origin, or bundled without it), Exu
falls back to a worker built from a Blob URL, which worker-src 'self' refuses.
Deno, Bun and Node always use the Blob worker, which needs no file access.
To start a different copy of the worker, pass its URL:
const module = new Module({ source, workerUrl });The worker must come from the same Exu version. A mismatched worker rejects tasks with an "Incompatible Exu sandbox worker" error.
Exu is made available under the Mozilla Public License 2.0 (MPL 2.0). You are encouraged to use, modify, and distribute the library in compliance with the terms set forth in the MPL 2.0.