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.
// WASI = WebAssembly System Interface
// A standardized API for Wasm to access OS capabilities:
// - filesystem (files, directories)
// - network sockets (preview2)
// - environment variables, args
// - clocks, random numbers
// - process exit
// Capability-based security: a Wasm program can
// ONLY access resources it was explicitly granted.
// Targets:
// wasm32-wasi (preview1)
// wasm32-wasip1 (preview1, new name)
// wasm32-wasip2 (preview2, component model)
// Runtimes: wasmtime, wasmer, WasmEdge, browser_wasi_shimwasmtime Runtime
wasmtime is the reference WASI runtime from Bytecode Alliance. --dir preopens a host directory (granting file access). --env passes env vars. --net enables networking in preview2. Without explicit grants, the Wasm program sees nothing — that's capability security in action.
# Install wasmtime (the reference WASI runtime)
# macOS: brew install wasmtime
# Linux: curl https://wasmtime.dev/install.sh -sSf | bash
# Windows: scoop install wasmtime
# Run a WASI module
wasmtime hello.wasm
# Pass arguments
wasmtime hello.wasm arg1 arg2
# Preopen a directory (grant filesystem access)
wasmtime --dir=. app.wasm
# Set environment variables
wasmtime --env FOO=bar app.wasm
# Allow network (preview2)
wasmtime --net allow app.wasmRunning Wasm with WASI (C Example)
Programs written for WASI look like ordinary C programs using libc. Compile with clang --target=wasm32-wasi and a WASI sysroot (download the wasi-sdk release). The same .wasm runs on wasmtime, wasmer, WasmEdge, and other WASI hosts without recompilation — true portability.
// wasi_hello.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
printf("Hello from WASI!\n");
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++) printf(" argv[%d] = %s\n", i, argv[i]);
return 0;
}
# Compile with the WASI SDK
clang --target=wasm32-wasi \
--sysroot=/path/to/wasi-sysroot \
-o hello.wasm wasi_hello.c
# Run
wasmtime hello.wasm foo bar
# Hello from WASI!
# argc = 3
# argv[0] = hello.wasm
# argv[1] = foo
# argv[2] = barWASI Filesystem Access
WASI exposes a POSIX-like filesystem API (open, read, write, close). Files outside preopened directories are invisible. Use --dir=path:alias to grant access and rename. WASI preview2 introduces component-model interfaces (wasi filesystem, wasi sockets) for richer APIs.
// wasi_fs.c — read a file passed via --dir
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("input.txt", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) { buf[n] = '\0'; printf("%s", buf); }
close(fd);
return 0;
}
# Grant access, then run:
# wasmtime --dir=. wasi_fs.wasmWASI Environment & Args
WASI provides args and environment variables via standard C interfaces (argv, environ, getenv). The host controls what's visible — --env grants specific variables. This makes Wasm programs configurable without code changes, and limits leakage of host secrets.
// wasi_env.c
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[]) {
printf("Args:\n");
for (int i = 0; i < argc; i++) printf(" %s\n", argv[i]);
printf("Env:\n");
for (char **e = environ; *e; e++) printf(" %s\n", *e);
const char *home = getenv("HOME");
printf("HOME=%s\n", home ? home : "(unset)");
return 0;
}
# Run with env vars:
# wasmtime --env HOME=/tmp --env FOO=bar wasi_env.wasm a b cWASI Sockets (Preview)
WASI sockets are part of preview2 (component model). wasi:sockets/tcp and wasi:sockets/udp provide networking; wasi:http offers a higher-level HTTP client. APIs are stabilizing — prefer wasi:http for portability today. Runtimes like wasmtime and WasmEdge implement these.
// WASI preview2 adds TCP/UDP sockets.
// In Rust (using wasi crate):
use wasi::sockets::network::{IpAddress, IpSocketAddress};
// Component-model based sockets:
// - wasi:sockets/tcp
// - wasi:sockets/udp
// - wasi:sockets/ip-name-lookup
# Compile a Rust program for preview2:
cargo build --target wasm32-wasip2 --release
# Run with network access:
wasmtime --net allow target/wasm32-wasip2/release/myapp.wasm
// Note: socket APIs are still stabilizing.
// Use HTTP via wasi:http/types for stability.Debugging
wasm-objdump
wasm-objdump (from WABT) prints a module's structure: types, imports, exports, function signatures, code disassembly, and section sizes. -x gives a high-level summary; -d disassembles function bodies into WAT-like instructions. Essential for understanding what a module actually contains.
# Inspect a .wasm file's structure
wasm-objdump -x module.wasm # everything
wasm-objdump -d module.wasm # disassemble functions
wasm-objdump -s module.wasm # raw section contents
wasm-objdump -j Import -x m.wasm # only the Import section
# Sample -x output:
# Section Type (1) at 0x0008:
# type[0] (i32, i32) -> i32
# Section Function (3) at 0x0015:
# func[0] type=0wasm2wat for Inspection
wasm2wat converts a binary .wasm back into human-readable WAT. This is invaluable for understanding third-party modules, debugging unexpected behavior, and reverse-engineering. The round-trip (wasm2wat → edit → wat2wasm) preserves behavior, making WAT a true source format.
# Disassemble binary to readable text
wasm2wat module.wasm -o module.wat
# View in your editor
cat module.wat
# Round-trip: edit .wat, recompile
wat2wasm module.wat -o module.wasm
# Common inspection: find exports
grep "(export" module.wat
# Find imports
grep "(import" module.watBrowser DevTools
All major browsers can debug Wasm in DevTools: view disassembled WAT, set breakpoints, inspect locals and the call stack. With source maps (Rust, AssemblyScript, Emscripten -g), DevTools maps back to the original source. DWARF debug info enables richer debugging for C/C++/Rust.
// Chrome/Edge/Firefox DevTools support Wasm debugging:
// 1. Sources tab -> file tree shows .wasm entries
// 2. Click a .wasm to view disassembled WAT
// 3. Set breakpoints on function entries
// 4. Inspect the call stack and locals
// With source maps (Rust/AS), DevTools shows the
// ORIGINAL source (.rs / .ts) instead of WAT.
// Enable in chrome://flags:
// "WebAssembly debugging: Support DWARF"
// Console logging from Wasm:
// Import a JS log function and call it from WAT.
// Rust: wasm_logger / console_log crate.
// AS: console.log via env import.Source Maps
Source maps link Wasm instructions back to the original source language. Rust emits them via wasm-pack; AssemblyScript via --sourceMap; Emscripten via -g4. The sourceMappingURL comment in the .wasm (custom section) tells DevTools where to find the .map. Without it, you debug raw WAT.
# Rust: source maps come automatically with wasm-pack
wasm-pack build --target web --dev
# AssemblyScript: emit source maps
asc index.ts -o release.wasm --sourceMap \
--sourceMap include 'src/**/*.ts'
# Emscripten: include debug info and source map
emcc app.c -g4 -O0 -o app.js \
--source-map-base http://localhost:8000/
# The .map file points DevTools back to the
# original .rs / .ts / .c source files.
# Validate a source map exists:
wasm-objdump -x app.wasm | grep sourceMappingURLCommon Errors & Traps
Recurring Wasm errors: missing imports (mismatched names/types), traps (unreachable, OOB memory, integer overflow on div by zero), detached ArrayBuffers (after grow), and i64 boundary issues. Wrap risky calls in try/catch and validate imports/pointers up front to surface problems early.
// 1. LinkError: "import not found"
// Cause: import object missing a name.
const imports = { env: { log: console.log } };
// Must match (import "env" "log" ...) exactly.
// 2. RuntimeError: "unreachable"
// Cause: Wasm executed unreachable / panic / assert.
// 3. RuntimeError: "out of bounds memory access"
// Cause: load/store at an invalid address.
// Fix: check ptr/len before access; grow memory.
// 4. CompileError: "section size mismatch"
// Cause: corrupted or truncated .wasm file.
// 5. TypeError: "i64 not supported"
// Cause: i64 returns without BigInt integration.
// Fix: use BigInt, or return two i32 values.
// 6. "detached ArrayBuffer"
// Cause: used a TypedArray view after memory.grow.
// Fix: re-create views after grow.Performance Optimization
Inlining & -O Flags
-O3 enables aggressive inlining and loop optimizations. wasm-opt (Binaryen) runs additional Wasm-level passes (inlining, dead-code, constant folding) — always run it for production. -Os/-Oz trade some speed for smaller downloads. -flto enables whole-program optimization.
# Compilation optimization levels
emcc app.c -O0 -o app.wasm # no optimization (debug)
emcc app.c -O1 -o app.wasm # basic
emcc app.c -O2 -o app.wasm # standard
emcc app.c -O3 -o app.wasm # aggressive (inlining)
emcc app.c -Os -o app.wasm # optimize for size
emcc app.c -Oz -o app.wasm # max size reduction
# Post-optimize with Binaryen
wasm-opt -O3 app.wasm -o app.opt.wasm
wasm-opt -Os app.wasm -o app.small.wasm
# LTO across translation units (C/C++)
emcc app.c -flto -O3 -o app.wasmMemory Layout & Access Patterns
Wasm linear memory is just an ArrayBuffer — CPU cache behavior applies. Contiguous access patterns (row-major for row-major data) are dramatically faster than strided access. Maintain natural alignment (4-byte for i32) so loads/stores hit single instructions.
// Cache-friendly access: linear, contiguous iteration
// GOOD: row-major traversal of a 2D array
for (i32 row = 0; row < rows; row++) {
for (i32 col = 0; col < cols; col++) {
sum += data[row * cols + col];
}
}
// BAD: column-major traversal (cache misses)
for (i32 col = 0; col < cols; col++) {
for (i32 row = 0; row < rows; row++) {
sum += data[row * cols + col];
}
}
// Prefer aligned access (4-byte for i32, 8 for i64).
// Allocators should respect natural alignment.Minimizing JS Boundary Crossings
Every JS→Wasm call has overhead (argument marshaling, stack setup). For tight loops, move the loop INTO Wasm and call it once with a buffer. This is the single biggest Wasm perf tip — bulk operations beat per-element calls by orders of magnitude.
// BAD: call Wasm per element (boundary overhead)
for (let i = 0; i < arr.length; i++) {
result[i] = exports.process(arr[i]);
}
// GOOD: batch — one call for the whole array
const ptr = exports.alloc(arr.length * 4);
new Float32Array(memory.buffer, ptr, arr.length).set(arr);
exports.processBatch(ptr, arr.length); // single call
const out = Array.from(new Float32Array(memory.buffer, ptr, arr.length));
exports.free(ptr, arr.length * 4);
// Each crossing costs ~tens of nanoseconds.
// Batch loops to amortize the overhead.SIMD (Single Instruction, Multiple Data)
SIMD (v128) processes 4 floats or 4 int32s per instruction — 4x speedups for image/audio/matrix math. Emscripten enables it with -msimd128; Rust uses std::arch::wasm32 intrinsics. Auto-vectorization can help, but explicit intrinsics give predictable wins. Widely supported in browsers.
// C/C++ with Emscripten auto-vectorization
#include <emscripten/simd.h>
void addArrays(float *a, float *b, float *out, int n) {
for (int i = 0; i < n; i += 4) {
v128_t va = wasm_v128_load(a + i);
v128_t vb = wasm_v128_load(b + i);
wasm_v128_store(out + i, wasm_f32x4_add(va, vb));
}
}
# Compile with SIMD enabled
emcc simd.c -O3 -msimd128 -o simd.wasm
// Rust: use std::arch::wasm32 intrinsics
use std::arch::wasm32::*;
unsafe { let v = f32x4_splat(1.0); }Threads & SharedArrayBuffer
Wasm threads share a Memory across Web Workers, using atomics for synchronization. Requires COOP/COEP headers (cross-origin isolation). Emscripten's -pthread emulates pthreads; Rust's std::thread works on wasm32-wasi-threads. Great for CPU-bound parallel work — image processing, codecs, ML inference.
// Enable threads in Emscripten
# emcc app.c -O3 -pthread -s PTHREAD_POOL_SIZE=4 -o app.js
// Browser requires cross-origin isolation:
// /_headers (Cloudflare/Netlify)
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp
// Spawn workers sharing Wasm memory:
const memory = new WebAssembly.Memory({
initial: 10, maximum: 100, shared: true
});
// Each Worker instantiates the module with this memory
// and uses Atomics for synchronization.Profiling & Benchmarks
Profile with performance.now() and the DevTools Performance tab — Wasm shows up in flame charts like JS. Warm up before measuring (compilation overhead is one-time). Compare against optimized JS — modern V8 is fast; Wasm wins on predictable perf, numeric kernels, and large workloads, not micro-tasks.
// Use performance.now() for high-res timing
const t0 = performance.now();
exports.heavyWork(ptr, len);
const t1 = performance.now();
console.log(`Wasm: ${(t1 - t0).toFixed(2)}ms`);
// Compare against JS equivalent
const t2 = performance.now();
jsEquivalent(data);
console.log(`JS: ${(performance.now() - t2).toFixed(2)}ms`);
// Chrome DevTools Performance tab: profile Wasm
// like JS — flame charts show function time.
// Avoid microbenchmarks pitfalls:
// - warm up first (JIT compilation)
// - run many iterations
// - compare against OPTIMIZED JS, not naive JSWasm vs JavaScript
Performance Comparison
Wasm shines for compute-heavy numeric work (image processing, codecs, crypto, ML) — typically 2-10x faster than JS. For DOM, JSON, or string-heavy work, JS is often faster because the work is already in native code (V8's JSON.parse, browser's DOM). Profile to decide — never assume Wasm is always faster.
// Wasm vs JS — typical scenarios:
//
// Image processing (8MB):
// JS: 120 ms
// Wasm (-O3): 35 ms ~3.4x faster
//
// SHA-256 (1MB):
// JS: 45 ms
// Wasm: 8 ms ~5.6x faster
//
// DOM manipulation:
// JS: 5 ms
// Wasm: 20 ms SLOWER (DOM is JS-native)
//
// JSON parse:
// JS: 3 ms
// Wasm: 15 ms SLOWER (JSON.parse is C++ in V8)
//
// Rule of thumb: Wasm wins on compute-heavy numeric
// workloads; JS wins on DOM/JSON/string-heavy tasks.Use Cases for Wasm
Wasm excels at: porting existing C/C++/Rust codebases (Figma, Photoshop web), compute-bound numeric kernels (image/ML/crypto), and sandboxed plugins. Real-world wins include Figma (renderer), Photoshop (full app port), and Google Meet (background blur). For pure DOM apps, JS is usually the right call.
// Strong Wasm use cases:
// - Image/video/audio processing (FFmpeg, OpenCV)
// - Games & 3D engines (Unity, Unreal)
// - Cryptography (hashing, encryption, ZK proofs)
// - Machine learning inference (ONNX, TFLite)
// - Codecs (AV1, VP9, MP3)
// - Emulators (DOSBox, retro consoles)
// - Heavy math (physics simulations, linear algebra)
// - Reusing C/C++/Rust libraries (FFI alternative)
// - Sandboxed plugins in editors (Figma, Photoshop)
// Figma: rendering engine in Wasm
// Photoshop: web port of full desktop app
// Google Meet: video background blurWhen to Use JS
JS remains the right choice for UI, DOM, browser APIs, and most web app logic — V8 is heavily optimized. Reach for Wasm when profiling reveals a hotspot that JS can't optimize away, or when porting an existing native codebase. A hybrid approach (JS UI + Wasm hotspots) is often ideal.
// JS is better when:
// - DOM manipulation dominates
// - Working with browser APIs (fetch, IndexedDB, WebRTC)
// - Heavy string/JSON processing
// - Small utilities and glue code
// - Rapid prototyping (no compile step)
// - Team lacks C/Rust expertise
// - Module size matters more than runtime speed
// JS engines (V8, SpiderMonkey, JSC) are highly
// optimized. For most web apps, JS is fast enough
// AND simpler to ship and maintain.
// Hybrid: keep UI in JS, offload hotspots to Wasm.Loading & Startup Cost
Wasm has a fixed startup cost (download + compile + instantiate) that JS avoids. Mitigate with streaming compilation, caching compiled modules via the Cache API, and lazy-loading non-critical Wasm. After warm-up, Wasm function calls are as fast as JS — the cost is up-front, paid back by faster execution.
// Wasm has upfront costs JS doesn't:
// 1. Download the .wasm binary
// 2. Compile to machine code (~10-50 ms for small modules)
// 3. Instantiate (initialize memory, run start function)
// Mitigations:
// - Cache compiled modules (WebAssembly.compileStreaming + Cache API)
// - Stream compilation (instantiateStreaming)
// - Lazy-load non-critical Wasm on demand
// - Use Web Workers to compile off the main thread
// Streaming compile of a 1MB module:
// ~30 ms download + ~10 ms compile (parallel)
// Cached: ~5 ms (re-instantiate from cached module)Interop Cost & Boundary Overhead
JS↔Wasm calls are cheap but not free (~10-100ns each, more for strings). For high-frequency events, batch in JS and call Wasm once with a buffer. Avoid passing JS objects per-call — use externref or copy bytes into memory. The boundary is fast for numbers, slow for serialized data.
// Each JS<->Wasm call costs ~10-100 ns:
// - Marshal arguments (i32/f64 fast; strings/objects slow)
// - Stack frame setup
// - Inlined trampoline in the engine
// Implications:
// - Don't call Wasm 1M times for tiny work — batch!
// - Don't pass large objects per-call — copy once, share memory
// - For DOM events, keep handlers in JS, call Wasm with batched data
// Pseudocode: batched event handling
let pendingEvents = [];
button.addEventListener('click', e => {
pendingEvents.push(...);
if (pendingEvents.length >= 100) {
exports.processBatch(ptr, pendingEvents.length);
pendingEvents = [];
}
});Security Model
Sandbox Model
Wasm's security foundation is the sandbox: a module cannot access host memory, files, network, or devices unless the host explicitly provides functions for them via imports. This makes Wasm safe to run untrusted code — it can only do what its imports allow. Combined with type safety, this prevents buffer overflows and RCE.
// Wasm executes in a sandboxed virtual machine:
// - No direct access to host memory (only its linear memory)
// - No syscalls (must go through imported functions)
// - No I/O unless the host provides it
// - Control flow is structured (no arbitrary jumps)
// - Type-checked at compile and run time
// All host access is EXPLICIT through imports.
// A module that didn't import "fs" cannot read files.
// Code:
// (module
// (import "env" "log" (func (param i32)))
// ;; the ONLY host capability this module has is log()
// )Same-Origin Policy & Fetch
Wasm doesn't escape the browser's security model — same-origin policy and CORS apply to .wasm fetches just like any resource. Cross-origin Wasm requires proper CORS headers and the application/wasm MIME type. Wasm also cannot bypass CSP, cookies, or any other browser security mechanism.
// Wasm modules obey the same-origin policy:
// - fetch('https://other-origin/x.wasm') needs CORS headers
// - The remote server must send:
// Access-Control-Allow-Origin: *
// - And the correct MIME: application/wasm
// Load cross-origin Wasm with streaming:
const { instance } = await WebAssembly.instantiateStreaming(
fetch('https://cdn.example.com/lib.wasm', {
mode: 'cors'
}),
imports
);
// Wasm cannot bypass CORS — it's subject to the
// browser's normal security model.Memory Isolation
Wasm memory is bounds-checked at runtime — any OOB access traps immediately rather than corrupting memory. Modules cannot read each other's memory, the JS heap, or arbitrary host addresses. Function pointers (call_indirect) are type-checked, preventing call-target forgery. This eliminates entire classes of C/C++ vulnerabilities.
// Each Wasm instance has its OWN linear memory.
// - Out-of-bounds access traps (RuntimeError)
// - One module's memory cannot read another's
// - JS writes via TypedArray views are bounds-checked
// Even shared memory (threads) requires explicit
// opt-in and is still bounded:
const mem = new WebAssembly.Memory({ initial: 1, maximum: 10 });
// Wasm code CANNOT:
// - Read JS heap
// - Read other instances' memory
// - Forge function pointers (call_indirect type-checks)
// - Read arbitrary host memory addresses
// All memory access is bounds-checked at runtime.Capability-Based Security
Wasm follows capability-based security: the import object IS the module's capabilities. By carefully choosing what to import, you give a module minimal privilege. A plugin that only imports log() cannot do anything else. This is the foundation of safe plugin systems (Figma, Shopify functions, etc.).
// Capability security: a module can ONLY do what
// the host explicitly grants via imports.
// Strict imports = minimal privilege
const imports = {
env: {
log: msg => console.log('[wasm]', msg)
// NO file IO, NO network, NO timers
}
};
WebAssembly.instantiateStreaming(fetch('plugin.wasm'), imports);
// The plugin can ONLY call log(). It cannot:
// - Access the DOM
// - Make network requests
// - Read files
// - Spawn workers
// unless you import those capabilities.CSP & Trusted Types
Content Security Policy treats Wasm like eval — by default, CSP blocks it. Add 'wasm-unsafe-eval' to script-src to allow Wasm while keeping eval() blocked. This is more permissive than unsafe-eval and recommended for production. Trusted Types compatibility requires wrapping any dynamic JS glue in a policy.
// Content Security Policy applies to Wasm:
// - 'wasm-unsafe-eval' is required for instantiation
// - Without it, CSP blocks WebAssembly.compile
// HTTP header:
// Content-Security-Policy:
// default-src 'self';
// script-src 'self' 'wasm-unsafe-eval';
// Wasm output (e.g., Emscripten) often uses eval-like
// patterns for instantiation — CSP 'wasm-unsafe-eval'
// specifically allows Wasm while still blocking eval().
// Trusted Types: Wasm glue code must produce
// trusted scripts; use policy.createScript() for
// any dynamically generated JS that loads Wasm.Tools Chain
WABT (wat2wasm, wasm2wat)
WABT is the canonical toolkit for working with Wasm text and binary formats. wat2wasm/wasm2wat round-trip; wasm-objdump inspects structure; wasm-interp runs modules in a reference interpreter. wasm-decompile produces a C-like readable form. Essential for hand-authoring, debugging, and teaching.
# WABT = WebAssembly Binary Toolkit
# Install:
# macOS: brew install wabt
# Ubuntu: apt install wabt
# Windows: scoop install wabt
# Tools included:
wat2wasm hello.wat -o hello.wasm # text -> binary
wasm2wat hello.wasm -o hello.wat # binary -> text
wasm-validate hello.wasm # validate
wasm-objdump -x hello.wasm # inspect structure
wasm-interp hello.wasm # interpret (run)
wasm-decompile hello.wasm -o hello.dly # decompile to C-like
# Online: https://webassembly.github.io/wabt/demo/Emscripten SDK
Emscripten is the most mature Wasm toolchain — compiles C/C++, Fortran, and other LLVM languages. emcc/em++ wrap Clang; emcmake/emconfigure let you build existing CMake/Autotools projects unmodified. It also provides libc, SDL, OpenGL (via WebGL), and pthreads ports for compatibility.
# Emscripten: compile C/C++/LLVM languages to Wasm
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest
source ./emsdk_env.sh
# Key tools:
emcc # C/C++ compiler (gcc/clang-like)
em++ # C++ compiler
emcmake # wrapper for cmake
emconfigure # wrapper for ./configure
emranlib # archive tool
emar # archiver
# Common output:
emcc app.c -O3 -o app.js # JS glue + .wasm
emcc app.c -o app.html # runnable HTML demowasm-pack (Rust)
wasm-pack is the official Rust→Wasm tool: builds, runs wasm-bindgen, generates TypeScript types, and packages for npm. --target chooses the JS module format. wasm-pack test runs Rust tests in real browsers via webdriver. Use the wasm-pack-template for a ready project scaffold.
# wasm-pack: build, test, publish Rust Wasm packages
cargo install wasm-pack
# Build for various targets
wasm-pack build --target web # modern browsers
wasm-pack build --target bundler # webpack/vite
wasm-pack build --target nodejs # Node.js
# Test in a headless browser
wasm-pack test --headless --chrome
# Publish to npm
wasm-pack publish
# Generate a template project
cargo generate --git https://github.com/rustwasm/wasm-pack-templateBinaryen (wasm-opt)
Binaryen is the optimization toolkit Emscripten and wasm-pack use internally. wasm-opt runs Wasm-level passes (inlining, dead-code elimination, constant folding) — always run it on production builds. -O3 for speed, -Oz for minimum size. wasm-metadce removes unused exports across modules.
# Binaryen: Wasm optimization toolkit
# Often installed via Emscripten or standalone:
npm install -g binaryen
# wasm-opt: optimize .wasm files
wasm-opt -O3 input.wasm -o output.wasm # speed
wasm-opt -Os input.wasm -o output.wasm # size
wasm-opt -Oz input.wasm -o output.wasm # min size
# Other tools:
wasm-as input.wat -o output.wasm # assemble
wasm-dis input.wasm -o output.wat # disassemble
wasm-merge a.wasm b.wasm -o merged.wasm # link
wasm-shell input.wat # interpreter
wasm-metadce input.wasm -o output.wasm # aggressive dead-codewasm-tools (Bytecode Alliance)
wasm-tools is a modern Rust-based toolkit from Bytecode Alliance. It parses/prints/validates, but its standout feature is component-model support (component new/embed/describe). It's stricter and faster than WABT for many operations and is the recommended tool for component-model work.
# wasm-tools: modern Rust-based Wasm toolkit
cargo install wasm-tools
# Subcommands:
wasm-tools parse input.wat -o output.wasm # text -> binary
wasm-tools print input.wasm -o output.wat # binary -> text
wasm-tools validate input.wasm # validate
wasm-tools component new input.wasm -o c.wasm # wrap as component
wasm-tools strip input.wasm -o stripped.wasm # remove debug info
wasm-tools objdump input.wasm # inspect
wasm-tools smith # generate random modules (fuzzing)
# Component model support is a key feature.
# Stronger WAT parsing than WABT in edge cases.Runtimes (wasmtime, wasmer, WasmEdge)
Three major standalone runtimes: wasmtime (Bytecode Alliance reference, best component-model support), wasmer (multiple backends including LLVM for peak perf, JS bindings), WasmEdge (CNCF, optimized for cloud-native and AI inference). All implement WASI, so the same .wasm runs across them. Pick based on ecosystem fit.
# wasmtime — Bytecode Alliance reference runtime
curl https://wasmtime.dev/install.sh -sSf | bash
wasmtime app.wasm
# wasmer — cross-platform, multiple backends
curl https://get.wasmer.io -sSf | sh
wasmer run app.wasm
# WasmEdge — CNCF, optimized for cloud/edge
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
wasmedge app.wasm
# Comparison:
# wasmtime: reference, WASI preview1+2, components
# wasmer: multiple backends (Cranelift, LLVM), JS API
# WasmEdge: AI/ML extensions, Kubernetes-friendly
// All three run WASI modules portably.Components & Best Practices
Wasm Components Overview
The Component Model is Wasm's evolution: a module format that lets components communicate with rich types (strings, records, lists) without manually marshaling through linear memory. Components compose like libraries across languages (Rust component + JS component). Supported by wasmtime, WasmEdge, and the wider Bytecode Alliance ecosystem.
// Component Model: a higher-level module format
// that lets Wasm modules talk to each other and hosts
// using rich types (strings, records, variants),
// not just (i32, f64).
// Core Wasm module: exports raw functions over memory.
// Component: wraps a core module, exposes typed
// interfaces described in WIT (Wasm Interface Type).
// Build flow:
// 1. Write WIT interface
// 2. Generate bindings for your language (Rust, C, JS)
// 3. Implement, compile to core .wasm
// 4. Wrap into a component with wasm-tools
// Tooling:
wasm-tools component new app.wasm -o app.wasm
// Run with wasmtime:
wasmtime app.wasm # uses component interfacesWIT (Wasm Interface Type)
WIT (Wasm Interface Type) is the IDL for components — declare records, variants, enums, resources, and functions. wit-bindgen generates language-specific bindings (Rust, C, JS, Python). The 'world' keyword bundles imports and exports a component provides/consumes. This is the foundation of portable, language-agnostic Wasm libraries.
// example.wit — describe a component's interface
package example:greeter;
interface api {
// A record type
record person {
name: string,
age: u32,
}
// A function taking/returning rich types
greet: func(p: person) -> string;
}
world greeter-world {
export api;
}
# Generate bindings:
# Rust: cargo component bindings
# JS: jco transpile
# C/C++: wit-bindgen c component.wit
# wit-bindgen is the binding generator
cargo install wit-bindgen-cliBest Practice: Small, Focused Modules
Prefer several small, focused Wasm modules over one monolith. Smaller modules download faster, cache better, and can be loaded lazily (load the audio module only when needed). Share memory across modules via imports, or use the component model for type-safe inter-module calls. Run wasm-opt -Oz on each.
// GOOD: One focused .wasm per concern
// - renderer.wasm (drawing)
// - audio.wasm (mixing)
// - physics.wasm (simulation)
// Loaded lazily as needed.
// BAD: One giant monolithic .wasm
// - app.wasm (1MB+, includes everything)
// Benefits of small modules:
// - Faster initial load (only what's needed)
// - Better caching (changed modules re-download)
// - Easier parallel development
// - Smaller attack surface per module
// Use wasm-opt -Oz to minimize each module.
// Share memory across modules via imports.Best Practice: Error Handling
Wasm traps (divide-by-zero, OOB memory, unreachable) cannot be caught from inside Wasm — they abort the function. Design exported functions to return error codes or Result types instead of trapping. Catch WebAssembly.RuntimeError on the JS side as a last resort. The component model supports typed error variants natively.
// Wasm traps are unrecoverable — handle errors gracefully.
// BAD: let divide crash
(func (export "div") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.div_s ;; traps on 0
)
// GOOD: return a sentinel or error code
(func (export "div") (param i32 i32) (result i32)
local.get 1
i32.eqz
if (result i32)
i32.const -1 ;; error sentinel
else
local.get 0
local.get 1
i32.div_s
end
)
// In Rust/AS: use Result<T, E> and export
// error variants via the component model.Best Practice: Memory Management
Treat Wasm memory like C: every alloc needs a matching free. Export alloc/free helpers and wrap JS-side buffer use in try/finally. Re-create TypedArray views after any memory.grow (the ArrayBuffer detaches). Document ownership: who is responsible for freeing — the caller or the callee? Leaks degrade perf over time.
// Export alloc/free and use them consistently.
// C/C++ (Emscripten): _malloc / _free
// Rust: wasm_bindgen's __alloc / __free
// AssemblyScript: __new / __retain / __release
// JS wrapper for safe buffer passing:
function withBuffer(size, fn) {
const ptr = exports.alloc(size);
try {
return fn(new Uint8Array(memory.buffer, ptr, size));
} finally {
exports.free(ptr, size);
}
}
// NEVER leak:
// - Re-create TypedArray views after memory.grow
// - Free buffers passed in from JS
// - For long-lived allocations, document ownershipBest Practice: Testing & CI
Test Wasm in real environments: wasm-pack test runs Rust tests in headless browsers; AssemblyScript and Emscripten have their own runners. Add wasm-validate and size checks to CI. Use -s ASSERTIONS=1 (Emscripten) and debug builds to catch issues early. Lint with wat2wasm and always run wasm-opt on release artifacts.
# Test Wasm just like any other code.
# Rust: wasm-pack test runs in a real browser
wasm-pack test --headless --chrome --firefox
# AssemblyScript: use the built-in test framework
npm run test
# C/C++ (Emscripten): use emcc with -s ASSERTIONS=1
# and write tests in C, run with node:
node tests.js
# Validate .wasm in CI:
wasm-validate module.wasm
# Check size budgets:
ls -la module.wasm # e.g., must be < 200KB
# Lint WAT with wat2wasm (catches syntax errors)
# Run wasm-opt -O3 to ensure optimization is applied
# GitHub Actions example:
# - uses: actions-rs/toolchain@v1
# - run: cargo install wasm-pack
# - run: wasm-pack test --headlessSnippets de WebAssembly relacionados
Copy-paste ready code for common tasks.
Module Instantiation
Compile and instantiate a .wasm module.
Linear Memory
Shared, growable byte buffer.
Tables
Function references for indirect calls.
Imports and Exports
Exchange functions, memory, and globals.
JS Interop
Pass strings through shared memory.
Text Format (WAT)
Human-readable WebAssembly.
Performance
Benchmark and parallelize WASM workloads.
Debugging
Inspect modules and handle traps.
Was this helpful?