WASM Worker for Image Module
This file implements a Web Worker that acts as a bridge between JavaScript and the Image WASM module. It handles memory allocation, TypedArray mapping, dynamic function calls, and returning results or errors asynchronously.
This allows expensive WASM computations to run independently from the main thread, improving UI performance.
To ensure safety and proper memory management, all data is copied between JavaScript and WASM.
- JavaScript receives a detached copy, allowing its garbage collector (GC) to clean up unused objects.
- WASM manages its own memory independently, preventing accidental overwrites or leaks.
This pattern avoids bugs that could occur if JS arrays directly referenced WASM memory that gets freed.
Overviewβ
Key responsibilitiesβ
- Load and initialize the Image WASM module.
- Accept messages from the main thread describing:
- The function to call in WASM.
- Arguments to pass (including TypedArrays).
- Allocate and copy memory for JS arrays into WASM.
- Invoke the WASM function dynamically.
- Read back any modified buffers from WASM memory.
- Free allocated memory to prevent leaks.
- This keeps the code written in JavaScript looking more like JavaScript and prevents forgotten memory frees.
- Post results or errors back to the main thread.
WASM Module Initializationβ
import createImageModule from '@wasm-image';
let wasmModule;
let readyResolve;
const readyPromise = new Promise((res) => (readyResolve = res));
// Initialize the module once
createImageModule().then((mod) => {
wasmModule = mod;
readyResolve();
});
- Imports the
createImageModulefactory function generated by Emscripten. - Uses a Promise (
readyPromise) to ensure the module is fully loaded before handling any messages. - Initialization only happens once.
Message Handlingβ
self.onmessage = async ({ data }) => {
await readyPromise;
const { id, funcName, args, bufferKeys } = data;
const pointers = Object.create(null); // Track malloc pointers
try {
// Allocate buffers for Array payloads
...
-
The worker listens to
self.onmessagefrom the main thread. -
Each message should contain:
id: Unique identifier for correlating responses.funcName: WASM function to call.args: Object containing arguments.bufferKeys(optional): Keys inargsthat are TypedArrays and require special WASM memory mapping.
-
The worker waits for the WASM module to be ready before proceeding.
Memory Allocation for TypedArraysβ
if (bufferKeys) {
for (const key of bufferKeys) {
const arr = args[key];
const sizeInBytes = arr.byteLength;
const ptr = wasmModule._malloc(sizeInBytes);
if (arr instanceof Int32Array) {
wasmModule.HEAP32.set(arr, ptr / Int32Array.BYTES_PER_ELEMENT);
} else if (arr instanceof Uint8ClampedArray || arr instanceof Uint8Array) {
wasmModule.HEAPU8.set(arr, ptr);
} else {
throw new Error(`Unsupported TypedArray type for key: ${key}`);
}
pointers[key] = { ptr, sizeInBytes, type: arr.constructor };
args[key] = ptr; // pass pointer to WASM
}
}
- For each buffer key:
- Allocate memory in WASM using
_malloc(C-style code applies here for interoperability, this is why thenewkeyword from C++ is not used). - Copy the JS array into the appropriate HEAP view:
Int32ArrayβHEAP32Uint8ClampedArrayβHEAPU8Uint8ArrayβHEAPU8
- Replace the JS array in
argswith the pointer. - Track allocation for later cleanup.
- Allocate memory in WASM using
Any new TypedArray types must be added to this section and their corresponding EXPORTED_RUNTIME_METHODS in the C++ module's CMake build file.
For example, an Int32Array cannot be read if HEAP32 is not exported from WASM. This means that it needs to be exported explicitly in the CMake file.
Dynamic WASM Function Callβ
const exportName = `_${funcName}`;
if (typeof wasmModule[exportName] !== 'function') {
throw new Error(`WASM export not found: ${exportName}`);
}
const targetFunction = wasmModule[exportName];
const targetFunctionArgs = funcName && args ? Object.values(args) : [];
const result = targetFunction(...targetFunctionArgs);
- All Emscripten exports are prefixed with
_.- We add the prefix in the API for the caller's convenience because it is easy to forget.
- Uses dynamic lookup so any exported function can be called.
- Arguments are passed as an array of values or pointers.
Reading Back Modified Buffersβ
const outputs = {};
if (bufferKeys) {
for (const key of bufferKeys) {
const { ptr, sizeInBytes, type } = pointers[key];
if (type === Int32Array) {
outputs[key] = new Int32Array(wasmModule.HEAP32.buffer, ptr, sizeInBytes / Int32Array.BYTES_PER_ELEMENT).slice();
} else if (type === Uint8ClampedArray) {
outputs[key] = new Uint8ClampedArray(wasmModule.HEAPU8.subarray(ptr, ptr + sizeInBytes)).slice();
} else if (type === Uint8Array) {
outputs[key] = new Uint8Array(wasmModule.HEAPU8.subarray(ptr, ptr + sizeInBytes)).slice();
}
}
}
- After the function call, reads back any modified arrays.
- Uses
.slice()to detach from WASM memory. - Ensures main thread receives independent copies.
Memory Cleanupβ
finally {
for (const { ptr } of Object.values(pointers)) {
wasmModule._free(ptr);
}
}
- Always frees allocated memory with
_free. - Prevents memory leaks in long-running workers.
Posting Results Backβ
self.postMessage({ id, output: outputs, returnValue: result });
-
Returns both:
returnValue: the functionβs direct return value.output: object containing updated TypedArrays.
-
If any error occurs, it posts back:
self.postMessage({ id, error: error.message });
Diagram (WASM Worker Flow)β
- Shows the end-to-end flow of handling a message:
- JS array β WASM memory β function call β output β free memory β return to main thread.
-
Always add new TypedArray types in both:
- This worker file (
wasmWorker.js). - The Emscripten moduleβs
EXPORTED_RUNTIME_METHODSin CMake.
- This worker file (
-
Avoid calling WASM functions before the module is fully initialized.
-
Use
bufferKeysto specify which arguments are arrays needing memory allocation.