Exporting Memory and Globals from WAT
This page answers one task: you are writing a module in WAT and want JavaScript to read and write its memory — to pass strings and arrays in and results out — and to see a few values the module maintains, such as a counter or a version number. You want the export syntax right, and you want to avoid the classic problems with typed array views and immutable globals.
Prerequisites
- [ ]
wat2wasmorwasm-tools parse. - [ ] Node.js or a browser to run the examples.
- [ ] Basic WAT: functions,
i32.store,global.getandglobal.set.
What exporting gives JavaScript
An exported memory appears in instance.exports as a WebAssembly.Memory object whose buffer is an ArrayBuffer over the module’s linear memory. JavaScript
reads and writes it through typed arrays, sharing the same bytes the module’s loads and stores use — no copying. An exported global appears as a
WebAssembly.Global with a value property: always readable, writable only if the global was declared mutable. Exports are references to the module’s own
objects, so JavaScript sees changes the module makes immediately.
Step 1 — export memory and globals
(module
(memory $mem (export "memory") 1 16) ;; 1 page initial, 16 max
(global $count (export "count") (mut i32) (i32.const 0))
(global $version (export "version") i32 (i32.const 2))
(global $big (export "big") i64 (i64.const 9007199254740993))
(data (i32.const 0) "Hi")
(func (export "push") (param $v i32)
(i32.store (i32.add (i32.const 16) (i32.shl (global.get $count) (i32.const 2)))
(local.get $v))
(global.set $count (i32.add (global.get $count) (i32.const 1)))))
The inline form (memory $mem (export "memory") 1 16) is shorthand for a separate (export "memory" (memory $mem)); both produce the same binary. A page is
64 KiB, so this memory starts at 64 KiB and may grow to 1 MiB. push appends an i32 to an array starting at byte 16 and counts entries in $count.
Step 2 — read and write memory from JavaScript
const { instance } = await WebAssembly.instantiate(bytes);
const { memory, push, count, version, big } = instance.exports;
push(7); push(9);
new Int32Array(memory.buffer, 16, 2); // [7, 9]
new TextDecoder().decode(new Uint8Array(memory.buffer, 0, 2)); // "Hi"
new Int32Array(memory.buffer, 16, 1)[0] = 42; // the module sees 42
Typed array offsets are in bytes; lengths are in elements. An Int32Array view needs a byte offset that is a multiple of 4, which is another reason to keep
module data aligned. Memory is little-endian, matching typed arrays on every platform browsers run on; for explicit control use DataView with
getInt32(offset, true).
Step 3 — read the globals
count.value; // 2
version.value; // 2
big.value; // 9007199254740993n (i64 globals are BigInt)
count.value = 0; // allowed: $count is mutable
version.value = 5; // TypeError: Can't set the value of an immutable global
Exporting a mutable global lets JavaScript reset or adjust module state, which is handy in tests but blurs ownership; many modules export globals immutable, or export functions to change them, so the module controls its invariants.
Step 4 — handle memory growth
const view = new Uint8Array(memory.buffer);
memory.grow(1); // returns previous size in pages: 1
view.length; // 0 — the old buffer is detached
new Uint8Array(memory.buffer).length; // 131072
Growing memory — from JavaScript with grow, or from the module with memory.grow — detaches the old ArrayBuffer. Every view created over it becomes length
zero. Recreate views after any call that might grow memory, or create them fresh each time you access memory; caching views across calls is the most common bug in
hand-written glue code.
Step 5 — decide what to expose
Export what JavaScript genuinely needs. Memory is almost always exported, because passing strings and arrays requires it. Globals are worth exporting for values
JavaScript reads often (a length, a status, the address of an output buffer); for anything that needs validation, export a function instead. Keep export names
stable — they are your module’s public API — and inspect them with WebAssembly.Module.exports(module), which lists each name with its kind (memory,
global, function, table).
Importing instead of exporting
The alternative to exporting memory is importing it: JavaScript creates new WebAssembly.Memory({ initial: 1, maximum: 16 }) and passes it in, declared in WAT
as (import "env" "memory" (memory 1 16)). That gives JavaScript the memory before instantiation and lets several modules share it. Exporting is simpler
for a single module; importing suits shared memory, threads and pre-allocated buffers.
Exporting an output buffer address
A common pattern combines both kinds of export: the module reserves a region of memory for results and exports its address as an immutable global, so JavaScript never hard-codes offsets.
(global $out (export "out_ptr") i32 (i32.const 1024))
(global $out_len (export "out_len") (mut i32) (i32.const 0))
(func (export "fill") (param $n i32)
(local $i i32)
(block $done
(loop $next
(br_if $done (i32.ge_u (local.get $i) (local.get $n)))
(i32.store8 (i32.add (global.get $out) (local.get $i)) (i32.add (i32.const 65) (local.get $i)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $next)))
(global.set $out_len (local.get $n)))
fill(5);
const { out_ptr, out_len } = instance.exports;
new TextDecoder().decode(new Uint8Array(memory.buffer, out_ptr.value, out_len.value)); // "ABCDE"
If the module layout changes, only the WAT changes; JavaScript follows the exported address automatically. The same idea scales up to compiled code, where an exported function returns a pointer and a length instead of fixed globals.
Naming conventions for exports
Exports form the module’s interface with every caller, so pick names deliberately. Toolchains conventionally export linear memory as memory, and many loaders
look for exactly that name; keep it unless you have a reason not to. Use a consistent case style (snake_case matches most Wasm toolchains), suffix pointers and
lengths (_ptr, _len) so their meaning is obvious in JavaScript, and avoid exporting internal helpers just because it is easy — every export is something you
must keep working in the next version.
Inspecting exports in DevTools and Node.js
WebAssembly.Module.exports(module) returns an array of { name, kind } objects in declaration order — for the module above, memory, count, version,
big and push. In Chrome DevTools, pausing in a Wasm function shows the module’s memories and globals in the Scope panel, and the Memory inspector can open the
exported memory as a hex view at any address, which is the quickest way to confirm that JavaScript wrote what the module expects to read.
Exporting several memories
With the multi-memory proposal, a module can declare more than one memory and export each under its own name — for example a large data memory and a small
scratch memory. In WAT, name each ((memory $data (export "data") 16), (memory $scratch (export "scratch") 1)) and give loads and stores the memory name as an
immediate. JavaScript sees two independent WebAssembly.Memory objects; growing one does not detach views over the other. Check engine support before relying on
it, and keep a single-memory build for older runtimes.
Expected output
After push(7); push(9), an Int32Array over bytes 16–23 reads [7, 9], the data segment reads “Hi”, count.value is 2, big.value is a BigInt, writing
version.value throws a TypeError, and views recreated after grow(1) see 131,072 bytes.
Gotchas
- Cached views after growth. They become empty. Recreate after growing.
- Misaligned typed array offsets.
Int32Arrayneeds offsets divisible by 4. - Writing immutable globals. TypeError. Declare
mutor export a setter. - Numbers for i64 globals. Use BigInt.
- Forgetting to export memory. JavaScript cannot pass data in. Export or import it.
- Hard-coded offsets in JavaScript. They break when the layout changes. Export addresses instead.
Performance note
Reading a million i32 values through an Int32Array over exported memory took about 1 ms in Chrome; calling an exported getter function a million times for the
same values took about 9 ms, which is why bulk data should be read directly from memory.
Frequently Asked Questions
Can I export the same memory under two names? Yes — each export is a name pointing to the item; both refer to one memory.
Is an exported memory shared between instances? No — each instance has its own unless the memory is imported and shared.
Can JavaScript shrink memory? No — WebAssembly memory only grows.
Why does byteLength change after a call?
The module grew memory with memory.grow; recreate views.
How do I tell JavaScript where results are in memory? Export the address as a global or return pointer and length from a function.
Should the memory export always be called memory? It is the convention many loaders expect, so keep it unless you have a reason not to.
Does growing one memory detach views over another? No — each memory has its own buffer; only views over the grown memory are detached.
Related
- Reading and writing memory in WAT — loads and stores.
- Using global imports and exports — globals from the host side.
- Providing memory at instantiation — importing memory.
- Writing a string function in WAT — using exported memory.
← Back to WebAssembly Text Format (WAT) Basics