WebAssembly Global Variables and How to Mutate Them

WebAssembly (Wasm) global variables are accessible across different functions within a Wasm module and can even be shared between the module and its host environment, such as JavaScript. This article explores what Wasm global variables are, how they are defined, and the specific mechanisms required to mutate (change) their values both inside WebAssembly and from the host environment.

What Are WebAssembly Global Variables?

In WebAssembly, a global variable is a storage location that holds a single numeric value of a specific primitive type: i32, i64, f32, or f64. Global variables are declared at the module level, meaning they exist outside the scope of individual functions.

Wasm globals serve two primary purposes: 1. Internal State: Keeping track of module-wide state that multiple functions need to read or write. 2. Host Sharing: Sharing configuration, pointers, or state between the WebAssembly module and the host environment (like a web browser or Node.js).

By default, global variables in WebAssembly are immutable (read-only constants). To make a global variable changeable, you must explicitly declare it as mutable.

Defining Globals in WebAssembly Text Format (WAT)

To understand how globals work, it is helpful to look at how they are defined in WebAssembly Text Format (WAT).

Immutable Global

An immutable global is defined with a type and an initializing expression:

(global $MAX_CONNECTIONS i32 (i32.const 100))

Mutable Global

A mutable global wraps the type in a mut keyword:

(global $visitor_count (mut i32) (i32.const 0))

How to Mutate Wasm Globals Inside WebAssembly

Within a WebAssembly module, global variables are manipulated using two specific instructions: global.get and global.set.

Example: Mutating a Global in WAT

Below is a simple Wasm function that increments a mutable global variable named $counter by 1:

(module
  ;; Define a mutable global variable
  (global $counter (mut i32) (i32.const 0))

  ;; Function to increment the global
  (func (export "increment")
    ;; Load the current value of $counter onto the stack
    global.get $counter
    
    ;; Load the constant 1 onto the stack
    i32.const 1
    
    ;; Add the two values
    i32.add
    
    ;; Pop the result and write it back to $counter
    global.set $counter
  )
)

If you attempt to use global.set on an immutable global (one declared without mut), the WebAssembly module will fail to validate and compile.

How to Mutate Wasm Globals from JavaScript

WebAssembly globals can be exported to the JavaScript host environment or imported from it. To interact with Wasm globals in JavaScript, the WebAssembly.Global API is used.

Creating and Mutating Globals in JavaScript

You can create a mutable global in JavaScript and pass it to a WebAssembly module during instantiation:

// Create a mutable i32 global initialized to 10
const jsGlobal = new WebAssembly.Global({ value: 'i32', mutable: true }, 10);

// Mutate the global variable from JavaScript
jsGlobal.value = 20;

console.log(jsGlobal.value); // Outputs: 20

Mutating Exported Globals

If a WebAssembly module exports a mutable global, JavaScript can access and mutate it directly through the module’s exports object:

const importObject = {};

WebAssembly.instantiateStreaming(fetch('module.wasm'), importObject)
  .then(obj => {
    // Access the exported mutable global
    const counter = obj.instance.exports.counter;
    
    // Read the value
    console.log(counter.value); 
    
    // Mutate the value from JavaScript
    counter.value = 42; 
    
    // Call a Wasm function that uses this updated global
    obj.instance.exports.printCounter(); 
  });

If the exported global is immutable (not declared with mut in the Wasm code), attempting to assign a new value to global.value in JavaScript will throw a TypeError.