This guide will show you how to run Rust with POSIX emulation in de browser with emscripten.
To compile using emscripten we need to install the rustup target
rustup target add wasm32-unknown-emscripten
and the emscripten toolchain
git clone https://github.com/emscripten-core/emsdk.git
./emsdk/emsdk install latest
./emsdk/emsdk activate latest
. ./emsdk/emsdk_env.sh
Let's create our own demo
component, we need a crate for it
cargo init --bin --name demo
Note that this is a bin
crate (and not a lib
). This means that our module could have a fn main()
that starts executing when the module loads. As we don't need that we are going to add to src/main.rs
#![no_main]
We can now implement our add
function
#[no_mangle]
pub fn add(a: u32, b: u32) -> u32 {
a + b
}
Before compiling the module, we need to add some linker flags to make emscripten generate code modern ES6 code and to also include the cwrap
utility. We do that in the .cargo/config.toml
file
[target.wasm32-unknown-emscripten]
rustflags = [
"-Clink-args=-sEXPORT_ES6",
"-Clink-args=-sENVIRONMENT=web",
"-Clink-args=-sEXPORTED_RUNTIME_METHODS=cwrap",
]
Compiling the WebAssembly module is straight forward, simply run
cargo build --release --target=wasm32-unknown-emscripten
We can now make a copy of the files for easier access
mkdir -p wasm
cp ./target/wasm32-unknown-emscripten/release/demo.{js,wasm} ./wasm/
To run our code we create a main.js
file. Running an emscripten module in JavaScript is straight forward
- import the generated module
import Demo from "./wasm/demo.js"
- This exports an async factory function
const demo = await Demo();
In order to call the add
function we need to ask empscripten to wrap it, for that we use the cwrap
method that we mentioned earlier, which needs the function signature
const add = demo.cwrap("add", 'number', ['number', 'number']);
const sum = add(21, 21);
console.log(`21 + 21 = ${sum}`);
We can now run the script with
deno -A main.js
which prints
21 + 21 = 42
Lets now add a function to show the content of a directory
#[no_mangle]
pub fn list_dir(path: *const i8) {
let path = unsafe { std::ffi::CStr::from_ptr(path) }.to_str().unwrap();
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 std::fs::read_dir(&path).is_ok() {
println!(" {}/", path.display());
} else {
println!(" {}", path.display());
}
}
}
}
Emscripten targets C and C++, as such we should expose a C ABI in our exported function to take full advantage of its tooling.
We also edit .cargo/config.toml
so that we export the emscripte's FS
module
"-Clink-args=-sEXPORTED_RUNTIME_METHODS=cwrap,FS",
We now recompile, and call it from JavaScript
const list_dir = demo.cwrap("list_dir", null, ['string']); // This assumes C style strings
console.log("listing /");
list_dir("/");
With the exported FS
module we can interact with emscripten's file system from JavaScript
console.log("listing /home/web_user");
list_dir("/home/web_user");
console.log("writing /home/web_user/foo.txt");
demo.FS.writeFile("/home/web_user/foo", "bar");
console.log("listing /home/web_user");
list_dir("/home/web_user");
We can now run it with Deno, and we get
listing /
/tmp/
/home/
/dev/
/proc/
listing /home/web_user
< Empty >
writing /home/web_user/foo.txt
listing /home/web_user
/home/web_user/foo
Being a C/C++ toolchain, emscripten provides lots of features for those languages. Third party crates bring some of those featues to Rust, let's add one of those crates
cargo add emscripten-functions
We can now write a print function that shells out to JavaScript
fn print(s: &str) {
emscripten_functions::emscripten::run_script(format!("console.log({s:?})"));
}
And we can use it from out greet
function
#[no_mangle]
pub fn greet(name: *const i8) {
let name = unsafe { std::ffi::CStr::from_ptr(name) }.to_str().unwrap();
let s = format!("hello {name}, from Rust!");
print(&s);
}
After compiling, we can call greet
in main.js
const greet = demo.cwrap("greet", null, ['string']);
greet("Jorge");
We can now run it with Deno, and we get
hello Jorge, from Rust!
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 the emscripten website, in particular the documentation about interacting with code.