Compile Rust & Go to a Wasm+Wasi module and run in a Wasm runtime


In my Exploring WebAssembly outside the browser post, I talked about how WebAssembly System Interface (WASI) enables Wasm modules to run outside the browser and interact with the host in a limited set of use cases that Wasi supports (see Wasi proposals).

WASI

In this blog post, let’s look into details of how to compile code to a Wasm+Wasi module and then run it in a Wasm runtime. Notice that I use Wasm+Wasi module deliberately (instead of just Wasm) because some languages have Wasm support and can run perfectly fine in the browser but they have no or limited Wasi support to run outside the browser.

We will build a simple application in Rust and then Go that prints HelloWorld and also writes a HelloWorld text file to show that the module can indeed interact with the host using Wasi.

State of Wasm+Wasi in different languages

Before we delve into details, it’s important to point out that whether it’s possible to compile an app to Wasm+Wasi largely depends on the language. Languages like Rust, C, C++ have excellent support, whereas languages like Go, C# have emerging support, and Java has some limited/indirect support via a fork of TeaVM. This is confusing and maybe in a future blog post, I’ll do a deep dive on different languages and their Wasm+Wasi support.

Even if a language has good Wasm+Wasi support, you’re still limited with what Wasi supports (e.g. no socket support), unless you use something non-standard like WASIX on wasmer runtime (but that’s also a topic for another blog post). This means that you cannot take any code you have and expect to compile and run it as a Wasm+Wasi module.

Given all these caveats, Rust is a good language to start with, as it has excellent Wasm+Wasi support.

A HelloWorld Rust app on Wasm+Wasi

First, you need to add wasm32-wasi target. This enables Rust to compile to Wasm+Wasi modules:

rustup target add wasm32-wasi

Create a HelloWorld app:

cargo new hello-wasm
cd hello-wasm

Change main.rs to print a HelloWorld message and write a HelloWorld text file to access the filesystem:

use std::io::prelude::*;
use std::fs;

fn main() {
    println!("Hello, Wasm!");

    // Create a file
    // We are creating a `helloworld.txt` file in the `/helloworld` directory
    // This code requires the Wasi host to provide a `/helloworld` directory on the guest.
    // If the `/helloworld` directory is not available, the unwrap() will cause this program to panic.
    // For example, in Wasmtime, if you want to map the current directory to `/helloworld`,
    // invoke the runtime with the flag/argument: `--mapdir /helloworld::.`
    // This will map the `/helloworld` directory on the guest, to  the current directory (`.`) on the host
    let mut file = fs::File::create("/helloworld/helloworld.txt").unwrap();

    // Write the text to the file we created
    write!(file, "Hello world!\n").unwrap();

    println!("Created helloworld.txt");
}

To build for Wasm+Wasi, you need to specify the target:

cargo build --target wasm32-wasi

This creates a Wasm module that you can run in a Wasm runtime such as wasmtime:

wasmtime --mapdir /helloworld::. target/wasm32-wasi/debug/hello-wasm.wasm

Hello, Wasm!
Created helloworld.txt

You can try another Wasm runtime like wasmedge and it runs the same way:

wasmedge --dir /helloworld:. target/wasm32-wasi/debug/hello-wasm.wasm

Hello, Wasm!
Created helloworld.txt

Notice how we map the /helloworld folder in the Wasm module to the current directory in the host. This is how we’re enabling the Wasm module to access the filesystem on the host.

A HelloWorld Go app on Wasm+Wasi

Let’s see how we can do the same in another language like Go.

Up until recently, there was no Wasm+Wasi support in Go and you had to rely on something called tinygo to compile to Wasm+Wasi. This has changed very recently and the latest version of Go, 1.21 RC2, added Wasm+Wasi support.

First, make sure you have the latest 1.21 RC2 installed:

go install golang.org/dl/go1.21rc2@latest

Then, use the go1.21rc2 command instead of the go command until 1.21 is released officially.

