Importing Memory from the Host with --import-memory

This page answers one task: build a WebAssembly module that uses a linear memory created by the host — by JavaScript, or by an embedding runtime — rather than one it defines itself, and wire the two together at instantiation.

Prerequisites

  • [ ] clang targeting wasm32 (LLVM, the WASI SDK or emsdk) or Rust with -C link-arg access.
  • [ ] wasm-ld and wasm-objdump.
  • [ ] A reason to need it — see the next section — because the default is simpler.

Why a module would import its memory

By default, wasm-ld makes the module define its memory: the memory section declares initial and maximum sizes, and the module exports it as memory so the host can reach it. The instance creates the memory when it is instantiated, and the memory lives and dies with that instance.

Importing reverses the relationship. The host creates a WebAssembly.Memory, and the module receives it as an import at instantiation. Three situations need that. Threads require it in practice: every worker instantiates the same module, and all of them must share one memory, which the main thread creates as shared and passes to each. Several modules sharing one memory — a main module and side modules, or modules from different languages working on one buffer — need a memory that none of them owns. And a host that wants to set up memory before the module runs — preloading data, or controlling the maximum size by policy — can do so only if it creates the memory.

Module-defined memory versus host-imported memory A module that defines its memory creates it at instantiation and exports it. A module that imports its memory receives one created by the host, which allows sharing between workers and modules and lets the host size and prefill it. defined + exported (default) created with each instance one instance, one memory sizes fixed at link time host reaches it via exports.memory single-threaded, single module imported (--import-memory) created by the host first shareable across workers and modules host chooses sizes within limits host can prefill before start threads, multi-module, host policy
clang --target=wasm32 -O2 -nostdlib -fvisibility=hidden \
  -Wl,--no-entry -Wl,--import-memory \
  -Wl,--initial-memory=1048576 -Wl,--max-memory=67108864 \
  -Wl,--export=sum_bytes kernel.c -o kernel.wasm

--initial-memory and --max-memory are in bytes and must be multiples of the 64 KiB page size. With an imported memory they become the module’s requirements: the memory the host provides must be at least the initial size, and — if the module declares a maximum — must have a maximum no larger than the module’s. The host may provide more initial memory than requested.

Inspect the import:

wasm-objdump -x -j Import kernel.wasm
Import[1]:
 - memory[0] pages: initial=16 max=1024 <- env.memory

The module name is env and the field name memory by default. They become the keys in the import object.

Step 2 — create the memory in JavaScript and pass it

const memory = new WebAssembly.Memory({ initial: 16, maximum: 1024 });   // pages of 64 KiB

// prefill before the module runs, if useful
new Uint8Array(memory.buffer, 65536, 4).set([1, 2, 3, 4]);

const { instance } = await WebAssembly.instantiateStreaming(
  fetch("kernel.wasm"),
  { env: { memory } }
);
console.log(instance.exports.sum_bytes(65536, 4));   // 10

The module’s static data — string literals, initialised globals — is written into the provided memory during instantiation by its active data segments. Anything you prefill must avoid those regions; the linker places static data starting at --global-base (1024 by default), and the stack after it, so prefill above the module’s __heap_base or coordinate addresses through an exported allocator. Details of the layout are in setting stack size and memory limits at link time.

Instantiating a module that imports its memory JavaScript creates a WebAssembly.Memory, optionally writes data into it, and instantiates the module with the memory in the import object. The engine checks the sizes against the module's limits, writes the module's data segments into the memory, and the instance then reads and writes that same memory. JavaScript host engine module instance new WebAssembly.Memory({initial:16, maximum:1024}) instantiate(bytes, { env: { memory } }) check initial ≥ 16 pages and maximum ≤ 1024 write data segments into the imported memory exports ready; same memory.buffer

Step 3 — share it between workers

For threads, the memory must be shared, and the module must be linked to expect a shared memory:

clang --target=wasm32 -O2 -pthread -matomics -mbulk-memory -nostdlib \
  -Wl,--no-entry -Wl,--import-memory -Wl,--shared-memory \
  -Wl,--initial-memory=1048576 -Wl,--max-memory=268435456 \
  -Wl,--export=worker_main kernel_mt.c -o kernel_mt.wasm
const memory = new WebAssembly.Memory({ initial: 16, maximum: 4096, shared: true });
const module = await WebAssembly.compileStreaming(fetch("kernel_mt.wasm"));
for (let i = 0; i < 4; i++) {
  const w = new Worker("worker.js", { type: "module" });
  w.postMessage({ module, memory, id: i });     // both can be posted to workers
}

