DEV Community

Yangholmes
Yangholmes

Posted on

Notes on Using wabt

wabt is a WebAssembly binary toolkit that provides compilation, analysis, debugging, and validation tools for wasm-related code. This article briefly introduces the usage of common commands.

Compiling wat Code

Implementing the Fibonacci sequence in wat:

;; fib.wat
(module
  (import "env" "log" (func $log (param i32)))

  ;; Allocate one page of memory
  (memory (export "memory") 1)

  ;; Global variable: heap pointer (points to next available memory address)
  (global $heap_ptr (mut i32) (i32.const 0))

  ;; Allocate memory block
  ;; params: size (i32) - bytes to allocate
  ;; return: starting address (i32)
  (func $allocate (param $size i32) (result i32)
    (local $start i32)
    (local.set $start (global.get $heap_ptr))
    (global.set $heap_ptr
      (i32.add
        (global.get $heap_ptr)
        (local.get $size)
      )
    )
    (local.get $start)
  )

  ;; Fibonacci sequence
  ;; params: n (i32) - array length
  ;; return: array starting address (i32)
  (func (export "fib") (param $n i32) (result i32)
    (local $i i32)
    (local $arr_ptr i32)
    (local $prev i32)
    (local $curr i32)
    (local $next i32)

    ;; Allocate memory: n * sizeof(i32) = n * 4
    (local.set $arr_ptr
      (call $allocate
        (i32.mul
          (local.get $n)
          (i32.const 4)
        )
      )
    )

    ;; Handle edge cases
    (if (i32.le_s (local.get $n) (i32.const 0))
      (then (return (local.get $arr_ptr)))  ;; Return empty array address
    )

    ;; Initialize first two elements
    (i32.store (local.get $arr_ptr) (i32.const 0))
    (if (i32.gt_s (local.get $n) (i32.const 1))
      (then
        (i32.store
          (i32.add (local.get $arr_ptr) (i32.const 4))
          (i32.const 1)
        )
      )
    )

    ;; Iteratively calculate subsequent elements
    (local.set $prev (i32.const 0))
    (local.set $curr (i32.const 1))
    (local.set $i (i32.const 2))
    (loop $loop
      ;; Calculate next Fibonacci number
      (local.set $next (i32.add (local.get $prev) (local.get $curr)))
      (local.set $prev (local.get $curr))
      (local.set $curr (local.get $next))

      ;; Store in memory
      (i32.store
        (i32.add
          (local.get $arr_ptr)
          (i32.mul (local.get $i) (i32.const 4))
        )
        (local.get $next)
      )

      ;; Loop control
      (local.set $i (i32.add (local.get $i) (i32.const 1)))
      (br_if $loop (i32.lt_s (local.get $i) (local.get $n)))
    )

    ;; Return array starting address
    (local.get $arr_ptr)  
  )
)
Enter fullscreen mode Exit fullscreen mode

Compile the code using wabt:

wat2wasm ./fib.wat -o ./fib.wasm
Enter fullscreen mode Exit fullscreen mode

Resulting wasm files:

├── fib.wasm
├── fib.wat
└── main.ts
Enter fullscreen mode Exit fullscreen mode

main.ts provides the host environment for calling wasm code:

import fibUrl from './fib.wasm?url';

WebAssembly.instantiateStreaming(fetch(fibUrl), {
  env: {
    log: (value: string | number) => console.log(value)
  }
}).then(res => {
  // Destructure exports to get fib function and memory
  const { fib, memory } = res.instance.exports as unknown as {
    fib: (n: number) => number;
    memory: WebAssembly.Memory
  };
  // Fibonacci sequence length
  const n = 10;
  // Compute, storing result in memory, get result pointer
  const addr = fib(n);
  // Read result
  const buffer = new Uint32Array(memory.buffer, addr, n);
  const result = Array.from(buffer);
  console.log(result);
})
Enter fullscreen mode Exit fullscreen mode

