This guide will show you how to run Rust with POSIX emulation in de browser with WASI preview 2.
Right now rustup
doesn't provide the std
crate for wasm32-wasip2
in the stable channel, but it does provide it in the beta channel, so we need to install beta rust and the wasm32-wasip2
target.
rustup install beta
rustup +beta target add wasm32-wasip2
Let's create our own demo
component, we need a crate for it
cargo init --lib --name demo
We need to specify the crate type by adding to ./Cargo.toml
[lib]
crate-type = ["cdylib"]
Then we need to define its interface in ./wit/demo.wit
package local:demo;
world demo {
export greet: func(name: string);
export list-dir: func(name: string);
}
To consume this interface from our Rust code, we will use the wit-bindgen
crate
cargo add wit-bindgen
This crate provides a generate!
macro to generate the Rust traits from the wit
file
mod bindings {
wit_bindgen::generate!({
world: "demo",
});
}
This generates a Guest
trait for implementing the behaviour of the demo
world
use bindings::Guest;
Now we just need to create a new type to represent our component, and let wit-bindgen
know about it
struct Component;
bindings::export!(Component with_types_in bindings);
We can use an empty struct since we don't need to preserve any state.
Finally we need to implement the Guest
trait for our component
impl Guest for Component {
fn greet(name: String) {
let s = format!("hello {name}, from Rust!");
println!("{s}");
}
fn list_dir(path: String) {
let Ok(dir) = std::fs::read_dir(&path) else {
println!(" < Error opening directory {path:?} >");
return;
};
let dir: Vec<_> = dir.collect();
if dir.is_empty() {
println!(" < Empty >");
return;
}
for entry in dir {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_dir() {
println!(" {}/", path.display());
} else {
println!(" {}", path.display());
}
}
}
}
}
Note how we are using println!
and std::fs
, which rely on the conceps of a standard output and a filesystem respectively.
There are a two steps to compile the component:
- Compile the crate to a core wasm module
cargo +beta build --release --target wasm32-wasip2
- Generate JS bindings for the component
jco transpile ./target/wasm32-wasip2/release/demo.wasm --out-dir ./wasm/ --instantiation
To run our code we create a main.js
file and import the instantiate
function from the generated bindings
import { instantiate } from "./wasm/demo.js";
The instantiate function will require imports implementing for the WASI preview 2 world.
We can obtain a shim for the browser importing it from the jco
repo
import wasip2 from "https://cdn.jsdelivr.net/gh/bytecodealliance/jco@a72d4b38/packages/preview2-shim/lib/browser/wasip2.js";
We can now call the instantiate
function, which takes a function to fetch and compile the WebAssembly binary, an object with the required imports, and returns an object for our component
let demo = await instantiate(
(url) => fetch(new URL(`./wasm/${url}`, import.meta.url)).then(WebAssembly.compileStreaming),
{ ...wasip2 }
);
And we can now call the greet
function
demo.greet("Jorge");
We can run this with Deno
deno -A main.js
which prints
hello Jorge, from Rust!
Note how we didn't specify an import for printing to the console, our Rust code simple used println!
, and the WASI shim translated that to a console.log
call.
We also implemented a list-dir
function that interacts with the filesystem.
The WASI shim provides an emulated filesystem for our program to interact with.
Lets try it out
console.log("listing /");
demo.listDir("/");
which prints
listing /
< Empty >
The root of the emulated filesystem is empty. Lets populate it.
console.log("creating /home/web_user/");
const [[root]] = wasip2["wasi:filesystem/preopens"].getDirectories();
root.createDirectoryAt("/home/web_user/");
console.log("writing /foo.bar");
const fd = root.openAt({}, "/foo.bar", { create: true }, { write: true });
fd.write("hello world!", 0);
Now we can try calling list-dir
again
console.log("listing /");
demo.listDir("/");
console.log("listing /home");
demo.listDir("/home");
which prints
creating /home/web_user/
writing /foo.bar
listing /
/foo.bar
/home/
listing /home
/home/web_user/
To run the script in the browser we need an index.html
file
<!doctype html>
<script type="module" src="./main.js"></script>
and now we can run serve
and direct our browser to http://localhost:8000.
Check out jco
's WASI preview2 shim repo.
Also check out the full WIT definition for wasi:cli
in its repo