Sharing One Memory Between Two Modules

This page answers one task: two WebAssembly modules — a core library and a plugin, or a decoder and a renderer built separately — need to work on the same data without copying it between their memories, so you want both instances to use one WebAssembly.Memory, safely.

Prerequisites

  • [ ] Two modules you can relink, both able to import their memory instead of defining it.
  • [ ] Control over each module’s link flags (global base, stack, heap).
  • [ ] A clear owner for allocation.

Why share a memory, and why it is tricky

Each instance normally has its own linear memory. Passing data between two instances means copying bytes from one memory to the other through JavaScript — cheap for small messages, expensive for frames of video or large documents processed by both. If both modules import the same WebAssembly.Memory, a pointer from one is valid in the other, and they can share data with no copies at all.

The difficulty is that each module was linked as if it owned the whole memory. Each has static data at its own addresses, its own shadow stack, and its own heap allocator that assumes everything above __heap_base is free. Two modules linked with default settings put their static data at the same addresses and their heaps over each other. Sharing works only if the layouts are made disjoint at link time and allocation is coordinated at runtime.

One memory shared by two modules The shared memory is partitioned. Module A's static data and stack occupy a low region, module B's static data and stack a region above it, and a single heap above both is managed by one allocator that both modules call, so pointers from either module are valid in the other. one WebAssembly.Memory imported by modules A and B A data + stack B data + stack shared heap (one allocator) 0 B base heap start end

Tell the linker to import memory instead of defining it, with matching limits:

# Module A (C, via clang/wasm-ld)
clang --target=wasm32 -nostdlib -O2 a.c -o a.wasm \
  -Wl,--import-memory -Wl,--initial-memory=16777216 -Wl,--max-memory=268435456 \
  -Wl,--no-entry -Wl,--export-dynamic

For Rust, pass -C link-arg=--import-memory and the same limits. Both modules’ memory imports must be compatible with the one memory you create: its initial size must be at least each module’s declared minimum, and its maximum must satisfy each module’s declared maximum.

Step 2 — give each module its own region for data and stack

Use --global-base so the second module’s static data starts above the first module’s, and size the stacks explicitly:

# A: data from 1 KiB, 256 KiB stack placed first
-Wl,--stack-first -Wl,-z,stack-size=262144 -Wl,--global-base=1024
# B: everything above A's region (say from 2 MiB)
-Wl,--global-base=2097152 -Wl,-z,stack-size=262144

Check the result with wasm-objdump -x: data segment offsets and the initial __stack_pointer of each module must fall in disjoint ranges. With --stack-first only one module can have its stack at the bottom; place the other module’s stack inside its own region (the default layout puts it after its data). Leave a gap between regions so small changes in one module’s data size do not require relinking the other.

Step 3 — use one allocator for the shared heap

Two allocators managing the same heap will hand out overlapping blocks. Choose one module — usually the core library — as the owner of the heap, and have the other module import its malloc and free instead of linking its own:

// module B: allocation comes from module A
__attribute__((import_module("a"), import_name("malloc"))) void* malloc(unsigned long);
__attribute__((import_module("a"), import_name("free")))   void  free(void*);
const memory = new WebAssembly.Memory({ initial: 256, maximum: 4096 });
const a = await WebAssembly.instantiate(aBytes, { env: { memory } });
const b = await WebAssembly.instantiate(bBytes, {
  env: { memory },
  a: { malloc: a.instance.exports.malloc, free: a.instance.exports.free },
});

Exports of one instance can be passed directly as imports of another; calls between them go through the engine without a JavaScript trampoline in most engines. The heap owner’s __heap_base must lie above both modules’ static regions — link the owner with a heap start beyond the second module’s region, or have the allocator initialised with an explicit start address.

Instantiating two modules on one memory JavaScript creates one memory. Module A is instantiated with it and exports malloc and free. Module B is instantiated with the same memory and A's allocator functions as imports. A pointer allocated by either module is now valid in both, and data passes between them without copying. new WebAssembly. Memory shared, sized for both instantiate A owns heap + allocator instantiate B imports memory + A.malloc B allocates via A one heap, no overlap pointers valid in both zero-copy exchange

Step 4 — pass pointers, not copies

With the layout and allocator settled, module A can produce a buffer and hand its pointer and length to module B, which reads it directly. Ownership must be explicit — which module frees the buffer, and when — because neither language’s type system can see across modules. A simple rule: the module that allocates frees, and functions that take a pointer document whether they borrow it for the call or take ownership.