Using Vite as the build tool, which supports importing any resource type as a URL. Here we convert wasm to a resource URL and load it using WebAssembly.instantiateStreaming and fetch. Note that Vite also supports automatic wasm initialization using the ?init suffix (see Vite documentation for details).

We use the fib function to compute a Fibonacci sequence of length 10, with output:

Results match expectations.

Comparing wat code and wasm file sizes:

wasm vs. wat

After compilation, the wasm file is an order of magnitude smaller than the source. This efficiency comes from both wasm's compact format and LEB128 compression.

Analyzing wasm

The wasm-objdump command is similar to the OS objdump, used for analyzing wasm file information. Practical use case: A developer receives a wasm module and wants to quickly identify exported functions, their parameter counts/types, and return value types/lengths. Using the previous fib.wasm as an example:

wasm-objdump ./fib.wasm -j Export -x
Enter fullscreen mode Exit fullscreen mode

Output:

fib.wasm:       file format wasm 0x1

Section Details:

Export[2]:
 - memory[0] -> "memory"
 - func[2] <fib> -> "fib"
Enter fullscreen mode Exit fullscreen mode

This shows fib.wasm has two exports: a memory and a fib function (type 2). Next, examine function signatures:

wasm-objdump ./fib.wasm -j Function -x
Enter fullscreen mode Exit fullscreen mode

Output:

fib.wasm:       file format wasm 0x1

Section Details:

Function[2]:
 - func[1] sig=1
 - func[2] sig=1 <fib>
Enter fullscreen mode Exit fullscreen mode

Function 2 uses type index 1 declared in the Type section. Export the Type section:

wasm-objdump ./fib.wasm -j Type -x
Enter fullscreen mode Exit fullscreen mode

Type information:

fib.wasm:       file format wasm 0x1

Section Details:

Type[2]:
 - type[0] (i32) -> nil
 - type[1] (i32) -> i32
Enter fullscreen mode Exit fullscreen mode

We've now obtained complete information about fib.wasm's exports:

  1. Exported memory
  2. Exported function fib with one i32 parameter and one i32 return value

wasm-objdump -x can be used alone to output all section information. For large files, it's better to output sections individually for easier analysis.

Formatting Code

The wat-desugar command formats existing wat code to conform to certain specifications. For example, the original fib.wat source didn't strictly follow the "push operands -> execute instruction" pattern, often writing operands after instructions. While valid, this doesn't follow stack machine conventions. wat-desugar helps standardize this code. Here's the formatted $allocate function:

(func $allocate (param $size i32) (result i32)
    (local $start i32)
    global.get $heap_ptr
    local.set $start
    global.get $heap_ptr
    local.get $size
    i32.add
    global.set $heap_ptr
    local.get $start
)
Enter fullscreen mode Exit fullscreen mode

Compared to the original source, the formatted code is more compact and better follows stack-based calling conventions, though less readable. The most noticeable difference is the i32.add operation: the original placed operands after the instruction, while the standardized version pushes operands to the stack before calling the add instruction.

The name "desugar" contrasts with "Syntactic Sugar". Syntax like (i32.add (local.get 0) (local.get 1)) is syntactic sugar - operands aren't pushed to the stack before the instruction call (more like a register machine). Compilers accept this non-stack-machine syntax because it's more intuitive. "Desugar" is like the Cantonese phrase "走糖" (less sugar), removing the syntactic sugar to reveal the most fundamental code that strictly follows the push-operands-then-execute pattern.

Disassembly

wabt provides three disassembly commands:

  • wasm2wat: Disassembles wasm to wat code

  • wasm2c: Disassembles wasm to C source and header files

  • wasm-decompile: Disassembles wasm to readable C-style pseudocode

In practice, wasm-decompile combined with wasm2wat is most useful. Use wasm-decompile to analyze functionality implementation. If minor module modifications are needed, use wasm2wat to get a wat file, make changes, then recompile to wasm.

Top comments (0)