Understanding Globals in Wasm

This page answers one question: compiled WebAssembly modules contain globals — __stack_pointer, __heap_base, odd-looking exported values — and the WebAssembly JavaScript API has a WebAssembly.Global type. You want to understand what globals are, how they differ from static variables in linear memory, how compilers use them, and when your own code should use them.

Prerequisites

  • [ ] Basic WAT reading skills.
  • [ ] A compiled module to inspect (wasm-tools print or wasm-objdump -x).
  • [ ] Familiarity with the JavaScript WebAssembly API.

Globals are typed values outside memory

A WebAssembly global holds a single value of a value type — i32, i64, f32, f64, v128 or a reference type — and is declared either immutable ((global $g i32 (i32.const 42))) or mutable ((global $g (mut i32) (i32.const 0))). Code reads it with global.get and, if mutable, writes it with global.set. Globals do not live in linear memory: they have no address, cannot be read through pointers, and cannot be corrupted by out-of-bounds memory writes. Engines usually keep them in a per-instance area and often in registers in hot code.

That distinguishes them from what C and Rust call “global variables”. A static mut COUNTER: u32 in Rust or a file-scope int counter; in C becomes a location in linear memory (in the data or BSS region), accessed through loads and stores, because it can have its address taken. Wasm globals are used by compilers for a few special values, and by hand-written modules for small amounts of state.

Wasm globals versus static variables in linear memory Wasm globals are typed values outside linear memory with no address, read and written with global.get and global.set, and safe from memory corruption. Language-level static variables live at addresses in linear memory, are accessed with loads and stores, can be pointed to, and can be overwritten by memory bugs. Wasm global typed value, no address global.get / global.set immune to memory overflows special values static in linear memory lives at an address i32.load / i32.store pointers can reach it language globals

How compilers use globals

The most important compiler-generated global is __stack_pointer, a mutable i32 holding the current top of the shadow stack in linear memory. Functions that need stack space subtract from it on entry and restore it on exit. Because it is a global — not a memory location — a buffer overflow in linear memory cannot overwrite it directly. Linkers also define globals such as __heap_base, __data_end and __memory_base/__table_base (for position-independent code in dynamic linking), and thread-local storage uses a global (__tls_base) per instance. Most of these are immutable or set once at startup.

(global $__stack_pointer (mut i32) (i32.const 1048576))
(func $uses_stack
  (local $sp i32)
  (local.set $sp (i32.sub (global.get $__stack_pointer) (i32.const 64)))
  (global.set $__stack_pointer (local.get $sp))
  ;; ... use 64 bytes at $sp ...
  (global.set $__stack_pointer (i32.add (local.get $sp) (i32.const 64))))

Initialisation with constant expressions

A global’s initial value is a constant expression: a constant, a global.get of an imported immutable global, a ref.null or ref.func, and — in newer versions of the spec (extended constant expressions) — simple arithmetic such as i32.add of constants and imported globals. That last addition lets dynamic linking compute addresses like __memory_base + offset at instantiation without running code.

Importing and exporting globals

Globals can be imported and exported, which makes them a way to share configuration between JavaScript and a module:

const debugLevel = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
const { instance } = await WebAssembly.instantiate(bytes, { env: { debug_level: debugLevel } });
debugLevel.value = 3;                                       // the module sees 3 on its next global.get
console.log(instance.exports.items_processed.value);        // read an exported global
(import "env" "debug_level" (global $debug (mut i32)))
(global $items (export "items_processed") (mut i32) (i32.const 0))

Mutable imported and exported globals (the “mutable globals” feature, long since standard) are shared by reference: JavaScript and every instance that imports the same WebAssembly.Global see the same value. Immutable globals are copied at instantiation.

A mutable global shared between JavaScript and a module JavaScript creates a mutable WebAssembly.Global and passes it as an import. The module reads it with global.get and may write it with global.set. JavaScript reads and writes the same value through the Global object's value property, so changes on either side are visible to the other. new WebAssembly. Global mutable i32 pass as import env.debug_level module global.get / set same storage JS reads .value sees module writes JS writes .value module sees it

When to use globals yourself

In hand-written WAT and small modules, globals are ideal for a handful of scalar values: a counter, a configuration flag, a pointer to a buffer. In compiled languages, you rarely create Wasm globals directly — the compiler decides — but you meet them when reading output, configuring linkers, or exporting values for JavaScript. For anything larger than a few scalars, or anything that needs an address, use linear memory.

