Frank DENIS random thoughts.

Compiling C to WebAssembly using clang/LLVM and WASI.

LLVM 8 has recently been released, with quite a few significant changes and improvements. One of them being that the WebAssembly target is no longer considered experimental, and is built by default.

Around the same time, Mozilla’s WASI was announced. WASI includes a modified musl C library designed to work on top of a minimal set of external functions (/imports /hostcalls /whatever you call functions called from WebAssembly that are not defined in the module itself).

This set of external functions has already been implemented, with different levels of completion, in several WebAssembly runtimes: wasmtime, wasmer and lucet-wasi.

Compiling C code to WebAssembly using standard tools, and running the resulting module should now be way easier than it used to.

Unfortunately, documentation is still lacking, so here’s a quick guide to achieve this.

Install clang/LLVM 8

If you are using macOS, brew install llvm will immediately get you the latest stable version of LLVM, with (almost) everything you need to build WebAssembly modules.

Linux distributions with up-to-date packages should also have LLVM 8 packages ready to install. On Arch Linux pacman -S llvm clang will do the job.

Install WASI

Clone wasi-sysroot.

On macOS, the Homebrew version of clang should be used instead of the Xcode one. This can be achieved with:

export PATH=/usr/local/opt/llvm/bin:$PATH

Compile and install WASI with:

make install INSTALL_DIR=/tmp/wasi

Obviously, this can be installed to a more permanent location. The default is /usr/local, which may mess with files installed through proper packages, so you may want to always supply a custom INSTALL_DIR.

Try compiling to WebAssembly

Start with something simple, saved as example.c:

#include <stdio.h>

int main(void)
{
    puts("Hello");
    return 0;
}

Make sure that you are still using the correct clang/llvm versions.

As we previously did in order to compile WASI, on macOS, put the Homebrew path before the system path in order to do so:

export PATH=/usr/local/opt/llvm/bin:$PATH

Compile the previous example with:

clang --target=wasm32-unknown-wasi --sysroot /tmp/sysroot \
  -O2 -s -o example.wasm example.c

Which may fail with:

/usr/lib/clang/8.0.0/lib/wasi/libclang_rt.builtins-wasm32.a:
  No such file or directory

Damn. We were so close.

wasi/libclang_rt.builtins-wasm32.a

The wasi/libclang_rt.builtins-wasm32.a file is part of the clang_rt package distributed with the WASI SDK.

Since compiling it is quite time consuming, you can directly get the precompiled file here: libclang_rt.builtins-wasm32.a.

The same file works everywhere, since this archive contains WebAssembly code.

Copy it where the previous error message suggests. On Linux, it’s likely to be something like the above. On macOS, it should be in /usr/local/Cellar/llvm/8.0.0/lib/clang/8.0.0/lib/wasi/.

You probably have to create the wasi folder first.

Note that this file is not required to build WebAssembly objects. But it definitely is if you want to build a module.

Really compile to WebAssembly

Your compilation environment is finally complete.

clang --target=wasm32-unknown-wasi --sysroot /tmp/sysroot \
  -Os -s -o example.wasm example.c

should succeed and produce a WebAssembly module called example.wasm. Yay!

Run it

This is a WebAssembly module. Which has to be interpreted and/or translated to native code in order to be executed.

Since it uses WASI, we need something that natively supports the WASI low-level API.

To compile and/or install them, see these project’s respective documentation.

With wasmtime

wasmtime /tmp/example.wasm

Hello

With wasmer

wasmer run /tmp/example.wasm

Hello

With lucet

Move example.wasm to the directory of the Lucet source code first, as Docker cannot access parent directories.

The runtime (lucet-wasi) loads native code that has to be compiled first, using lucetc-wasi:

source ./devenv_setenv.sh
lucetc-wasi -o example example.wasm
lucet-wasi example

Hello

Compiling more complex projects

In order to compile some projects, you may need to use llvm-ar instead of ar, llvm-nm instead of llvm, llvm-strip instead of strip, and llvm-ranlib instead of ranlib.

Failure to do so with result in symbols not being found at link time, or archives containing objects with paths that don’t make any sense.

Using autotools

The wasm32-unknown-wasi target is not recognized by autotools yet. Namely, the config.sub needs to be patched.

Here is a simple way to do it:

grep -q -F -- '-wasi' config.sub || \
  sed -i -e 's/-nacl\*)/-nacl*|-wasi)/' config.sub

CFLAGS and LDFLAGS

On a variety of tests, -Os and -O2 appear to be the optimization flags producing the fastest code.

The LLVM linker (lld) currently has a bug that can make builds randomly stall. A workaround is to add -Wl,--no-threads to LDFLAGS.