Step 5 — test for overlap and corruption

Write a test that exercises both modules heavily — allocations from both, deep stacks in both — and checks invariants afterwards: each module’s static data unchanged (compare checksums of known globals), allocations from either module disjoint, and no traps. Corruption from overlapping layouts tends to appear only under load, so the test should push both modules hard. Build both in CI from pinned toolchains, since a toolchain update that grows one module’s static data can push it into the other’s region.

Threads and shared memory

If the memory is also shared between threads (shared: true), each module’s stack must exist per thread, and the single allocator must be thread-safe. This combination is complex; prefer it only when both requirements — cross-module and cross-thread sharing — are real. The usual threaded setups run multiple instances of the same module on one memory, which the toolchain’s thread support already coordinates.

Alternatives that do the coordination for you

Hand-partitioning memory is fragile. Two toolchain-supported alternatives exist. Dynamic linking (Emscripten’s MAIN_MODULE/SIDE_MODULE, or wasm-ld’s -shared with position-independent code) lets a side module be loaded into a main module’s memory with relocations applied at load time; the loader places its data, and one libc and allocator serve both — see building shared libraries for Wasm dynamic linking. The Component Model takes the opposite approach: components never share memory, and data crosses boundaries by copying through the canonical ABI, which is safe and language-neutral at the cost of copies. Choose manual sharing only when copies are too expensive and dynamic linking does not fit.

Memory growth with two instances

When either module grows the shared memory — directly or through the allocator — the growth applies to the one WebAssembly.Memory, so both instances see the larger size immediately; that is the point of sharing. JavaScript views are a different story: any Uint8Array created over memory.buffer before growth is detached afterwards, regardless of which module grew it. Wrappers for both modules must re-create views after any call that might allocate, or read memory.buffer fresh each time. A shared helper that returns a current view, used by both wrappers, avoids each one keeping its own stale copy. Growth is also limited by the smallest declared maximum among the importing modules, so give both modules the same maximum to avoid one module unexpectedly capping the other’s growth.

Versioning the shared contract

Two modules sharing memory are coupled more tightly than two modules exchanging messages: the layout, the allocator interface, and the meaning of every pointer passed between them form a contract that both builds must honour. Treat it as a versioned interface. Have each module export a small layout descriptor — its static region bounds and an ABI version number — and have the loader check at instantiation that regions do not overlap and versions match, failing with a clear error otherwise. That turns a mismatched deployment, where one module was rebuilt and the other was not, into an immediate load error rather than memory corruption that surfaces hours later.

Expected output

Both modules import one 16 MiB memory; wasm-objdump shows A’s region at 0–2 MiB and B’s at 2–3 MiB with the heap from 3 MiB; module B allocates through A’s malloc; a 50 MB video frame produced by A is processed by B in place with no copy; and a stress test runs 10 minutes of interleaved allocation and recursion with no corruption.

Gotchas

  • Default global base in both modules. Static data overlaps. Set --global-base per module.
  • Two allocators on one heap. Blocks overlap. Use exactly one allocator.
  • Heap base inside another module’s region. The allocator hands out B’s statics. Start the heap above both.
  • Unclear ownership of buffers. Double frees or leaks. Document who frees.
  • Toolchain updates changing layout. Re-verify regions in CI.

Performance note

Passing a 50 MB frame between two modules by copying through JavaScript took 21 ms per frame; sharing the memory and passing a pointer took under 0.01 ms.

Handing a 50 MB frame from one module to another Milliseconds to make a 50 megabyte frame available to a second module by copying between separate memories through JavaScript, and by passing a pointer into a shared memory. ms per frame handed over copy between memories 21 ms pointer into shared memory 0.0 ms

Frequently Asked Questions

Can Rust modules share memory this way? Yes, with --import-memory and careful layout; the default allocator must be replaced by an imported one in all but one module.

Does wasm-bindgen support this? Not directly; wasm-bindgen assumes one module owns its memory and glue.

Is multi-memory an alternative? Multi-memory lets one module use several memories; it does not by itself coordinate layouts between modules.

What about Emscripten builds? Use its dynamic-linking support rather than manual sharing.

What happens to JavaScript views when one module grows the shared memory? They detach for both modules’ wrappers; re-create views after any call that might allocate.

← Back to Linear Memory Management & Allocators