Compile once, run anywhere with WebAssembly and WASI

3.2.2023 | 9 minutes of reading time

WebAssembly was initially created to bring languages other than JavaScript to the browser. Its design goals include portability, safety and performance. WASI (WebAssembly System Interface) lifts those capabilities to the world outside the browser. This article explores some possible use cases and shows a simple example in which WASI enables running the same binary in three different environments.

Wait, what? WASI? What is that?

WASI, the WebAssembly System Interface, aims to standardize how WebAssembly modules interact with the outside world.

WebAssembly, abbreviated wasm, is a compilation target for programming languages. It was designed to bring languages like C++ or Rust to the browser. Today it supports a multitude of other languages, and it is not limited to the browser anymore.

Each WebAssembly module runs in a sandboxed environment. It exposes functions to be called from its host, but it cannot access the host by itself. WebAssembly does not have any built-in functionality. All system calls which are normally provided by a standard library, such as reading from or writing to a file, retrieving the system clock, or generating random numbers, have to be injected by the host.

WASI provides an interface specification for these common functions. WASI-compliant runtimes like Wasmtime, Wasmer, or WasmEdge make it possible to run WebAssembly embedded in other languages, across a variety of platforms such as desktop, browser, mobile, or IoT. Furthermore, WASI already enables WebAssembly support to be built-in into databases, web servers, and container runtimes.

WASI allows building applications in any language that compiles to WebAssembly and run them in any place where a runtime is available.

Use Cases

WASI allows for a lot of interesting use cases:

Portable business logic. When developing applications for a large and diverse user base with multiple supported devices, we are faced with the challenge of code duplication across the different clients. With WASI, we can write business logic or a complicated subsystem only once, compile to WebAssembly and embed it in all client implementations. WASI runtimes such as Wasmtime provide support for many kinds of clients, from browsers over mobile phones to IoT devices. Compared to similar solutions, like the JVM or JavaScript, WebAssembly offers predictable performance, which makes it a better target for applications in domains like Multimedia, AR/VR, or CAD. The BBC for example has used WebAssembly for their multimedia offerings.

Cross-language platform. Assume you are writing an application in Go. You want to integrate a specific ML/AI library from the Python ecosystem. But also a Rust-based high performance image manipulation library. And maybe a certified implementation for a security-relevant algorithm written in C. You are faced with the task to transpile or port the existing 3rd party codebases. Or you call them via a language binding, with the challenge that it has to work for all operating systems and environments. WASI provides a convenient solution to this challenge. All those libraries could be (pre-)compiled to WebAssembly, and integrated with a WASI runtime. From that perspective, WASI might transform WebAssembly into a universal cross-language application platform. The WebAssembly package manager, wapm, could facilitate this vision.

Lightweight containers. Container runtimes such as containerd (Docker) let us run isolated container workloads by providing OS-level virtualization. WebAssembly allows sandboxed, isolated execution of applications. So an alternative to packaging an application in a container image could be to instead compile it to WebAssembly. Compared to running OCI containers, WebAssembly offers faster startup and lower disk footprint. This can be a great advantage for highly scalable microservice architectures, but also for serverless offerings of cloud providers. Crun and containerd already support WebAssembly, and Azure Kubernetes has preview support for WASI workloads.

An example with Go

We build a WebAssembly application and run it in three different environments. Let's say we are interested in prime number generation. The Go standard library includes a probabilistic primality check for arbitrary big integers. So checking if a number is prime is just a few lines in Go. We build a WebAssembly binary and run the same application from the command line, in a web browser, and from GraalVM.

In a previous blog article, we already saw how to compile a Go application to WebAssembly. The article highlighted the great interoperability between Go and JavaScript, but also mentioned that Go does not support WASI. However, there is an alternative Go implementation, TinyGo, which does comply with WASI.

TinyGo to the rescue

TinyGo was initially created for embedded environments. In recent times, it has also become the go-to compiler for Go WebAssembly projects. Not only does it support WASI, it also solves another well-known problem of Go: its big binary size. Even for small programs, Go application binaries total in about 3 MB, due to the embedded Go runtime. This is not so much of a problem for CLI applications or microservices running on the server, but can be a challenge for application files distributed over the web. TinyGo produces WebAssembly binaries by a factor of 3-5 smaller than the standard Go compiler. The example application below can be brought down to about 350 kB with a few reasonable compiler flags.

TinyGo should be the first consideration for Go applications targeting WebAssembly. However, it has some drawbacks. Besides the already mentioned lack of implemented WebAssembly features, it also lags behind a few months in new Go language features. Also some parts of the standard library, e.g. reflection, are not supported. This can be a problem when we have no or little control over the code we want to compile, e.g. when compiling 3rd party libraries.

Below is the Go application source code for our primality check. It is written like a normal, runnable CLI-application. We do not import the syscall/js package, in contrast to a Go Webassembly application targeting the browser. The input is a command line argument and the program outputs directly to the standard output (stdout).

