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).
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: