Getting Started
Loading Wasm in JS
WebAssembly (Wasm) is a binary format that runs at near-native speed in browsers. instantiateStreaming loads and compiles a .wasm file. Exports are functions callable from JavaScript.
// Load and instantiate a .wasm module
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
const { instance } = result;
// call exported functions
console.log(instance.exports.add(2, 3)); // 5
});
// Alternative: from ArrayBuffer
const bytes = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
WebAssembly.instantiate(bytes).then(result => {
console.log(result.instance.exports);
});WebAssembly Module & Instance
WebAssembly.Module is the compiled, stateless representation — safe to reuse across multiple instances. WebAssembly.Instance holds runtime state (memory, globals). Compiling once and instantiating many times is efficient for spawning parallel workers.
// Two-step: compile then instantiate
const module = await WebAssembly.compile(bytes);
const instance = await WebAssembly.instantiate(module, importObject);
console.log(instance.exports.add(1, 2));
// Module is stateless & shareable across instances
const instance2 = await WebAssembly.instantiate(module, importObject);
// Each instance has its own memory and globalsBrowser Support & Feature Detection
WebAssembly 1.0 is supported in all modern browsers. Newer proposals (SIMD, exceptions, threads, GC) need feature detection. WebAssembly.validate checks if bytes form a valid module without compiling.
// Check basic WebAssembly support
if (typeof WebAssembly !== 'undefined') {
console.log('WebAssembly is supported');
}
// Verify the engine can actually compile
WebAssembly.validate(new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00
])); // true if valid module
// Feature-detect newer proposals
const hasSIMD = typeof WebAssembly.Module !== 'undefined';
const hasExceptions = 'exceptions' in WebAssembly;Streaming vs Non-Streaming
instantiateStreaming compiles the module as it streams over the network — faster startup. The server MUST send 'Content-Type: application/wasm'. If using a CDN or static host without that header, fall back to the ArrayBuffer path.
// Streaming (preferred): compiles while downloading
WebAssembly.instantiateStreaming(fetch('m.wasm'), imports);
// Non-streaming: must download fully first
const res = await fetch('m.wasm');
const buf = await res.arrayBuffer();
WebAssembly.instantiate(buf, imports);
// Streaming requires correct MIME type:
// Content-Type: application/wasmWasm Magic Number & Header
The Wasm binary magic number is the ASCII string '\0asm' followed by a 4-byte little-endian version. This header lets you validate a file is Wasm before compilation. The format is organized into sections (Type, Import, Function, Memory, etc.).
// Every .wasm file starts with this 8-byte header:
// 0x00 0x61 0x73 0x6d -> "\0asm"
// 0x01 0x00 0x00 0x00 -> version 1
const magic = [0x00, 0x61, 0x73, 0x6d];
const version = [0x01, 0x00, 0x00, 0x00];
// Quick validity check
function isWasm(bytes) {
return bytes[0] === 0x00 && bytes[1] === 0x61
&& bytes[2] === 0x73 && bytes[3] === 0x6d;
}Hello Wasm (Minimal Example)
A minimal Wasm app: the module imports a 'log' function from JS and exports 'greet' that calls it. The WAT (WebAssembly Text) format is the human-readable representation, compiled to binary with wat2wasm. Browsers only execute the binary form.
<!-- index.html -->
<script>
WebAssembly.instantiateStreaming(fetch('hello.wasm'))
.then(({ instance }) => {
instance.exports.greet(); // logs "Hello, Wasm!"
});
</script>
;; hello.wat (compiled to hello.wasm via wat2wasm)
(module
(import "console" "log" (func $log (param i32)))
(func (export "greet")
i32.const 72 ;; 'H'
call $log
)
)WAT Text Format
Module & S-Expressions
WAT (WebAssembly Text format) is the textual representation of Wasm. Everything is wrapped in a (module ...) s-expression. Instructions are written in stack-based order: operands pushed first, then the operation consumes them.
;; WAT uses s-expressions: (keyword args...)
(module
;; function definitions, exports, etc. go here
)
;; Nested expressions form the instruction sequence
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(export "add" (func $add))
)Comments in WAT
WAT supports two comment styles: ';;' for single-line and '(; ... ;)' for block comments. Block comments can nest and span multiple lines — useful for disabling chunks of code during development.
;; Single-line comment (starts with ;;)
(func (export "f")
i32.const 1 ;; push 1
drop ;; discard
)
(; Block comment
can span
multiple lines ;)
(func (export "g")
nop
)Function Definition Syntax
Functions are declared with (func ...). Parameters use (param $name type) and results use (result type). The '$name' is optional — without it, functions/locals are referenced by numeric index. Multi-value returns require the multi-value proposal (widely supported).
;; Named function with params and result
(func $name (param $p1 i32) (param $p2 f64) (result i32)
;; body
)
;; Anonymous function (referenced by index)
(func (param i32) (result i32)
local.get 0
)
;; Multiple results (multi-value proposal)
(func $swap (param i32 i32) (result i32 i32)
local.get 1
local.get 0
)Inline Export & Import
WAT supports inline (export "name") and (import "module" "field") annotations on functions, memories, tables, and globals — a shorthand for the expanded (export ...) / (import ...) forms. Both compile to identical binary output.
;; Inline export on a function
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
;; Inline import
(func $log (import "env" "log") (param i32))
;; Inline export on memory
(memory (export "mem") 1)
;; Expanded form (equivalent)
(func $add (param i32 i32) (result i32) ...)
(export "add" (func $add))Data Segments
Data segments initialize linear memory at instantiation (active) or lazily via memory.init (passive). Strings include escape sequences like \00 for null terminator and \n for newline. data.drop releases the segment's storage after copying.
;; Initialize memory with bytes at offset 0
(memory 1)
(data (i32.const 0) "Hello, Wasm!\00")
;; Named data segment (active)
(data $msg (i32.const 0) "Hello\00")
;; Passive data segment (used with memory.init)
(data $passive "lazy-loaded bytes")
;; Drop a data segment to free its memory
(data.drop $msg)wat2wasm & wasm2wat Conversion
WABT (WebAssembly Binary Toolkit) provides wat2wasm (text→binary), wasm2wat (binary→text), wasm-validate, and wasm-objdump. These are essential for inspecting, debugging, and hand-authoring Wasm modules. The .wat form is for humans; the .wasm form is what browsers run.
# Install the WebAssembly Binary Toolkit (WABT)
# macOS: brew install wabt
# Ubuntu: apt install wabt
# Convert text -> binary
wat2wasm hello.wat -o hello.wasm
# Convert binary -> text (disassemble)
wasm2wat hello.wasm -o hello.wat
# Validate a .wasm file
wasm-validate hello.wasm
# Print module structure
wasm-objdump -x hello.wasmValue Types
Basic Value Types (i32/i64/f32/f64)
Wasm has only four basic value types: i32, i64, f32, f64. Integer signedness is determined by the operation (e.g., i32.div_s vs i32.div_u), not the type. This keeps the type system minimal and predictable.
;; Wasm has exactly four basic value types:
;; i32 - 32-bit integer
;; i64 - 64-bit integer
;; f32 - 32-bit float
;; f64 - 64-bit float
(func (param i32) (param i64) (param f32) (param f64)
(result f64)
local.get 2 ;; f32
f64.promote_f32
)
;; Integers are signed or unsigned PER OPERATION,
;; not per type. Use i32.add_s / i32.div_u etc.Numeric Literals in WAT
Numeric literals in WAT support decimal, hex (0x), and binary (0b) forms. Floats support scientific notation, infinity (inf), and NaN. WAT literals are unsigned by default — negative integers are encoded with a leading '-'.
;; Integer literals
i32.const 42
i32.const -7
i32.const 0xff ;; hex
i32.const 0b1010 ;; binary
;; Float literals
f64.const 3.14
f64.const 1e10
f64.const -0.0
f64.const inf
f64.const nan
;; 64-bit
i64.const 9007199254740992 ;; 2^53Type Conversions
Conversions use a consistent naming scheme: extend (smaller→larger), wrap (larger→smaller), convert (int↔float), reinterpret (bit-level cast), demote/promote (float precision). Signed variants use _s, unsigned use _u.
;; Extend i32 to i64
i64.extend_i32_s ;; sign-extend
i64.extend_i32_u ;; zero-extend
;; Wrap i64 to i32
i32.wrap_i64
;; Integer <-> Float
f32.convert_i32_s
f64.convert_i64_u
i32.trunc_f32_s ;; truncate (toward zero)
i32.reinterpret_f32 ;; bit-level reinterpret
;; Demote / promote floats
f32.demote_f64
f64.promote_f32v128 SIMD Type
The v128 type packs 16 bytes into a single SIMD register. Lanes are addressed as i32x4 (4×int32), f32x4 (4×float32), i16x8, i8x16, f64x2. SIMD speeds up image/audio/crypto workloads by operating on multiple values per instruction.
;; SIMD: 128-bit vector type (fixed-width SIMD proposal)
(module
(func (param v128) (result v128)
local.get 0
v128.not ;; bitwise NOT
)
(func (result v128)
v128.const i32x4 1 2 3 4
)
)
;; Vector operations: i32x4, i16x8, i8x16, f32x4, f64x2
v128.const f32x4 1.0 2.0 3.0 4.0
i32x4.addReference Types (funcref/externref)
The reference types proposal adds funcref (function pointer) and externref (opaque host reference). externref is powerful: it lets Wasm hold JS objects without copying or serialization. Combined with tables, this enables flexible callback patterns.
;; Reference types proposal adds:
;; funcref - reference to a function (in a table)
;; externref - opaque reference to a host (JS) object
(module
(table 1 funcref)
(func (param externref) (result externref)
local.get 0 ;; pass through a JS object
)
)
;; In JS, externref lets you pass arbitrary objects
;; through Wasm without serialization:
instance.exports.store({ hello: 'world' });Functions
Defining & Calling Functions
Functions are the fundamental unit of code. call invokes a function by name/index. Arguments are pushed onto the stack before the call; results are left on the stack when the function returns. The stack must match the (result ...) declaration.
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(func $double (param $x i32) (result i32)
local.get $x
local.get $x
i32.add ;; or: i32.const 2 i32.mul
)
(func (export "use")
i32.const 5
i32.const 7
call $add ;; -> 12 on stack
drop
)
)Parameters & Local Variables
Parameters are immutable locals indexed from 0. Declared locals (local $name type) are zero-initialized and mutable. Use local.get, local.set, and local.tee (set + leave on stack). Locals are Wasm's only mutable storage outside memory/globals.
(func $f (param $a i32) (param $b i32) (result i32)
(local $sum i32) ;; mutable local
(local $i i32)
local.get $a
local.get $b
i32.add
local.set $sum ;; store into local
i32.const 0
local.set $i
local.get $sum
)
;; tee: set and keep on stack
local.tee $sum ;; $sum = top; stays on stackMultiple Return Values
The multi-value proposal allows functions to return multiple values (and accept zero). All major engines support it since 2020. This removes the need for out-parameters via memory and makes idioms like swap/minmax natural.
;; Multi-value: return two i32 values
(func $minmax (param $a i32) (param $b i32) (result i32 i32)
local.get $a
local.get $b
i32.lt_s
if (result i32 i32)
local.get $a
local.get $b
else
local.get $b
local.get $a
end
)
;; Caller receives both on the stack
(func (export "use")
i32.const 3
i32.const 8
call $minmax
;; stack: [min=3, max=8]
drop drop
)Recursion
Wasm supports direct recursion — a function can call itself. There is no tail-call optimization in MVP Wasm (the tail-call proposal adds return_call). Deep recursion risks stack overflow, so iterative loops are preferred for production code.
(module
(func $fact (param $n i32) (result i32)
local.get $n
i32.const 1
i32.le_s
if (result i32)
i32.const 1
else
local.get $n
local.get $n
i32.const 1
i32.sub
call $fact ;; recursive call
i32.mul
end
)
(export "factorial" (func $fact))
)Inline Export & Named Calls
Inline (export "name") on a function definition is shorthand for a separate (export ...) declaration. call_ref (reference types proposal) invokes a function via a funcref directly, enabling first-class function values without a table lookup.
(module
;; Inline export on definition
(func $add (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
;; call_ref: call via funcref (reference types)
(func $caller (param $f funcref) (param i32) (result i32)
local.get 1
local.get 0
call_ref (param i32) (result i32)
)
)Start Function
A module may declare one (start $func) function that runs automatically at instantiation — useful for initialization. It takes no arguments and returns nothing. Only one start function is allowed per module.
(module
(func $init
;; runs once at instantiation
i32.const 0
i32.const 42
i32.store
)
(start $init) ;; declare as start function
(memory (export "mem") 1)
)
;; The start function runs automatically after
;; instantiation, before instantiate() resolves.
;; It takes no params and returns nothing.Memory
Declaring Linear Memory
Linear memory is a contiguous ArrayBuffer-like region, grown in 64KiB pages. (memory 1 10) declares initial=1, max=10. Memory is the only way to pass bulk data (strings, arrays) between Wasm and JS. Export it to access from JS.
(module
;; 1 page = 64KiB. Declare 1 page, max 10.
(memory $mem 1 10)
(export "memory" (memory $mem))
;; JS-side: read/write the exported memory
;; const buf = new Uint8Array(instance.exports.memory.buffer);
)Memory Grow
memory.grow adds pages and returns the previous size (in pages), or -1 on failure. Crucially, growing detaches the underlying ArrayBuffer — all existing TypedArray views become unusable. Always re-create views after any grow call.
(func $grow (result i32)
i32.const 1 ;; request 1 more page
memory.grow ;; returns previous size (or -1)
)
;; In JS, growing detaches the old buffer!
const prev = instance.exports.grow();
const buf = new Uint8Array(instance.exports.memory.buffer);
// OLD buf references are now detached — re-create views!Load & Store Operations
Loads/stores have variants by size (8/16/32/64-bit) and sign-extension (_s/_u). offset=N adds a constant to the address operand; align= is a hint (must be ≤ size). All addresses are byte offsets into linear memory.
;; Load: read from memory at address (top of stack)
i32.load offset=0 align=2 ;; 4-byte aligned load
i32.load8_u ;; 1 byte, zero-extended
i32.load16_s ;; 2 bytes, sign-extended
f64.load ;; 8-byte float load
;; Store: write value at address
i32.const 100 ;; address
i32.const 42 ;; value
i32.store ;; mem[100] = 42
i32.const 100
i32.const 0xFF
i32.store8 ;; write 1 byteMemory Sizes & Alignment
Alignment in Wasm is a performance hint, not a correctness constraint — unaligned access always works. The 'align' value must not exceed the operation's natural size. memory.size returns the current number of pages (each 64KiB).
;; Each load/store has a natural alignment:
;; i32.load -> 4 bytes
;; i64.load -> 8 bytes
;; f32.load -> 4 bytes
;; i32.load8 -> 1 byte
;; Explicit alignment hint (does not affect correctness)
i32.load align=2 ;; may be slower but still works
;; memory.size returns current pages
(func $pages (result i32)
memory.size
)Shared Memory & Atomics
Shared memory (the threads proposal) lets multiple Wasm instances/Workers share one memory for parallelism. Atomic operations (i32.atomic.load/store/rmw.add/cmpxchg) provide synchronization. Requires cross-origin isolation (COOP/COEP headers) in browsers.
(module
;; Shared memory (threads proposal)
(memory 1 1 shared)
(export "memory" (memory 0))
;; Atomic operations
(func (param i32 i32)
local.get 0 ;; address
local.get 1 ;; value
i32.atomic.store
)
(func (param i32) (result i32)
local.get 0
i32.atomic.load
)
(func (param i32) (result i32)
local.get 0
i32.const 1
i32.atomic.rmw.add ;; atomic read-modify-write
)
)Reading Strings from Memory
Wasm has no native string type — strings are byte sequences in linear memory, typically passed as (pointer, length) pairs. TextEncoder/TextDecoder handle UTF-8 conversion. Always ensure the memory region is large enough before writing.
// Wasm has no string type — strings live in linear memory.
// Decode UTF-8 from a (ptr, len) pair returned by Wasm:
function readString(memory, ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
return new TextDecoder('utf-8').decode(bytes);
}
// Writing a string into Wasm memory:
function writeString(memory, str, ptr) {
const bytes = new TextEncoder().encode(str);
const view = new Uint8Array(memory.buffer, ptr, bytes.length);
view.set(bytes);
return bytes.length; // return length
}Tables
Declaring Tables
Tables are arrays of references (typically funcref) used for indirect calls and dynamic dispatch. Unlike memory (which holds bytes), tables hold opaque references that Wasm cannot forge. (elem ...) segments initialize table slots at instantiation.
(module
;; A table of function references, initial size 1
(table 1 funcref)
(export "table" (table 0))
;; With max size
(table $t 4 16 funcref)
;; elem segment: initialize table slots
(elem (i32.const 0) $fn1 $fn2 $fn3)
(func $fn1)
(func $fn2)
(func $fn3)
)funcref Tables & call_indirect
call_indirect dispatches a call through a table entry by index. The (type ...) declaration is mandatory — Wasm checks the function's signature matches at runtime (a signature mismatch traps). This is the foundation of vtables and OO dispatch in Wasm.
(module
(type $bin (func (param i32 i32) (result i32)))
(table 3 funcref)
(func $add (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(func $sub (param i32 i32) (result i32)
local.get 0 local.get 1 i32.sub)
(func $mul (param i32 i32) (result i32)
local.get 0 local.get 1 i32.mul)
(elem (i32.const 0) $add $sub $mul)
(func (export "dispatch") (param i32 i32 i32) (result i32)
local.get 0 ;; arg a
local.get 1 ;; arg b
local.get 2 ;; table index
call_indirect (type $bin)
)
)Table Grow, Get & Set
table.grow appends slots and returns the previous size (or -1). table.get/table.set read/write entries by index. Growing a table enables runtime registration of callbacks. Initial size + max bounds growth to prevent runaway allocation.
(func $addSlot (param funcref) (result i32)
local.get 0
i32.const 1 ;; grow by 1
table.grow ;; returns old size, or -1
)
(func $read (param i32) (result funcref)
local.get 0
table.get
)
(func $write (param i32 funcref)
local.get 0 ;; index
local.get 1 ;; funcref
table.set
)externref Tables for Host Objects
externref tables store opaque references to host (JS) objects — Wasm can hold and pass them around but cannot inspect their contents. This is ideal for callback registration, JS handle tables, and bridging object-oriented JS APIs into Wasm.
(module
;; Table holding opaque JS objects
(table 8 externref)
(export "table" (table 0))
(func $store (param i32 externref)
local.get 0
local.get 1
table.set
)
(func $load (param i32) (result externref)
local.get 0
table.get
)
)
// JS: pass arbitrary objects
instance.exports.store(2, { callback: () => 42 });Dynamic Dispatch Pattern
The dynamic dispatch pattern: store operations in a table, then call_indirect by index. Combined with the mandatory type check, this gives safe vtable-style polymorphism. This is how Rust enums, C++ virtual functions, and interpreters implement dispatch.
(module
(type $op (func (param i32 i32) (result i32)))
(table 4 funcref)
(elem (i32.const 0) $add $sub $mul $div)
(func $add (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(func $sub (param i32 i32) (result i32)
local.get 0 local.get 1 i32.sub)
(func $mul (param i32 i32) (result i32)
local.get 0 local.get 1 i32.mul)
(func $div (param i32 i32) (result i32)
local.get 0 local.get 1 i32.div_s)
(func (export "apply") (param $op i32) (param $a i32) (param $b i32)
(result i32)
local.get $a
local.get $b
local.get $op
call_indirect (type $op)
)
)Globals
Declaring Globals
Globals are module-level single values, either immutable (default) or mutable (mut). They persist across function calls and are useful for counters, configuration flags, and constants. Mutable globals are the only shared mutable state outside memory/tables.
(module
;; Immutable global (const)
(global $counter (mut i32) (i32.const 0))
;; Read-only global
(global $pi f64 (f64.const 3.14159265))
(func $increment
global.get $counter
i32.const 1
i32.add
global.set $counter
)
)Mutable vs Immutable Globals
Without 'mut', a global is immutable and global.set is a compile error. The initializer must be a constant expression (e.g., i32.const, ref.null, global.get of an imported immutable global). Choose immutable by default for safety.
(module
;; (global $name type init) -> immutable
;; (global $name (mut type) init) -> mutable
(global $version i32 (i32.const 3))
;; global.set $version ;; ERROR: immutable
(global $state (mut i32) (i32.const 0))
(func (export "toggle")
global.get $state
i32.eqz
if
i32.const 1 global.set $state
else
i32.const 0 global.set $state
end
)
)Importing Globals
Imported globals let the host inject configuration values (or WebAssembly.Global objects) into a module at instantiation. Immutable imported globals can initialize other globals. WebAssembly.Global is the JS-visible wrapper for mutable shared globals.
(module
;; Import a global from JS
(global $env (import "env" "version") i32)
;; Mutable imported global
(global $flag (import "env" "flag") (mut i32))
(func (export "ver") (result i32)
global.get $env
)
)
// JS side
const imports = {
env: {
version: 3,
flag: new WebAssembly.Global({ value: 'i32', mutable: true }, 0)
}
};Exporting & Sharing Globals
Exported globals are accessible from JS as WebAssembly.Global objects with a .value property (gettable and, if mutable, settable). Sharing a Global object across multiple module instances is a clean way to coordinate state without touching memory.
(module
(global $g (export "counter") (mut i32) (i32.const 0))
(func (export "inc")
global.get $g
i32.const 1
i32.add
global.set $g
)
)
// JS reads/writes the global directly
const g = instance.exports.counter;
g.value; // 0
instance.exports.inc();
g.value; // 1
g.value = 100; // write directlyUse Cases for Globals
Globals suit three patterns: (1) imported configuration constants, (2) persistent counters/state shared across instances, (3) cached computation results. They are simpler than memory for single values and visible to multiple instances if shared via JS.
(module
;; Configuration constants
(global $maxSize (import "config" "maxSize") i32)
(global $debug (import "config" "debug") i32)
;; Persistent counters
(global $calls (mut i32) (i32.const 0))
(func (export "handle") (result i32)
global.get $calls
i32.const 1
i32.add
global.set $calls
global.get $calls
)
)Imports & Exports
Importing Functions
Imports let Wasm call host (JS) functions. Each import is identified by a two-level name (module, field). The import object passed to instantiate must match these names. Imports are how Wasm reaches the outside world — I/O, logging, scheduling.
(module
;; Import a function under module.field naming
(func $log (import "console" "log") (param i32))
(func $now (import "env" "now") (result f64))
(func (export "run")
i32.const 42
call $log ;; -> console.log(42)
call $now
drop
)
)
// JS
WebAssembly.instantiateStreaming(fetch('m.wasm'), {
console: { log: x => console.log('wasm:', x) },
env: { now: () => performance.now() }
});Importing Memory
Importing memory lets multiple Wasm modules (and JS) share one memory region — the basis for Wasm 'libraries'. One module allocates and writes; another reads. This composes modules without copying data and is the precursor to the component model.
(module
(memory (import "env" "memory") 1)
(func (export "writeHi")
i32.const 0
i32.const 72 ;; 'H'
i32.store8
)
)
// Shared memory across multiple modules:
const memory = new WebAssembly.Memory({ initial: 1 });
WebAssembly.instantiateStreaming(fetch('a.wasm'), { env: { memory } });
WebAssembly.instantiateStreaming(fetch('b.wasm'), { env: { memory } });
// Both modules read/write the SAME memoryImporting Globals & Tables
Globals and tables can also be imported, enabling shared state and function-pointer tables across modules. WebAssembly.Table is the JS constructor. Imported mutable globals let the host influence Wasm behavior dynamically without rebuilding the module.
(module
(global $seed (import "env" "seed") i32)
(table (import "env" "table") 4 funcref)
(func (export "rand") (result i32)
global.get $seed
i32.const 1103515245
i32.mul
i32.const 12345
i32.add
;; (truncated LCG step)
)
)
// JS provides the imports
const imports = {
env: {
seed: 42,
table: new WebAssembly.Table({
element: 'funcref', initial: 4, maximum: 16
})
}
};Exporting Functions, Memory, Tables
Anything declared in a module can be exported: functions, memory, tables, globals. Exports become properties on instance.exports. Memory exports give JS a view into the linear heap; global exports expose WebAssembly.Global objects; table exports expose function refs.
(module
(memory 1)
(table 2 funcref)
(global $c (mut i32) (i32.const 0))
(func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(export "memory" (memory 0))
(export "table" (table 0))
(export "counter" (global $c))
)
// JS usage
const { add, memory, table, counter } = instance.exports;
add(2, 3); // 5
new Uint8Array(memory.buffer); // raw bytes
table.get(0); // funcref
counter.value; // 0Inline Imports & Exports
Inline (import "m" "f") and (export "name") annotations are syntactic sugar — they compile to the same binary as the standalone (import ...)/(export ...) forms. A single definition can have an inline export, but imports and exports are declared separately for imported items.
(module
;; Inline import (compact form)
(func $log (import "env" "log") (param i32))
;; Inline export on function
(func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add
)
;; Inline export on memory
(memory (export "mem") 1)
;; Inline import + export combined is NOT allowed;
;; a definition imports OR exports, not both.
)Import Object Best Practices
Structure the import object to match the module's (module, field) names. Validate required imports before instantiation to give clear errors. Wrap host functions with try/catch where appropriate — an uncaught throw inside an import traps the Wasm caller.
// Build a typed import object
const imports = {
env: {
log: (i) => console.log('[wasm]', i),
now: () => performance.now(),
abort: () => { throw new Error('wasm abort'); },
memory: new WebAssembly.Memory({ initial: 1 }),
seed: 42,
}
};
// Validate imports before instantiation:
function checkImports(imports, required) {
for (const [mod, fields] of Object.entries(required)) {
for (const f of fields) {
if (!imports[mod]?.[f] === undefined) {
throw new Error(`Missing import: ${mod}.${f}`);
}
}
}
}Control Flow
block
block creates a labeled scope that branches exit (via br). Branches in Wasm are structured — they can only jump to the end of an enclosing block/loop/if. A block with a (result ...) leaves its value on the stack when branched to or fallen through.
;; block: a labeled group with a single exit
(block $b
i32.const 1
i32.const 2
i32.add
;; br $b exits the block (with optional value)
br $b
;; unreachable code
)
;; stack now has the result of the block
;; Blocks can produce a value (multi-value):
(block $b (result i32)
i32.const 42
br $b
)loop
loop is like block, but br $loop jumps to the TOP of the loop (continuing iteration) rather than the end. This is the opposite of block. Wasm has no for/while keywords — iteration is built from loop + a conditional br_if.
;; loop: a block whose br jumps back to the TOP
(func $count (param $n i32) (result i32)
(local $i i32) (local $sum i32)
i32.const 0 local.set $i
i32.const 0 local.set $sum
(loop $loop
local.get $i
local.get $n
i32.lt_s
if
local.get $sum
local.get $i
i32.add
local.set $sum
local.get $i
i32.const 1
i32.add
local.set $i
br $loop ;; jump back to loop start
end
)
local.get $sum
)if / else
if consumes an i32 from the stack (non-zero = true). The (result ...) clause makes the if produce a value when both branches agree on type. if without else is allowed; the branch can be empty. if is sugar for block + br_if.
;; if consumes a condition from the stack
(func $abs (param $x i32) (result i32)
local.get $x
if (result i32)
local.get $x
else
i32.const 0
local.get $x
i32.sub ;; negate
end
)
;; if without else (no result)
(func (param i32)
local.get 0
if
;; do something only if non-zero
end
)br & br_if
br $label jumps to the END of the named block/if (or the TOP of a loop). br_if does the same conditionally. Labels can also be numeric (br 0 = innermost). Branching with a value leaves it on the stack — Wasm's idiomatic way to return early.
;; br: unconditional branch to a label
(block $outer
(block $inner
br $inner ;; exits inner
br $outer ;; exits outer (unreachable here)
)
)
;; br_if: branch only if top of stack is non-zero
(func $clamp (param $x i32) (result i32)
(block $done (result i32)
local.get $x
i32.const 100
i32.gt_s
br_if $done ;; if x > 100, branch with...
i32.const 100 ;; ...100 on the stack
local.get $x
i32.const 0
i32.lt_s
br_if $done ;; if x < 0, branch with...
i32.const 0 ;; ...0
local.get $x ;; else x
)
)br_table (switch)
br_table implements a switch: it consumes an i32 index and jumps to one of the listed labels, or the last (default) label if out of range. Nest the blocks outermost-first so each case can return its value. This is the most efficient way to dispatch on a tag.
;; br_table: jump table (like a C switch)
(func $dayName (param $d i32) (result i32)
(block $default
(block $sat
(block $sun
local.get $d
br_table $sun $sat $default
)
i32.const 0 ;; Sunday
return
)
i32.const 6 ;; Saturday
return
)
i32.const -1 ;; default
)
;; br_table $l0 $l1 $l2 ... $default
;; index 0 -> $l0, 1 -> $l1, ... out-of-range -> $defaultreturn & unreachable
return immediately exits the function with the value on the stack. unreachable always traps (throws a runtime exception) — used to mark code paths that should never execute, similar to assert(false). Both are control-flow escape hatches beyond the structured block model.
(func $find (param $arr i32) (param $len i32) (param $key i32)
(result i32)
(local $i i32)
i32.const 0 local.set $i
(block $notFound
(loop $loop
local.get $i
local.get $len
i32.ge_s
br_if $notFound
;; load arr[i] and compare...
local.get $i
i32.const 1
i32.add
local.set $i
br $loop
)
)
unreachable ;; traps if reached
)
;; return: early exit from the function
;; unreachable: always traps (used to mark bugs)JS Interoperability
Passing Numbers
i32/f32/f64 map directly to JS numbers. i64 maps to JS BigInt (large integers exceed Number's safe range). Always use BigInt for i64 parameters/returns. The conversion is zero-copy at the boundary but crossing it frequently has overhead — batch operations in Wasm.
// Numbers pass directly across the Wasm/JS boundary.
const exports = instance.exports;
exports.add(2, 3); // 5 (i32 -> JS number)
exports.square(4); // 16
exports.compute(1.5, 2.5); // f64 -> JS number
// i64 caveat: before BigInt integration, i64 returns
// caused problems. Now Wasm i64 maps to JS BigInt:
const big = exports.bigResult(); // 9007199254740993n
// Passing a BigInt to an i64 param:
exports.takeBigInt(123n);Passing Strings
Strings cross the boundary as UTF-8 byte sequences: JS encodes them into Wasm memory, then passes (pointer, length). Often Wasm exports alloc/free helpers, or you pack both into a single i64 (low 32 = ptr, high 32 = len). Always free or reuse the buffer to avoid leaks.
// Wasm has no string type. Encode UTF-8 into memory,
// pass (ptr, len) to Wasm:
function passString(memory, str) {
const bytes = new TextEncoder().encode(str);
const ptr = instance.exports.alloc(bytes.length);
new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
instance.exports.processString(ptr, bytes.length);
instance.exports.dealloc(ptr, bytes.length);
}
// Receiving a string back:
function recvString(memory, ptrLen) {
const ptr = ptrLen >>> 0;
const len = ptrLen >>> 32;
const bytes = new Uint8Array(memory.buffer, ptr, len);
return new TextDecoder().decode(bytes);
}Passing Arrays & Bulk Data
For bulk numeric data, view the Wasm memory as a TypedArray of the right type (Float32Array, Int32Array, etc.) and use .set() to copy. This is the fastest path — memcpy-like. Remember pointer alignment (4-byte for i32/f32, 8-byte for i64/f64) and always free memory.
// Write a JS array into Wasm memory:
function passArray(memory, arr) {
const view = new Float32Array(memory.buffer);
const ptr = instance.exports.alloc(arr.length * 4);
const offset = ptr / 4;
for (let i = 0; i < arr.length; i++) view[offset + i] = arr[i];
return ptr;
}
// Faster: use TypedArray.set with a subarray
function passArrayFast(memory, arr) {
const ptr = instance.exports.alloc(arr.length * 4);
new Float32Array(memory.buffer, ptr, arr.length).set(arr);
return ptr;
}Memory Management Across the Boundary
Wasm memory is manually managed — there is no garbage collector. If Wasm exports alloc/free (or you use a malloc-style helper), wrap JS access in try/finally to guarantee freeing. Watch for memory.grow detaching ArrayBuffer views — recreate views after growth.
// Wasm has no GC — JS must manage allocations.
// Pattern: Wasm exports alloc/free, JS uses them:
const { memory, alloc, free, process } = instance.exports;
function withBuffer(size, fn) {
const ptr = alloc(size);
try {
return fn(new Uint8Array(memory.buffer, ptr, size));
} finally {
free(ptr, size);
}
}
// CAUTION: TypedArray views become detached after
// memory.grow — re-create them after any grow call.WebAssembly.Global
WebAssembly.Global is a JS wrapper around a Wasm global value. Creating one and passing it to multiple module instances shares mutable state cleanly without touching memory. The .value property reads/writes the underlying i32/i64/f32/f64 (or v128/externref).
// Create a global on the JS side
const counter = new WebAssembly.Global(
{ value: 'i32', mutable: true },
0
);
// Pass to Wasm at instantiation
WebAssembly.instantiateStreaming(fetch('m.wasm'), {
env: { counter }
});
// Read/write from JS
counter.value; // 0
counter.value = 10; // set
counter.value++; // 11
// Share across multiple instances
WebAssembly.instantiateStreaming(fetch('m2.wasm'), {
env: { counter } // same Global object
});WebAssembly.Table & Errors
WebAssembly.Table stores function references accessible from both JS and Wasm — useful for callback registries and dispatch tables. Wasm traps (divide by zero, OOB memory, stack overflow) surface as WebAssembly.RuntimeError in JS — wrap risky calls in try/catch.
// Table of function references, callable from JS indirectly
const table = new WebAssembly.Table({
element: 'funcref',
initial: 4,
maximum: 16
});
// Set a slot to a Wasm-exported function
table.set(0, instance.exports.add);
table.get(0)(2, 3); // 5
// Errors propagate as 'RuntimeError' across the boundary:
try {
instance.exports.divide(1, 0);
} catch (e) {
// e instanceof WebAssembly.RuntimeError
console.error('Wasm trapped:', e.message);
}Compiling C/C++ to Wasm
Emscripten Setup
Emscripten is the canonical toolchain for compiling C/C++ (and LLVM languages) to Wasm. It bundles Clang, Binaryen, and a libc/libstdc++ ported to the browser. Activate the environment in each shell session with emsdk_env. Updates are frequent — reinstall periodically.
# Install the Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh # Linux/macOS
# Windows: emsdk_env.bat
# Verify
emcc --version
# emcc (Emscripten gcc/clang-like replacement)Hello World in C
Standard C works as-is — printf writes to a virtual terminal Emscripten provides. main() is invoked automatically when the module loads. Functions called from JS should be exported with EMSCRIPTEN_KEEPALIVE or the -s EXPORTED_FUNCTIONS flag.
// hello.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
printf("Hello from C!\n");
printf("2 + 3 = %d\n", add(2, 3));
return 0;
}Compiling with emcc
emcc drives compilation. -O3 enables aggressive optimizations (inlining, dead-code elimination). EXPORTED_FUNCTIONS lists C symbols (with leading _) to expose. MODULARIZE wraps the loader as a factory for use in bundlers. Output formats: .wasm only, .js glue + .wasm, or .html demo.
# Standalone .wasm (no main, exported functions only)
emcc add.c -O3 -o add.wasm \
-s EXPORTED_FUNCTIONS='["_add"]' \
-s ALLOW_MEMORY_GROWTH=1
# HTML + JS + Wasm bundle (auto-runs main)
emcc hello.c -o hello.html
# With optimizations and a specific output
emcc hello.c -O3 -s WASM=1 -o hello.js
# Common flags:
# -O0/-O1/-O2/-O3/-Os optimization level
# -s MODULARIZE=1 export as JS module
# -s EXPORT_ES6=1 ES module output
# -s ENVIRONMENT=web target only the webEmscripten HTML Output & Module
The Emscripten 'Module' object is the JS-side runtime: it exposes _ prefixed C functions, manages memory (_malloc/_free), and provides hooks (print, onRuntimeInitialized). MODULARIZE makes it a Promise-returning factory suitable for bundlers and async loading.
// Generate a runnable HTML demo
// emcc hello.c -o hello.html
// Then open hello.html in a browser.
// Modularized loader (use from JS):
// emcc hello.c -o hello.js -s MODULARIZE=1 -s EXPORT_NAME='createModule'
import createModule from './hello.js';
const Module = await createModule({
print: (text) => console.log('C said:', text),
onRuntimeInitialized: () => {
Module._main(); // call main
const sum = Module._add(2, 3);
console.log('sum:', sum); // 5
}
});Calling JS from C (EM_JS / EM_ASM)
EM_JS defines a JS function inline that C can call directly — efficient and synchronous. EM_ASM embeds an inline JS snippet. Use $0, $1, ... for arguments. These macros glue C to browser APIs (DOM, fetch, Web Audio) without writing separate JS bindings.
#include <emscripten.h>
// EM_JS: define a JS function callable from C
EM_JS(void, callAlert, (int x), {
alert("Value from C: " + x);
});
// EM_ASM: inline JS (use $n for arguments)
void logValue(int n) {
EM_ASM({
console.log("C passed:", $0);
}, n);
}
int main() {
callAlert(42);
logValue(99);
return 0;
}File System Support (MEMFS)
Emscripten provides MEMFS, an in-memory virtual filesystem. fopen/fread/fwrite work as in C. Use --preload-file to embed assets into the .data sidecar at compile time. For persistent storage, IDBFS syncs to IndexedDB; NODERAWFS exposes the real FS in Node.
#include <stdio.h>
#include <emscripten.h>
int main() {
// MEMFS: a virtual in-memory filesystem
FILE *f = fopen("/tmp/data.txt", "w");
fprintf(f, "Hello, MEMFS!\n");
fclose(f);
f = fopen("/tmp/data.txt", "r");
char buf[64];
fgets(buf, sizeof(buf), f);
fclose(f);
printf("%s", buf);
return 0;
}
// Preload files at compile time:
// emcc app.c -o app.html \
// --preload-file assets/data.txtCompiling Rust to Wasm
wasm-pack Setup
Rust compiles to Wasm via the wasm32-unknown-unknown target. wasm-pack is the recommended tool — it wraps cargo, runs wasm-bindgen, and produces an npm-ready package. For WASI targets, use wasm32-wasi instead.
# Install Rust (rustup)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add the wasm32 target
rustup target add wasm32-unknown-unknown
# Install wasm-pack (the canonical Wasm tool for Rust)
cargo install wasm-pack
# or: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Verify
wasm-pack --versionRust Lib Structure
wasm-bindgen is the bridge between Rust and JS. #[wasm_bindgen] marks items for export. Strings, Vec<u8>, and JS objects are auto-converted. The constructor attribute exposes new() as a JS constructor. This generates the JS glue that makes Rust feel native.
// src/lib.rs
use wasm_bindgen::prelude::*;
// Export a function callable from JS
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Export a struct with methods
#[wasm_bindgen]
pub struct Counter {
count: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { count: 0 }
}
pub fn increment(&mut self) -> i32 {
self.count += 1;
self.count
}
}Building with wasm-pack
wasm-pack build produces a publishable npm package: the .wasm binary, JS glue, and TypeScript .d.ts files. --target web gives a self-contained ES module for direct browser use; --target bundler integrates with webpack/vite. The generated types give full TS autocomplete.
# Build for the browser (npm package output)
wasm-pack build --target web
# Targets:
# --target web ES module, modern browsers
# --target bundler for webpack/rollup/vite
# --target nodejs CommonJS for Node.js
# --target no-modules global-based
# Output goes to ./pkg/
# my_crate.js JS glue
# my_crate_bg.wasm the binary
# my_crate.d.ts TypeScript types
# Use in JS:
import init, { add } from './pkg/my_crate.js';
await init(); // load the .wasm
console.log(add(2, 3)); // 5wasm-bindgen: JS Interop
extern blocks import JS functions and classes into Rust. js_namespace = console calls console.log. method exposes JS methods on imported types. wasm-bindgen handles string conversions (Rust &str <-> JS string) and ownership — no manual memory juggling.
use wasm_bindgen::prelude::*;
// Call a JS function from Rust
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn run() {
log("Hello from Rust!");
}
// Import a JS class
#[wasm_bindgen(module = "/js/util.js")]
extern "C" {
pub type JsCounter;
#[wasm_bindgen(constructor)]
pub fn new() -> JsCounter;
#[wasm_bindgen(method)]
pub fn tick(this: &JsCounter) -> i32;
}Calling JS from Rust & Panics
Imported JS functions appear as ordinary Rust functions. Use console_error_panic_hook to get readable Rust panic backtraces in the browser console (default panics trap with no message). The #[wasm_bindgen(start)] attribute runs main() at instantiation — like C's main.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
// Install a panic hook for readable errors:
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
}AssemblyScript
Setup & Hello World
AssemblyScript is a TypeScript-like language that compiles to Wasm. It targets developers who want Wasm's performance without learning C/Rust. The syntax mirrors TS but with strict types (i32 instead of number). Use the loader or import the .wasm directly.
# Install AssemblyScript compiler
npm install -g assemblyscript
# or use the boilerplate:
npm init assemblyscript my-app
cd my-app
npm install
npm run asbuild
// assembly/index.ts
export function add(a: i32, b: i32): i32 {
return a + b;
}
// Load from JS:
import { instantiate } from "@assemblyscript/loader";
const module = await instantiate(fetch("build/release.wasm"));
console.log(module.exports.add(2, 3)); // 5Types in AssemblyScript
Unlike TypeScript, AssemblyScript enforces concrete numeric types: i32, i64, f32, f64, u8, u16, u32, etc. 'number' is rejected at compile time. StaticArray avoids GC; ArrayBuffer/TypedArray mirror the JS APIs. Strict typing is what enables compilation to efficient Wasm.
// Distinct numeric types (NOT just 'number')
let a: i32 = 42;
let b: i64 = 9007199254740993n;
let f: f64 = 3.14;
let u: u32 = 100;
// Bool, string, arrays
let flag: bool = true;
let s: string = "hi";
let arr: i32[] = [1, 2, 3];
// Static arrays (no GC overhead)
let sa: StaticArray<i32> = [1, 2, 3];
// TypedArray-like
let buf: ArrayBuffer = new ArrayBuffer(8);Memory & Strings
AssemblyScript has a small GC for managed objects (Array, Map, String). Use __retain/__release to control lifetimes when crossing to JS. The @assemblyscript/loader provides __getString, __newString, __getArray, etc., to bridge across the boundary.
// Strings are UTF-16 in AssemblyScript memory
export function greet(name: string): string {
return "Hello, " + name + "!";
}
// Manual memory management for buffers
export function process(len: i32): i32 {
let buf = new ArrayBuffer(len);
// ...fill buf...
let result = __retain(buf); // prevent GC
return result;
}
// In JS, use the loader's __getString/__newString:
const ptr = module.exports.greet("World");
console.log(module.exports.__getString(ptr));Exports & Entry Points
Functions and classes marked export become Wasm exports. Classes map to JS-compatible objects via the loader. _start is the conventional entry point (like C's main). AssemblyScript's stdlib mirrors JS Math, Array, Map — making the migration curve gentle.
// assembly/index.ts
export function fib(n: i32): i32 {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
export class Vector {
constructor(public x: f32, public y: f32) {}
magnitude(): f32 {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
// Module start function (runs at instantiation)
export function _start(): void {
// initialization code
}Compilation & Optimizations
asc is the AssemblyScript compiler. --optimize (-O) runs Binaryen's wasm-opt. --runtime controls the GC: 'full' (default), 'half' (smaller), 'none' (manual). --importMemory lets you share memory across modules. Source maps enable debugging in original .ts source.
# Build debug vs release
npm run asbuild # both debug and release
# or:
asc assembly/index.ts --outFile release.wasm --optimize
# Common flags
asc index.ts -o release.wasm \
--optimize \
--noAssert \
--runtime half # smaller GC runtime
--importMemory # use imported memory
--sourceMap # emit source map
# Optimize for size:
asc index.ts -o tiny.wasm --optimize --runtime none \
--conformance --noDebugWASI (WebAssembly System Interface)
What is WASI
WASI is a portable, capability-based system interface for Wasm outside the browser. Unlike the web, where Wasm calls into JS, WASI gives Wasm direct (sandboxed) access to files, network, and the environment. Capability-based security means a program can only touch resources explicitly preopened for it.