Hello world from a WASM module in a static binary

Dario Castañé - Feb 22 - - Dev Community

These are some quick notes documenting a Saturday morning's experiment. All the steps were executed on Linux.

TL;DR: What?

Compiling a static binary that runs a compiled Ahead-of-Time (AOT) WASM module.

Ideally it'd have been WASM bytecode compiled to native code, but I couldn't find a way that didn't rely on the WASM runtime, invoking it from the command line or embedded in a C program.

Why?

I've been thinking and learning this week about WebAssembly and its potential since I read "WASM will replace containers".

I decided initially to use Wasmer and ended filing a question on their repository because their own native binary build command doesn't work as expected.

Finally, I landed on Bytecode Alliance's WebAssembly Micro Runtime (WAMR) wamrc compiler.

How?

The code

(module
  ;; Import the required WASI functions
  (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))

  ;; Define a buffer to store the message
  (memory (export "memory") 1)
  (data (i32.const 8) "Hello, World!\n")

  ;; Define the _start function
  (func $main (export "_start")
    ;; Setup the iovec array
    (i32.store (i32.const 0) (i32.const 8))    ;; pointer to the message
    (i32.store (i32.const 4) (i32.const 14))   ;; length of the message

    ;; Call fd_write
    (call $fd_write
      (i32.const 1)  ;; file_descriptor - 1 for stdout
      (i32.const 0)  ;; *iovs - pointer to the iovec array
      (i32.const 1)  ;; iovs_len - number of iovec entries
      (i32.const 20) ;; nwritten - where to store the number of bytes written
    )
    drop ;; Discard the result
  )
)
Enter fullscreen mode Exit fullscreen mode

This is WebAssembly Text. Learn more here.

The tools

Everything had to be compiled from scratch.

wasm-opt isn't really required, so I'll skip it. I learnt about it while looking at these benchmarks, and I want these notes to keep track of the interesting tidbits I discovered.

The process

First, let's generate WASM bytecode from our WebAssembly Text code:

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

Next, compile it:

wamrc --format=aot -o ./hello_world.aot ./hello_world.wasm
Enter fullscreen mode Exit fullscreen mode

For the last step, we need some C glue to embed the WAMR runtime:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "wasm_export.h"  // Now points to WAMR's core/iwasm/include/
#include "hello_world.h"

// Define a static pool buffer with proper alignment
#define POOL_SIZE (256 * 1024)  // 256 KB pool
static uint8_t global_pool_buf[POOL_SIZE] __attribute__((aligned(8)));  // 8-byte alignment

int main() {
    RuntimeInitArgs init_args;
    memset(&init_args, 0, sizeof(RuntimeInitArgs));

    // Configure memory pool
    init_args.mem_alloc_type = Alloc_With_Pool;  // Use pool allocator
    init_args.mem_alloc_option.pool.heap_buf = global_pool_buf;
    init_args.mem_alloc_option.pool.heap_size = sizeof(global_pool_buf);

    // Initialize the WAMR runtime
    if (!wasm_runtime_full_init(&init_args)) {
        printf("Runtime initialization failed.\n");
        return -1;
    }

    // Load the AOT module from the embedded blob
    char error_buf[128];
    wasm_module_t module = wasm_runtime_load(hello_world_aot, hello_world_aot_len, error_buf, sizeof(error_buf));
    if (!module) {
        printf("Load failed: %s\n", error_buf);
        return -1;
    }

    // Instantiate the module
    wasm_module_inst_t inst = wasm_runtime_instantiate(module, 65536, 65536, error_buf, sizeof(error_buf));
    if (!inst) {
        printf("Failed to instantiate: %s\n", error_buf);
        return -1;
    }

    // Call a function named "main" exported from the WASM module
    wasm_function_inst_t func = wasm_runtime_lookup_function(inst, "_start");
    if (!func) {
        printf("Function '_start' not found.\n");
        return -1;
    }

    wasm_exec_env_t env = wasm_runtime_create_exec_env(inst, 65536);
    if (!env) {
        printf("Failed to create environment.\n");
        return -1;
    }

    // Execute the function
    wasm_runtime_call_wasm(env, func, 0, NULL);

    // Cleanup
    wasm_runtime_deinstantiate(inst);
    wasm_runtime_unload(module);
    wasm_runtime_destroy();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The sharp reader may be asking "Where are the wasm_export.h and hello_world.h includes? The former is in wasm-micro-runtime/core/iwasm/include, and the later needs to be generated:

xxd -i hello_world.aot > hello_world.h
Enter fullscreen mode Exit fullscreen mode

And we can compile and run it:

gcc -static $HOME/Code/hello-wasm/main.c $HOME/Code/wasm-micro-runtime/product-mini/platforms/linux/build/libiwasm.a -lm -I $HOME/Code/wasm-micro-runtime/core/iwasm/include -I $HOME/Code/hello-wasm -o $HOME/Code/hello-wasm/hello_world

$HOME/Code/hello-wasm/hello_world
Hello, World!
Enter fullscreen mode Exit fullscreen mode

Note: it's not a perfect statically-linked binary. It depends on getaddrinfo, but this is better left out of scope right now.

Image by Duncan Cumming.

. . . .