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-argaccess. - [ ]
wasm-ldandwasm-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.
Step 1 — link with --import-memory
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.
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 amaximumno larger than the module’s.LinkError: ... memory import must be a WebAssembly.Memory object. The import object key was wrong (memoryat the top level instead of underenv). 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: trueis 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.
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.
Related
- Providing memory at instantiation — the JavaScript side in more depth.
- Sizing initial and maximum memory — choosing the numbers.
- Building a Wasm thread pool — the main consumer of shared imported memory.
- Exporting symbols with wasm-ld flags — the export side of the same link.
← Back to Linking Wasm Objects with wasm-ld