1package main
3import (
4    "fmt"
5    "math/big"
6    "os"
9func main() {
10    number := new(big.Int)
11    fmt.Sscan(os.Args[1], number)
12    if number.ProbablyPrime(10) {
13        fmt.Println(os.Args[1] + " is probably prime\n")
14    } else {
15        fmt.Println(os.Args[1] + " is not prime\n")
16    }

This method, writing a runnable CLI application, is typical for WASI applications. The other option of targeting WebAssembly and WASI would be to create a library exporting multiple functions. However, as of today, these library functions can only accept WebAssembly native types, that is, integers or floats. The CLI style greatly simplifies handling strings, which is the reason we opt for it here.

With TinyGo installed on our system, we can compile the above code to WebAssembly with

1$ tinygo build -o prime.wasm -target wasi main.go

From the command line

After installing Wasmtime, a popular WebAssembly runtime, we can run our prime.wasm binary like this:

1$ wasmtime prime.wasm 359334085968622831041960188598043661065388726959079837
2359334085968622831041960188598043661065388726959079837 is probably prime

This works both in Windows and in Linux.

Running in browser

All browsers have built-in support for loading, compiling and running WebAssembly binaries. However, when targeting WASI, the WebAssembly module requires a few imports which have to be satisfied. For example, the module will need functions called args_sizes_get and args_get to retrieve the command line arguments. It is possible to provide these imports manually, but it is far easier to use a precompiled runtime, such as wasmer-js. Wasmer-js is a thin JavaScript wrapper over the wasmer runtime, which is implemented in Rust.

1import { init, WASI } from '@wasmer/wasi'
3let module = undefined;
5export async function computePrime(number) {
6    if (module === undefined) {
7        await init();
8        module = await WebAssembly.compileStreaming(fetch('prime.wasm'));
9    }
11    let wasi = new WASI({
12        env: {},
13        args: [
14            'prime.wasm', number
15        ,
16    });
17    await wasi.instantiate(module, {});
18    wasi.start();
19    return wasi.getStdoutString();

In line 8 of the above JavaScript source code, we use the browser API to download and compile the prime.wasm binary to a WebAssembly module. The rest of the code instantiates and runs this module, providing the input for the primality check.

Embedded in Java

In general, the integration between Java and WebAssembly had a rough start so far. For example, projects aiming to compile Java to WebAssembly face a few difficult challenges. One is that WebAssembly does not have garbage collection built-in. The other is that Java supports reflection, i.e. it allows to evaluate and manipulate properties of a program at runtime. This is close to impossible for compiled languages such as WebAssembly.

Calling WebAssembly from Java is also not well supported. Besides a few java libraries (e.g. Extism), there is one notable exception, which we will explore here: the Java runtime GraalVM. GraalVM is among other things known for its Polyglot API, allowing it to run other languages like Python, R, and Ruby. And also WebAssembly. Here is how we run our prime.wasm binary from Java in GraalVM:

1public static void main(String[] args) throws Exception {
2    File file = new File(Thread
3            .currentThread()
4            .getContextClassLoader()
5            .getResource("prime.wasm")
6            .getFile());
7    Source.Builder sourceBuilder = Source.newBuilder("wasm", file);
8    Source source = sourceBuilder.build();
10    Context.Builder contextBuilder = Context.newBuilder("wasm")//
11            .option("wasm.Builtins", "wasi_snapshot_preview1").//
12            arguments("wasm", new String[]{"", args[0]});
14    try (Context context = contextBuilder.build()) {
15        context.eval(source);
17        Value mainFunction = context
18                .getBindings("wasm")
19                .getMember("main")
20                .getMember("_start");
21        mainFunction.execute();
22    } 

Embedding WebAssembly in GraalVM requires a bit more effort than in JavaScript. This is mainly due to the Polyglot API, which is not WebAssembly specific, but aims to represent all embedded languages with the same programming interface. We first have to represent the prime.wasm file as a Source object (line 8). Here, it is loaded as a file, but it is also possible to work with byte streams, if we were to e.g. load the binary from the network.

Then we create a Context object for the language "wasm". We activate WASI for this context in line 11. Among lots of other things, this will inject System.out as the standard output for the WebAssembly application.

Our Go application accesses the second command line argument as the input number, because the first argument of CLI application is always reserved for the program name. Java does not have this convention, so we shift the argument array by one entry in line 12.

Line 15 will parse the code and create a runnable application, for which we then execute the main method. Given that GraalVM is installed, we can run the above with

1$ javac Prime.java
2$ java Prime 359334085968622831041960188598043661065388726959079837
3359334085968622831041960188598043661065388726959079837 is probably prime

The full Go, JavaScript, and Java source code of these examples is available here.


WebAssembly and WASI have gained a lot of attention in the last few years. In my opinion, together they have the potential to alter the way we write and deploy applications: as an example, they allow us to build applications once and run them in a wide variety of different environments, as we have seen in this blogpost.

A number of big players, for example Mozilla, Docker, Fastly, and Red Hat, have understood the potential and are investing in the WebAssembly community.

Also, an increasing number of developers are using WebAssembly and WASI for a growing set of use cases: The State of WebAssembly 2022 survey shows considerable increase in usage in serverless environments, containerization, and also plugin-development. This last topic, plugin-architectures, makes use of the security guarantees of WebAssembly to integrate untrusted third-party plugins in a secure environment. Shopify is successfully using such an architecture to allow customer adaptions to their checkout process. In an upcoming article, I will have a deeper look into this use case.

What are you doing with WebAssembly? I'd love to hear about your use cases, challenges, and ideas. Feel free to contact me!

share post




More articles in this subject area\n

Discover exciting further topics and let the codecentric world inspire you.


Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.