Run WebAssembly in Node.js and Pass Data

WebAssembly (Wasm) allows developers to run high-performance, compiled code inside a Node.js environment alongside JavaScript. This article provides a practical, step-by-step guide on how to load and execute Wasm modules in Node.js, and explains how to successfully pass both simple data types, like numbers, and complex data structures, like arrays and strings, between JavaScript and compiled code.


1. Loading and Executing Wasm Modules in Node.js

Node.js has native support for WebAssembly via the global WebAssembly object. To run a Wasm module, you must first read the .wasm file into a buffer and then compile and instantiate it.

Here is the standard pattern for loading a Wasm file:

const fs = require('fs');
const path = require('path');

async function loadWasm(wasmFilePath, importObject = {}) {
  const wasmBuffer = fs.readFileSync(path.resolve(__dirname, wasmFilePath));
  const { instance } = await WebAssembly.instantiate(wasmBuffer, importObject);
  return instance.exports;
}

2. Passing Simple Data (Numbers)

WebAssembly natively supports only numeric types (integers and floats). Passing numbers between JavaScript and Wasm is seamless because the WebAssembly.instantiate API automatically maps JS numbers to Wasm’s numeric types.

The JavaScript Implementation:

async function run() {
  // Load a module that exports an 'add' function
  const exports = await loadWasm('math.wasm');
  
  // Pass numbers directly
  const sum = exports.add(5, 10);
  console.log(`Result from Wasm: ${sum}`); // Outputs: 15
}

run();

3. Passing Complex Data (Arrays and Strings)

Because WebAssembly cannot directly access JavaScript’s heap memory, you cannot pass objects, arrays, or strings directly as arguments. Instead, you must write this data into the shared WebAssembly Memory buffer and pass the memory offset (pointer) and size to the Wasm function.

Step 1: Writing an Array to Wasm Memory

To pass an array of numbers to Wasm, you need to write it into the typed array backed by wasmInstance.exports.memory.buffer.

async function passArrayToWasm() {
  const exports = await loadWasm('analytics.wasm');
  
  // Access the exported WebAssembly memory
  const memory = exports.memory;
  
  // Define the JavaScript array to pass
  const jsArray = [10, 20, 30, 40, 50];
  const elementCount = jsArray.length;
  
  // Determine where in the Wasm memory to store the data
  // (In a production environment, use a Wasm allocator like malloc)
  const pointer = exports.get_shared_buffer_pointer(); 
  
  // Create a view matching the data type (e.g., 32-bit integers)
  const wasmMemoryView = new Int32Array(memory.buffer, pointer, elementCount);
  
  // Copy the JS array data into the Wasm memory view
  wasmMemoryView.set(jsArray);
  
  // Execute the Wasm function by passing the pointer and the length
  const average = exports.calculate_average(pointer, elementCount);
  console.log(`Average calculated by Wasm: ${average}`);
}

Step 2: Reading Strings from Wasm Memory

To retrieve a string generated by a Wasm module, the module must return a memory pointer and a length. JavaScript can then decode that slice of memory back into a JS string.

async function readStringFromWasm() {
  const exports = await loadWasm('greetings.wasm');
  
  // Call the Wasm function to generate a string
  const pointer = exports.greet_user();
  const length = exports.get_string_length();
  
  // Create a Uint8Array view of the string bytes in memory
  const memory = exports.memory;
  const stringBuffer = new Uint8Array(memory.buffer, pointer, length);
  
  // Decode the bytes into a JavaScript string
  const decoder = new TextDecoder('utf-8');
  const greeting = decoder.decode(stringBuffer);
  
  console.log(greeting); // Outputs: "Hello from WebAssembly!"
}

Summary of Data Flow Rules

  1. Primes and Floats: Pass directly as arguments and receive directly as return values.
  2. Arrays and Buffers: Allocate space in the Wasm memory, copy the bytes using a TypedArray view in JavaScript, and pass the starting memory address (pointer) and byte length to the Wasm function.
  3. Strings: Encode to UTF-8 bytes to write to memory, or decode from UTF-8 bytes using TextDecoder when reading from memory.