Lesson 03-WebAssembly Basic Programming

WebAssembly Text Format (WAT)

WAT Syntax Basics

Module Structure:

(module
  ;; Module content
)

Function Definition:

(func $add (param $a i32) (param $b i32) (result i32)
  local.get $a
  local.get $b
  i32.add)

Memory Definition:

(memory (export "memory") 1)  ;; Initial 1 page (64KB), exportable

Global Variables:

(global $counter (mut i32) (i32.const 0))  ;; Mutable i32 global variable, initial value 0

Basic Instruction Set

Numeric Operations:

i32.add    ;; Addition
i32.sub    ;; Subtraction
i32.mul    ;; Multiplication
i32.div_s  ;; Signed division
i32.eq     ;; Equality comparison
i32.lt_s   ;; Signed less-than comparison

Control Flow:

block
  ;; Code block
end

loop
  ;; Loop
end

if
  ;; Execute if condition is true
else
  ;; Execute if condition is false (optional)
end

br 1  ;; Branch to label at depth 1
br_if 1  ;; Branch if condition is true

Memory Access:

i32.load (offset=0 align=4)  ;; Load i32 from memory, offset 0, 4-byte alignment
i32.store (offset=4 align=4) ;; Store i32 to memory, offset 4, 4-byte alignment

Function Definition and Invocation

Function Parameters and Return Values:

(func $mul (param $a i32) (param $b i32) (result i32)
  local.get $a
  local.get $b
  i32.mul)

Function Invocation:

(func $main
  i32.const 2
  i32.const 3
  call $add  ;; Call $add function
)

Local Variables:

(func $calc
  (local $temp i32)
  i32.const 5
  local.set $temp
  local.get $temp
  i32.const 10
  i32.add)

Memory Management

Memory Allocation:
WebAssembly does not provide dynamic memory allocation instructions, but memory can be managed as follows:

  1. Pre-allocated Fixed-Size Memory:
(memory (export "memory") 10)  ;; Initial 10 pages (640KB)
  1. Memory Management via JavaScript:
const memory = new WebAssembly.Memory({ initial: 1 });
const wasmInstance = new WebAssembly.Instance(module, { env: { memory } });

Boundary Checks:
All memory access instructions include implicit boundary checks, and out-of-bounds access triggers an exception:

i32.load (offset=100 align=4)  ;; Throws exception if offset 100 exceeds memory range

Global Variables and Imports/Exports

Global Variables:

(global $pi (mut f64) (f64.const 3.1415926))  ;; Mutable f64 global variable

Importing Functions:

(import "env" "log" (func $log (param i32)))  ;; Import log function from env module

Importing Memory:

(import "env" "memory" (memory 1))  ;; Import 1 page of memory from env module

Exporting Functions:

(export "add" (func $add))  ;; Export $add function as "add"

Exporting Memory:

(memory (export "memory") 1)  ;; Export memory as "memory"

WebAssembly Binary Format (WASM)

WASM Binary Structure

File Structure:

Magic Number (4 bytes): 0x00 0x61 0x73 0x6D ('\0asm')
Version (4 bytes): 0x01 0x00 0x00 0x00 (Version 1)
Section Sequence...

Main Section Types:

  • Type Section (1): Function signatures
  • Import Section (2): Imported items
  • Function Section (3): Function definitions
  • Table Section (4): Function tables
  • Memory Section (5): Memory definitions
  • Global Section (6): Global variables
  • Export Section (7): Exported items
  • Start Section (8): Startup function
  • Element Section (9): Table elements
  • Code Section (10): Function bodies
  • Data Section (11): Data segments

Module Section Analysis

Type Section Example:

Section Type: 1 (Type)
Section Size: 0x0A (10 bytes)
Content:
  - Number of function types: 0x02 (2)
  - Type 1: 0x60 0x02 0x7F 0x7F 0x7F (2 i32 parameters, 1 i32 return)
  - Type 2: 0x60 0x01 0x7E 0x7F (1 i64 parameter, 1 i32 return)

Function Section Example:

Section Type: 3 (Function)
Section Size: 0x04 (4 bytes)
Content:
  - Number of functions: 0x02 (2)
  - Function Type Index 1: 0x00 (First type)
  - Function Type Index 2: 0x01 (Second type)

Binary Encoding and Decoding

LEB128 Encoding:
Variable-length integer encoding rules:

  • Most significant bit of each byte is the continuation flag (1=continue, 0=end)
  • Lower 7 bits are payload data
  • Little-endian ordering

Example Encoding:

  • Number 624485: 0xE5 0x8E 0x26
    • 0xE5: 11100101 (continue=1, value=000101)
    • 0x8E: 10001110 (continue=1, value=001110)
    • 0x26: 00100110 (continue=0, value=0100110)

WASM and WAT Conversion

Tools Used:

# WAT to WASM
wat2wasm add.wat -o add.wasm

# WASM to WAT
wasm2wat add.wasm -o add.txt

Conversion Example:

  1. WAT:
(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add))
)
  1. Corresponding WASM (Hexadecimal):
