Skip to content

WebAssembly Hoja de referencia

Binary instruction format for web-based fast execution.

01

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.

webassembly
// 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.

webassembly
// 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 globals

Browser 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.

webassembly
// 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.

webassembly
// 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/wasm

Wasm 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.).

webassembly
// 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.

webassembly
<!-- 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
  )
)
02

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.

webassembly
;; 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.

webassembly
;; 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).

webassembly
;; 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.

webassembly
;; 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.

webassembly
;; 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.

webassembly
# 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.wasm
03

Value 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.

webassembly
;; 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 '-'.

webassembly
;; 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^53

Type 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.

webassembly
;; 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_f32

v128 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.

webassembly
;; 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.add

Reference 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.

webassembly
;; 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' });
04

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.

webassembly
(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.

webassembly
(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 stack

Multiple 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.

webassembly
;; 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.

webassembly
(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.

webassembly
(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.

webassembly
(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.
05

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.

webassembly
(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.

webassembly
(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.

webassembly
;; 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 byte

Memory 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).

webassembly
;; 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.

webassembly
(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.

webassembly
// 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
}
06

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.

webassembly
(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.

webassembly
(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.

webassembly
(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.

webassembly
(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.

webassembly
(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)
  )
)
07

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.

webassembly
(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.

webassembly
(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.

webassembly
(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.

webassembly
(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 directly

Use 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.

webassembly
(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
  )
)
08

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.

webassembly
(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.

webassembly
(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 memory

Importing 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.

webassembly
(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.

webassembly
(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;                          // 0

Inline 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.

webassembly
(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.

webassembly
// 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}`);
      }
    }
  }
}
09

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.

webassembly
;; 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.

webassembly
;; 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.

webassembly
;; 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.

webassembly
;; 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.

webassembly
;; 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 -> $default

return & 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.

webassembly
(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)
10

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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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).

webassembly
// 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.

webassembly
// 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);
}
11

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.

webassembly
# 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.

webassembly
// 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.

webassembly
# 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 web

Emscripten 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.

webassembly
// 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.

webassembly
#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.

webassembly
#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.txt
12

Compiling 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.

webassembly
# 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 --version

Rust 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.

webassembly
// 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.

webassembly
# 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)); // 5

wasm-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.

webassembly
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.

webassembly
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();
}
13

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.

webassembly
# 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)); // 5

Types 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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.

webassembly
# 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 --noDebug
14

WASI (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.

webassembly
// 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_shim

wasmtime 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.

webassembly
# 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.wasm

Running 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.

webassembly
// 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] = bar

WASI 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.

webassembly
// 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.wasm

WASI 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.

webassembly
// 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 c

WASI 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.

webassembly
// 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.
15

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.

webassembly
# 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=0

wasm2wat 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.

webassembly
# 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.wat

Browser 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.

webassembly
// 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.

webassembly
# 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 sourceMappingURL

Common 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.

webassembly
// 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.
16

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.

webassembly
# 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.wasm

Memory 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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 JS
17

Wasm 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.

webassembly
// 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.

webassembly
// 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 blur

When 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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 = [];
  }
});
18

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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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.).

webassembly
// 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.

webassembly
// 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.
19

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.

webassembly
# 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.

webassembly
# 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 demo

wasm-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.

webassembly
# 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-template

Binaryen (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.

webassembly
# 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-code

wasm-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.

webassembly
# 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.

webassembly
# 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.
20

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.

webassembly
// 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 interfaces

WIT (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.

webassembly
// 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-cli

Best 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.

webassembly
// 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.

webassembly
// 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.

webassembly
// 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 ownership

Best 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.

webassembly
# 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 --headless

Was this helpful?