Providing Memory at Instantiation
This page answers one task: instead of letting a module create its own linear memory, create the memory in JavaScript, pass it in as an import, and use the fact that the host owns it — to share it, size it, or put data in it before the module runs.
Prerequisites
- [ ] A module that imports its memory — linked with
--import-memory, as in importing memory from the host with --import-memory, or written in WAT with an(import "env" "memory" (memory …)). - [ ] The import’s declared limits, from
wasm-tools print --skeletonorwasm-objdump -j Import.
Who owns the memory changes what the host can do
A module that defines its own memory creates it at instantiation and exports it if the host needs access. The host only ever sees the memory
after the module exists, through instance.exports.memory. A module that imports its memory receives one the host created earlier. That
small change in ownership opens up several things the export pattern cannot do.
The host can share one memory between several instances — a main module and side modules, or one instance per worker in a threaded build. It can prepare the memory before the module runs, writing input data or configuration into a known region. It can size it according to the application rather than the module, choosing initial and maximum pages within the module’s declared limits. And it can keep a reference to the memory that remains valid even if the instance is discarded and recreated, which simplifies hosts that restart modules after failures.
Step 1 — match the module’s limits
Read what the module requires, then create a memory that satisfies it:
wasm-tools print --skeleton kernel.wasm | grep 'import.*memory'
(import "env" "memory" (memory (;0;) 16 1024))
The module needs at least 16 pages (1 MiB) and accepts a maximum of at most 1024 pages (64 MiB). Create the memory within those bounds:
const memory = new WebAssembly.Memory({ initial: 32, maximum: 512 }); // 2 MiB now, up to 32 MiB
const { instance } = await WebAssembly.instantiateStreaming(fetch("kernel.wasm"), { env: { memory } });
initial may exceed the module’s minimum. maximum must be present if the module declares one and must not exceed it. A memory that violates
either rule fails instantiation with a LinkError naming the memory import, as described in
handling CompileError and LinkError.
Step 2 — prefill data the module will read
Because the memory exists before the module, the host can write into it first. The module’s own static data and stack are placed by its linker, typically starting near address 1024, so put host data above the module’s heap start or at an address both sides agree on:
const INPUT_AT = 4 * 65536; // agreed offset: 256 KiB, above static data and stack
const input = new Uint8Array(await (await fetch("/samples/input.bin")).arrayBuffer());
new Uint8Array(memory.buffer, INPUT_AT, input.length).set(input);
const { instance } = await WebAssembly.instantiateStreaming(fetch("kernel.wasm"), { env: { memory } });
const checksum = instance.exports.process(INPUT_AT, input.length);
Writing before instantiation has one trap: the module’s active data segments are written during instantiation, after your prefill, and will
overwrite anything in their range. Keep prefilled data clear of the module’s data region — check __data_end and __heap_base from the module’s
globals — or write after instantiation instead.
Step 3 — share one memory between instances
Several instances can import the same memory. A typical case is a main module and a plugin compiled to cooperate, both reading and writing one buffer:
const memory = new WebAssembly.Memory({ initial: 64, maximum: 1024 });
const main = await WebAssembly.instantiate(mainModule, { env: { memory } });
const plugin = await WebAssembly.instantiate(pluginModule, { env: { memory } });
Sharing memory between independently compiled modules only works if they agree on which regions each uses — each module’s linker assumes it owns the static data region and the heap. Real setups use toolchain support for this (dynamic linking, where side modules are relocated into the main module’s memory) rather than hand-coordinated offsets. Sharing between instances of the same module, on the other hand, is the normal threaded pattern, below.
Step 4 — share between workers with a shared memory
For threads, create the memory with shared: true and post it to each worker along with the compiled module. Every worker instantiates its own
instance against the same SharedArrayBuffer-backed memory:
const memory = new WebAssembly.Memory({ initial: 256, maximum: 4096, shared: true });
const module = await WebAssembly.compileStreaming(fetch("engine_mt.wasm"));
for (let i = 0; i < 4; i++) {
const w = new Worker(new URL("./engine.worker.js", import.meta.url), { type: "module" });
w.postMessage({ module, memory, threadId: i });
}
Shared memories require a maximum, require a cross-origin isolated page, and require a module built for threads — with atomics and passive data segments so data is initialised only once. The pattern is the foundation of building a Wasm thread pool.
Step 5 — keep views valid across growth
Whoever grows the memory — the module through memory.grow, or JavaScript through memory.grow(n) — the ArrayBuffer behind non-shared
memory is replaced, and every existing typed-array view becomes detached. Owning the memory makes it tempting to keep long-lived views; resist
that, or refresh them:
let u8 = new Uint8Array(memory.buffer);
function bytes() {
if (u8.buffer !== memory.buffer) u8 = new Uint8Array(memory.buffer); // refresh after growth
return u8;
}
Shared memory behaves differently: growth keeps the same SharedArrayBuffer object but its byteLength increases, so views remain attached but
may be shorter than the memory. Recreate them to see new pages. The details are in
why memory.grow invalidates pointers.
Agreeing on a layout with the module
Once the host writes into memory it does not fully control, the host and the module need an agreement about who uses which addresses, and it pays to make that agreement explicit rather than relying on a constant that happens to work. The cleanest arrangement is to let the module own the layout and tell the host where it may write: export a function that allocates an input buffer of a requested size and returns its address, or export globals holding the boundaries of a region reserved for the host.
const ptr = instance.exports.alloc_input(input.length); // module decides where
new Uint8Array(memory.buffer, ptr, input.length).set(input);
instance.exports.process(ptr, input.length);
That keeps the module’s allocator in charge of its heap, survives changes to the module’s static data size, and works the same whether the
memory was imported or exported. Reserve fixed offsets for the cases that genuinely need them — data written before instantiation, or a shared
header between cooperating modules — and document them next to the link flags that create the space, such as --global-base, so a later change
to the build cannot silently move the module’s data on top of the host’s.
When to provide memory and when not to
Providing memory is the right default in three situations: threaded builds, where it is required; multi-module designs that genuinely share data; and hosts that need to control memory policy, such as a plugin host enforcing a maximum per plugin regardless of what the plugin’s build declares. Outside those, letting the module define and export its memory is simpler and less error-prone — the module’s toolchain sizes it correctly, and the host cannot accidentally hand it a memory that is too small or overlapping with something else. Choosing the import form “just in case” adds configuration without benefit.
Expected output
WebAssembly.Module.imports(module); // [{ module: "env", name: "memory", kind: "memory" }]
instance.exports.memory; // undefined — imported, not exported (unless re-exported)
memory.buffer.byteLength; // 2097152 (32 pages)
Gotchas
memory import must be a WebAssembly.Memory object. The value underenv.memoryis missing or is anArrayBuffer. Pass the Memory.- Maximum missing when the module declares one. Instantiation fails. Always set a maximum no larger than the module’s.
- Prefilled data overwritten. Data segments are written during instantiation. Place host data above the module’s static region.
- Shared memory on a non-isolated page.
shared: truethrows. CheckcrossOriginIsolatedfirst.
Performance note
Providing memory has no effect on execution speed; loads and stores are compiled the same way. Prefilling input before instantiation saved one copy in a pipeline that previously instantiated, then copied the input in through an export call — about 3 ms for a 24 MB image on a laptop.
Frequently Asked Questions
Can a module both import and export its memory?
Yes — linking with --import-memory --export-memory re-exports it, which suits glue that expects exports.memory.
Can I give a module a memory larger than it asked for?
Yes. Initial size may exceed the module’s minimum; the module sees the extra pages as available heap if its allocator uses memory.size.
Can JavaScript shrink the memory? No. Memories only grow.
Does Node or Deno differ?
No; WebAssembly.Memory and imported memories work identically. Server runtimes like wasmtime have equivalent APIs in their embedding libraries.
Related
- Writing an import object by hand — memory is one entry in it.
- Sizing initial and maximum memory — choosing the numbers.
- Growing memory safely from JavaScript — growth from the host side.
- Using multiple memories in one module — when one memory is not enough.
← Back to Wasm Instantiation Lifecycle