Let’s create hello-wasm.go to print a HelloWorld message and write a HelloWorld text file to access the file system like the Rust example:

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    fmt.Println("Hello, Wasm!")

    // Create a file
    // We are creating a `helloworld.txt` file in the `/helloworld` directory
    // This code requires the Wasi host to provide a `/helloworld` directory on the guest.
    // If the `/helloworld` directory is not available, the `ioutil.WriteFile()` will fail.
    // For example, in Wasmtime, if you want to map the current directory to `/helloworld`,
    // invoke the runtime with the flag/argument: `--mapdir /helloworld::.`
    // This will map the `/helloworld` directory on the guest, to  the current directory (`.`) on the host
    err := ioutil.WriteFile("/helloworld/helloworld.txt", []byte("Hello world!\n"), 0644)
    if err != nil {
        panic(err)
    }

    fmt.Println("Created helloworld.txt")
}

To build for Wasm+Wasi, you need to pass in GOOS and GOARCH env variables that basically specify that this is a Wasm app on Wasi Preview 1:

GOOS=wasip1 GOARCH=wasm go1.21rc2 build -o hello-wasm.wasm hello-wasm.go

Just like before, run in a Wasm runtime such as wasmtime:

wasmtime --mapdir /helloworld::. hello-wasm.wasm

Hello, Wasm!
Created helloworld.txt

An HTTP server in Go on Wasm+Wasi?

At this point, you might be tempted to think that you can compile any code to Wasm+Wasi and run it in a Wasm runtime. But remember, Wasi is limited and it doesn’t support sockets, for example. What happens when we try to run an HTTP server in Go on Wasm+Wasi? Let’s find out.

Create a simple HelloWorld HTTP server in hello-http.go:

package main

import (
  "fmt"
  "log"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  log.Println("Request received:", r.Method, r.URL.Path)
  fmt.Fprint(w, "Hello World!")
}

func main() {
  http.HandleFunc("/", handler)
  log.Println("Server started. Listening on :8080...")

  if err := http.ListenAndServe(":8080", nil); err != nil {
    log.Fatalf("ListenAndServe error:%s ", err.Error())
  }
}

Build for Wasm+Wasi:

GOOS=wasip1 GOARCH=wasm go1.21rc2 build -o hello-http.wasm hello-http.go

It does compile to a Wasm module but when you run it with wasmtime:

wasmtime run hello-http.wasm

You get an error because sockets are not supported in Wasi yet:

2023/06/23 10:41:06 Server started. Listening on :8080...
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
net.(*fakeNetFD).accept(...)
        /Users/atamel/sdk/go1.21rc2/src/net/net_fake.go:231
net.(*netFD).accept(0x144e2c0)
        /Users/atamel/sdk/go1.21rc2/src/net/fd_wasip1.go:88 +0x44
net.(*TCPListener).accept(0x142a0e0)
        /Users/atamel/sdk/go1.21rc2/src/net/tcpsock_posix.go:152 +0x4
net.(*TCPListener).Accept(0x142a0e0)
        /Users/atamel/sdk/go1.21rc2/src/net/tcpsock.go:315 +0x8
net/http.(*Server).Serve(0x1474000, {0xc2138, 0x142a0e0})
        /Users/atamel/sdk/go1.21rc2/src/net/http/server.go:3056 +0x30
net/http.(*Server).ListenAndServe(0x1474000)
        /Users/atamel/sdk/go1.21rc2/src/net/http/server.go:2985 +0x10
net/http.ListenAndServe(...)
        /Users/atamel/sdk/go1.21rc2/src/net/http/server.go:3239
main.main()
        /Users/atamel/dev/github/meteatamel/wasm-basics/samples/go-wasm/hello-http.go:18 +0xe

This wraps up our discussion of compiling code to a Wasm+Wasi module. It’s important to consider language-specific limitations and the level of Wasi support provided by each language. You also need to remember that certain functionalities, such as socket support, are not yet available in Wasi.

Feel free to check out my previous blog post and GitHub repo to learn more and reach out to me on Twitter @meteatamel for questions:


See also