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

  • [ ] wat2wasm or wasm-tools parse.
  • [ ] Node.js or a browser to run the examples.
  • [ ] Basic WAT: functions, i32.store, global.get and global.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.

A module's memory and globals as seen from JavaScript The WAT module declares a memory and globals and exports them by name. After instantiation, JavaScript receives a WebAssembly.Memory and WebAssembly.Global objects. Typed array views over memory.buffer read and write the same bytes the module uses; global.value reads the current value and writes mutable globals. declare + export (memory (export …)) instantiate exports object WebAssembly. Memory .buffer → typed arrays WebAssembly. Global .value read / write module sees changes same objects

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.

What JavaScript can do with each exported item An exported memory can be read and written through typed arrays and grown with grow. A mutable global can be read and written through value. An immutable global can only be read. i64 globals use BigInt for value. export JS object read write memory WebAssembly.Memory typed arrays on .buffer typed arrays, .grow() mut i32 / f32 / f64 global WebAssembly.Global .value .value = n immutable global WebAssembly.Global .value no (TypeError) i64 global WebAssembly.Global .value as BigInt if mutable (BigInt)

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. Int32Array needs offsets divisible by 4.
  • Writing immutable globals. TypeError. Declare mut or 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.

Reading one million values from a module Milliseconds to read one million i32 values from a module through a typed array over exported memory, through an exported global read in a loop, and by calling an exported getter function per value. ms for 1M reads typed array over memory 1 ms exported global .value 4.5 ms exported getter per value 9 ms

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.

← Back to WebAssembly Text Format (WAT) Basics