Threads and globals

Each instance has its own globals, even when instances share a memory. In threaded programs, that is what makes __stack_pointer per-thread: every thread’s instance has its own stack pointer pointing to its own stack region. It also means a global is not a way to share state between threads — use shared memory and atomics for that.

Globals in dynamic linking

Position-independent code uses globals to find its data and functions. A side module compiled with -fPIC does not know where its static data or table entries will end up, so it imports __memory_base and __table_base as immutable i32 globals and computes addresses relative to them; references to data in other modules go through imported globals in a “global offset table” (GOT.mem and GOT.func imports), which the dynamic loader fills in at instantiation. Reading a side module’s WAT, you therefore see many imported globals and address arithmetic such as global.get $__memory_base followed by i32.add. Extended constant expressions let some of these computations happen in global initialisers rather than in code, which reduces startup work in dynamically linked programs.

Exported globals for diagnostics

Exporting a few mutable globals is a cheap way to expose diagnostics without function calls: a counter of processed items, the current phase of a long operation, the last error code. JavaScript can read them at any time through WebAssembly.Global.value — even while a worker is busy, if the global is shared appropriately — and a debug panel can poll them. Because reading a global requires no call into the module, it does not disturb the module’s execution or allocate. For values that must be consistent with each other, though, read them together after a call returns, since there is no atomic snapshot of several globals.

Tooling

wasm-objdump -x -j Global lists globals with types, mutability and initial values; wasm-tools print shows them in WAT. When a module fails to link with “imported global type mismatch”, compare the declared type and mutability with what JavaScript provides — a mutable import requires a mutable WebAssembly.Global, and plain numbers can only satisfy immutable imports.

A quick experiment

Write a three-line WAT module with a mutable exported global and a function that increments it, instantiate it in the browser console, call the function and read exports.counter.value. Then import a WebAssembly.Global created in JavaScript instead, change its value from JavaScript and watch the module read it. Five minutes of this makes the sharing semantics concrete.

Expected output

You can read (global $__stack_pointer (mut i32) ...) and explain its role, distinguish Wasm globals from static variables in linear memory, import and export globals with WebAssembly.Global, explain why a memory overflow cannot directly overwrite the stack pointer, and choose globals for small scalar state in hand-written modules.

Gotchas

  • Expecting C globals to be Wasm globals. They live in linear memory. Look for loads and stores.
  • Sharing state between threads with globals. Each instance has its own. Use shared memory.
  • Mutating an immutable imported global. Validation fails. Declare mut on both sides.
  • Initialising with non-constant code. Only constant expressions are allowed.
  • Assuming exported immutable globals update. They are fixed at instantiation.
  • Passing a plain number for a mutable global import. Linking fails. Provide a mutable WebAssembly.Global.

Performance note

global.get and global.set compile to a load or store from the instance’s globals area, and engines often keep hot globals in registers within a function; accessing a static in linear memory costs a similar load but cannot be cached as aggressively across stores to memory, because any store might alias it.

Relative cost of reading a counter in a hot loop Relative time for a hot loop that reads and increments a counter kept as a mutable Wasm global and as a static variable in linear memory, measured in an optimising tier. relative loop time mutable Wasm global 1 × static in linear memory 1.1 ×

Frequently Asked Questions

Can a global hold a reference? Yes — externref and funcref globals hold references.

Can I take the address of a global? No — globals have no address. Copy the value into memory if you need a pointer.

Are globals visible in DevTools? DevTools shows module globals in the scope view when paused in Wasm code.

Do globals count against memory? No — they are separate from linear memory.

Why do side modules import so many globals? Position-independent code finds its data and functions through __memory_base, __table_base and GOT globals filled in by the loader.

Can JavaScript read exported globals while the module runs? Yes, through .value; read related globals together after a call for a consistent view.

What does “imported global type mismatch” mean? The type or mutability of the provided value does not match the module’s declaration.

How do I list a module’s globals? wasm-objdump -x -j Global shows types, mutability and initial values.

Are extended constant expressions widely supported? They are in current engines and used by toolchains for dynamic linking; check your oldest supported engine if you rely on them.

← Back to Stack vs Heap Execution Model