0061736d0100000001070160027f7f7f017f030201000705010161646400000a09010700200020016a0b

WASM File Structure Analysis

Analysis Tools:

# View WASM module information
wasm2wat --debug-names add.wasm

# Visualize analysis
wasm-decompile add.wasm

Typical Module Structure:

Module Header
├── Type Section (Function signatures)
├── Import Section (Imported items)
├── Function Section (Function definitions)
├── Memory Section (Memory definitions)
├── Export Section (Exported items)
├── Code Section (Function bodies)
└── Data Section (Initialized data)

WebAssembly and JavaScript Interaction

Importing and Exporting Functions

JavaScript Calling WASM Functions:

// Load WASM module
WebAssembly.instantiateStreaming(fetch('math.wasm'))
  .then(obj => {
    // Call exported add function
    console.log(obj.instance.exports.add(2, 3)); // 5

    // Call exported mul function
    console.log(obj.instance.exports.mul(4, 5)); // 20
  });

WASM Export Function Definition:

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (func $mul (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.mul)
  (export "add" (func $add))
  (export "mul" (func $mul))
)

Memory Sharing

JavaScript Accessing WASM Memory:

WebAssembly.instantiateStreaming(fetch('memory.wasm'))
  .then(obj => {
    const memory = obj.instance.exports.memory;
    const buffer = new Uint8Array(memory.buffer);

    // Write data
    buffer[0] = 65; // 'A'
    buffer[1] = 66; // 'B'

    // Read data
    console.log(String.fromCharCode(buffer[0], buffer[1])); // "AB"
  });

WASM Memory Definition:

(module
  (memory (export "memory") 1)  ;; Export 1 page of memory
  (func $write (param $offset i32) (param $value i32)
    local.get $offset
    local.get $value
    i32.store (offset=0 align=4))
  (export "write" (func $write))
)

Data Type Conversion

Numeric Conversion:

// JavaScript numbers passed directly to WASM
const result = wasmInstance.exports.add(2, 3); // Automatic conversion

// Handling large integers
const bigNum = BigInt(2**53);
const wasmResult = wasmInstance.exports.processBigInt(Number(bigNum));

String Conversion:

// JavaScript string to WASM
function stringToWasm(str, memory) {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);
  const ptr = wasmInstance.exports.alloc(bytes.length);
  new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
  return ptr;
}

// WASM string to JavaScript
function wasmToString(ptr, length, memory) {
  const bytes = new Uint8Array(memory.buffer, ptr, length);
  return new TextDecoder().decode(bytes);
}

Array Conversion:

// JavaScript array to WASM
function arrayToWasm(arr, memory) {
  const ptr = wasmInstance.exports.alloc(arr.length * 4); // Assuming i32 array
  new Int32Array(memory.buffer, ptr, arr.length).set(arr);
  return ptr;
}

// WASM array to JavaScript
function wasmToArray(ptr, length, memory) {
  return new Int32Array(memory.buffer, ptr, length);
}

Asynchronous Interaction

Promises with WASM:

async function loadAndRunWASM() {
  try {
    const wasmModule = await WebAssembly.instantiateStreaming(
      fetch('async.wasm'),
      { env: { log: console.log } }
    );

    // Call async-friendly WASM function
    const result = wasmModule.instance.exports.computeAsync();
    console.log('Result:', result);
  } catch (err) {
    console.error('WASM loading failed:', err);
  }
}

loadAndRunWASM();

Asynchronous Pattern in WASM:

;; WASM does not support async natively but can cooperate with JS:
;; 1. WASM calls JS callback function
;; 2. JS completes async operation and calls back to WASM
(func $async_operation
  (import "env" "async_callback" (func $callback (param i32)))
  ;; Set callback and return
  call $callback
  i32.const 0
)

Error Handling and Exception Passing

JavaScript Error Handling:

try {
  const wasmInstance = await WebAssembly.instantiateStreaming(fetch('module.wasm'));
  // Call WASM function that may throw
  wasmInstance.exports.might_fail();
} catch (err) {
  if (err instanceof WebAssembly.RuntimeError) {
    console.error('WASM runtime error:', err.message);
  } else {
    console.error('Other error:', err);
  }
}

WASM Error Handling Pattern:

;; Error handling in WASM typically uses error codes
(func $safe_divide (param $a i32) (param $b i32) (result i32)
  local.get $b
  i32.eqz
  if
    i32.const -1  ;; Error code -1 for division by zero
    return
  end
  local.get $a
  local.get $b
  i32.div_s
)

;; JavaScript checks error code
const result = wasmInstance.exports.safe_divide(10, 0);
if (result === -1) {
  console.error('Division by zero error');
} else {
  console.log('Result:', result);
}

Advanced Error Handling:

// Using WebAssembly.Exception (newer feature)
try {
  const wasmInstance = await WebAssembly.instantiateStreaming(fetch('exceptions.wasm'));
  wasmInstance.exports.might_throw();
} catch (e) {
  if (e instanceof WebAssembly.Exception) {
    console.error('WASM exception:', e);
    // Access exception data
    const tag = e.tag;
    const values = e.values;
  }
}
Share your love