I have been experimenting a little bit with LLVM and its WASM backend. Unfortunately, although it is easy to find examples on how to compile a “hello world” using C, resources are a bit scattered when it comes to doing something a little more specific like compiling LLVM IR into WASM with WASI support.
I found Surma’s article on how to compile C into WASM from scratch to be a very valuable resource, and Frank Denis’s article on Compiling C to WebAssembly using clang/LLVM and WASI was especially useful, even though it is now a bit outdated.
So here is an easy-to-follow, up-to-date, step-by-step guide.
Setup 🔗
Make sure you have installed an
llvm
version that supportsWASM
. For instance, on macOS, systemclang
does not supportwasm
compilation. I installedllvm
13 usingbrew
.❯ llc --version Homebrew LLVM version 13.0.1 Optimized build. Default target: x86_64-apple-darwin21.4.0 Host CPU: icelake-client Registered Targets: ... wasm32 - WebAssembly 32-bit wasm64 - WebAssembly 64-bit ...
Download the WASI SYSROOT matching your version of LLVM. For instance, in my case, version 13.
curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-13/wasi-sysroot-13.0.tar.gz tar xzvf wasi-sysroot-13.0.tar.gz
You have now extracted the
wasi-sysroot
in your current directory. For convenience, you canexport WASI_SYSROOT=$PWD/wasi-sysroot
This will be useful later.
Download
libclang
forwasm32
matching your version of LLVM.curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-13/libclang_rt.builtins-wasm32-wasi-13.0.tar.gz tar xzvf libclang_rt.builtins-wasm32-wasi-13.0.tar.gz
This will extract
lib/wasi/libclang_rt.builtins-wasm32.a
to your current directory.
Compiling LLVM IR into WASM 🔗
We are are now ready to go. Let us create a simple program wasi.ll
that prints “Hello\n” to standard output using LLVM IR.
target triple = "wasm32-unknown-wasi"
define i32 @main() #0 {
call i32 @putchar(i32 72)
call i32 @putchar(i32 101)
call i32 @putchar(i32 108)
call i32 @putchar(i32 108)
call i32 @putchar(i32 111)
call i32 @putchar(i32 10)
ret i32 0
}
declare i32 @putchar(i32) #1
Update 2022-04-16: Shortly after I published this blog post
I have learned from Dan Gohman (@Sunfishcode) that the easiest way
to compile LLVM IR sources (*.ll
) with WASI support is to rely on clang
.
In this case, this is the proper command line:
clang wasi.ll --target=wasm32-unknown-wasi --sysroot=$WASI_SYSROOT -lc \
$PWD/lib/wasi/libclang_rt.builtins-wasm32.a -o wasi.wasm
Unless you are doing something very specific and you know what you are doing, you should not invoke
wasi-ld
directly, as the interface will be subject to changes in the future.
If you understand the risks of invoking the tools manually:
compile
wasi.ll
into an object file (wasi.o
) usingllc
:llc -march=wasm32 -filetype=obj wasi.ll
produce the final binary from
wasi.o
usingwasm-ld
:wasm-ld -m wasm32 -L$WASI_SYSROOT/lib/wasm32-wasi \ $WASI_SYSROOT/lib/wasm32-wasi/crt1.o wasi.o -lc \ $PWD/lib/wasi/libclang_rt.builtins-wasm32.a -o wasi.wasm
Regardless how you generated wasi.wasm
, that’s it! Now you can run our boring program with your favorite wasm
runtime:
wasmtime wasi.wasm
Hello
Have fun!