Update (2023-04-22): here's a repository with many WAT code samples, including this one: wasm-wat-samples.

This is a brief blog post that mostly consists of a single, well-documented code snippet.

I've been getting more and more interested in WebAssembly recently, and found there's a dearth of high-quality WAT (WebAssembly Text language) code samples dealing with some of the trickier aspects of WASM like working with strings and passing data between WASM and the host [1] via memory.

So here's a complete WASM module written in WAT that exports an itoa function -- just like its C counterpart, it converts an integer into a string representation:

(module
    ;; Logging function imported from the environment; will print a single
    ;; i32.
    (import "env" "log" (func $log (param i32)))

    ;; Declare linear memory and export it to host. The offset returned by
    ;; $itoa is relative to this memory.
    (memory (export "memory") 1)

    ;; Using some memory for a number-->digit ASCII lookup-table, and then the
    ;; space for writing the result of $itoa.
    (data (i32.const 8000) "0123456789")
    (global $itoa_out_buf i32 (i32.const 8010))

    ;; itoa: convert an integer to its string representation. Only supports
    ;; numbers >= 0.
    ;; Parameter: the number to convert
    ;; Result: address and length of string in memory.
    ;; Note: this result is only valid until the next call to $itoa which will
    ;; overwrite it; obviously, this isn't concurrency-safe either.
    (func $itoa (export "itoa") (param $num i32) (result i32 i32)
        (local $numtmp i32)
        (local $numlen i32)
        (local $writeidx i32)
        (local $digit i32)
        (local $dchar i32)

        ;; Count the number of characters in the output, save it in $numlen.
        (i32.lt_s (local.get $num) (i32.const 10))
        if
            (local.set $numlen (i32.const 1))
        else
            (local.set $numlen (i32.const 0))
            (local.set $numtmp (local.get $num))
            (loop $countloop (block $breakcountloop
                (i32.eqz (local.get $numtmp))
                br_if $breakcountloop

                (local.set $numtmp (i32.div_u (local.get $numtmp) (i32.const 10)))
                (local.set $numlen (i32.add (local.get $numlen) (i32.const 1)))
                br $countloop
            ))
        end

        ;; Now that we know the length of the output, we will start populating
        ;; digits into the buffer. E.g. suppose $numlen is 4:
        ;;
        ;;                     _  _  _  _
        ;;
        ;;                     ^        ^
        ;;  $itoa_out_buf -----|        |---- $writeidx
        ;;
        ;;
        ;; $writeidx starts by pointing to $itoa_out_buf+3 and decrements until
        ;; all the digits are populated.
        (local.set $writeidx
            (i32.sub
                (i32.add (global.get $itoa_out_buf) (local.get $numlen))
                (i32.const 1)))

        (loop $writeloop (block $breakwriteloop
            ;; digit <- $num % 10
            (local.set $digit (i32.rem_u (local.get $num) (i32.const 10)))
            ;; set the char value from the lookup table of digit chars
            (local.set $dchar (i32.load8_u offset=8000 (local.get $digit)))

            ;; mem[writeidx] <- dchar
            (i32.store8 (local.get $writeidx) (local.get $dchar))

            ;; num <- num / 10
            (local.set $num (i32.div_u (local.get $num) (i32.const 10)))

            ;; If after writing a number we see we wrote to the first index in
            ;; the output buffer, we're done.
            (i32.eq (local.get $writeidx) (global.get $itoa_out_buf))
            br_if $breakwriteloop

            (local.set $writeidx (i32.sub (local.get $writeidx) (i32.const 1)))
            br $writeloop
        ))

        ;; return (itoa_out_buf, numlen)
        (global.get $itoa_out_buf)
        (local.get $numlen)
    )
)

Some notes about this code:

  • itoa uses the multi-value feature of WASM to return multiple values. This feature is supported pretty uniformly by WASM hosts at this point.
  • itoa writes its string output into memory, and returns the address of this string and its length to the host. This address (in WASM's linear memory) is currently hard-coded; there is no dynamic memory allocation built into WASM. It's possible to implement it, and higher-level languages do, but for a simple examples this will do.
  • The module exports its linear memory to the host so that the host can read the string itoa wrote.
  • The algorithm is straightforward and unoptimized; it runs one O(log(N)) loop to find the output size, and another such loop to populate the output.

For the accompanying host code and instructions for compiling & running, see the GitHub repository.

Exercises

The itoa function presented here has a few limitations that can be fixed without too much effort; if you're interested in WAT programming, these could be good exercises:

  • Extend it to support negative numbers
  • Extend it to work for other common bases like hexadecimal (will require an additional parameter).

[1]A note on nomenclature: by host I mean the execution environment a WASM module runs in. The most common environment is a web browser, but recently it's more and more common to see WASM executed in non-browser environments like Node.js, wasmtime (Rust) or wazero (Go).