JavaScript Views on WebAssembly Linear Memory

This article explores how JavaScript interacts with WebAssembly linear memory using TypedArray views and DataView wrappers. WebAssembly operates within an isolated, contiguous block of raw memory that JavaScript can inspect, manipulate, and share via standard ArrayBuffer references without incurring data copying overhead.

The Structure of WebAssembly Memory

WebAssembly memory is exposed to both the host environment (JavaScript) and the WebAssembly runtime as an instance of WebAssembly.Memory. Under the hood, this object wraps an underlying ArrayBuffer, which represents an expandable, contiguous block of raw bytes. WebAssembly code references memory locations using integer offsets, commonly referred to as pointers.

Accessing Memory via memory.buffer

JavaScript cannot directly read raw pointers, but it can access the memory’s backing store through the WebAssembly.Memory.prototype.buffer property. This property exposes the underlying ArrayBuffer.

To read or write specific data types at specific offsets, JavaScript creates TypedArray views (such as Uint8Array, Int32Array, or Float64Array) or a DataView over that buffer:

// Assume wasmInstance exports a memory object
const wasmMemory = wasmInstance.exports.memory;

// Create a view over the entire linear memory as bytes
const byteView = new Uint8Array(wasmMemory.buffer);

// Create a view for a 32-bit integer array located at a specific pointer
const pointer = wasmInstance.exports.get_data_pointer();
const length = 10;
const int32View = new Int32Array(wasmMemory.buffer, pointer, length);

Because these views point directly to the shared ArrayBuffer, modifications made by JavaScript are immediately visible to WebAssembly, and vice versa. This enables zero-copy data sharing between the two execution environments.

Resolving Pointers and Structs

When a WebAssembly function returns a pointer to an allocated data structure, string, or array, it is returning a byte offset relative to the start of its linear memory:

  1. Strings (UTF-8): JavaScript creates a Uint8Array view starting at the string’s pointer offset for the specified byte length, then decodes it using TextDecoder.
  2. Homogeneous Arrays: JavaScript wraps the memory range in the corresponding TypedArray (e.g., Float32Array for floating-point vectors).
  3. Complex Structs: JavaScript uses a DataView at the base pointer offset to read heterogeneous fields with methods like .getInt32(offset, littleEndian) and .getFloat64(offset, littleEndian).

Buffer Detachment and Memory Growth

A critical behavior occurs when WebAssembly memory grows via the memory.grow() instruction (or the JavaScript WebAssembly.Memory.prototype.grow() method).

Growing memory allocates new WebAssembly pages (each page is 64 KiB) and reallocates the underlying ArrayBuffer. Consequently, the previous ArrayBuffer becomes detached (neutered), rendering all existing TypedArray and DataView instances invalid:

let view = new Uint8Array(wasmMemory.buffer);
console.log(view.byteLength); // Valid size

// WebAssembly grows memory internally or explicitly via JS
wasmMemory.grow(1);

console.log(view.byteLength); // 0 (detached buffer)

To prevent runtime errors caused by detached buffers, JavaScript memory views should either be short-lived—created on demand right before reading or writing—or refreshed whenever a memory growth event occurs.