A shared memory must declare a maximum, and memory.buffer is a SharedArrayBuffer — available only on cross-origin isolated pages. The data segments of a threaded module are passive and initialised once, by the first instance, using a flag in memory; wasm-ld generates that logic when you link with --shared-memory. The worker side is covered in sharing memory between Wasm and Web Workers.

Step 4 — do the same from Rust

Rust’s wasm32-unknown-unknown target defines its memory by default. Pass the linker flags through RUSTFLAGS or a .cargo/config.toml:

# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = ["-C", "link-arg=--import-memory", "-C", "link-arg=--initial-memory=1048576",
             "-C", "link-arg=--max-memory=67108864"]

wasm-bindgen recognises an imported memory and generates glue that creates it — or, for threaded builds, accepts one passed in through the initialiser. For non-bindgen modules, provide the memory in the import object exactly as in step 2.

Who should own the memory

It is worth deciding ownership deliberately, because it shapes the rest of the integration. When the module owns its memory, the module is self-contained: instantiate it and it works, and the host reaches its data through exports. That is the right default for a single component used by one page, and it keeps the JavaScript side simple — no memory to create, size or pass around.

When the host owns the memory, the host takes on responsibilities. It must create memory large enough for the module’s static data and stack, choose a maximum that suits the whole application rather than one module, and know which regions the module will use so it does not write over them. In exchange it gains control: one memory shared by several modules or many workers, data placed before the module starts, and limits enforced by the application’s own policy.

A useful rule is that the component closest to the resource’s lifetime should own it. A memory shared by a pool of workers lives as long as the pool, so the code that manages the pool should create it. A memory used only by one instance lives as long as that instance, so the module should define it. Mixed arrangements — a module that defines its memory but whose host writes into it heavily from outside — usually work better with the ownership made explicit through an import.

Step 5 — choose the import name deliberately

env.memory is the conventional name and what most toolchains expect. If a module must import from a namespace that matches your host’s conventions — for example when composing modules from different sources — rename it at link time:

wasm-ld ... --import-memory=host,shared_heap ...

The import then appears as host.shared_heap. Keep a single convention across a project; mismatched import names fail at instantiation with a LinkError that names the missing field.

Expected output

console.log(instance.exports.memory);                 // undefined — imported, not exported
console.log(memory.buffer.byteLength);                // 1048576 (16 pages)
console.log(WebAssembly.Module.imports(module));
// [ { module: 'env', name: 'memory', kind: 'memory' } ]

If the host also needs the memory accessible as an export — some glue expects exports.memory — add --export-memory, which re-exports the imported memory.

Gotchas

  • LinkError: WebAssembly.instantiate(): memory import has no maximum limit, expected at most 1024. The module declares a maximum and the host memory has none. Give the host memory a maximum no larger than the module’s.
  • LinkError: ... memory import must be a WebAssembly.Memory object. The import object key was wrong (memory at the top level instead of under env). Match the module and field names exactly.
  • Data the host wrote is overwritten. Active data segments are written at instantiation, after the host’s prefill. Prefill above __heap_base, or write after instantiation.
  • Shared memory fails with a type error. The page is not cross-origin isolated, so shared: true is rejected. See detecting cross-origin isolation at runtime.

Performance note

Importing memory has no measurable effect on execution speed — loads and stores are compiled identically whether the memory was defined or imported. Shared memories do cost more to grow: in Chrome, growing a 64 MB shared memory by 64 MB took about 0.4 ms against 0.1 ms for a non-shared one, because every agent must observe the new size. Size shared memories generously up front.

Cost of memory.grow by 64 MB, defined versus shared imported memory Time for a single memory.grow of 1,024 pages in Chrome, for a module-defined memory, a host-imported non-shared memory and a host-imported shared memory. milliseconds per grow of 64 MB module-defined memory 0.1 ms imported, not shared 0.1 ms imported, shared 0.4 ms

Frequently Asked Questions

Can a module both import and define memories? With the multiple-memories proposal, yes — a module can import one memory and define another. Toolchain support is still limited; see using multiple memories in one module.

Does Emscripten import or define memory? Emscripten builds define and export memory by default and switch to importing for threaded builds and some dynamic-linking configurations; -sIMPORTED_MEMORY forces importing.

Can the host shrink the memory later? No. WebAssembly memories only grow — see why Wasm memory never shrinks.

Is it safe to keep a typed array over the imported memory? Only until the memory grows. Growth replaces memory.buffer, so views must be recreated afterwards, exactly as with an exported memory.

Do WASI runtimes support imported memory? Yes, through their embedding APIs — wasmtime’s Memory::new passed to a linker definition, for example. The CLI runners assume modules define their own memory.

← Back to Linking Wasm Objects with wasm-ld