The WebAssembly VM provides a sandbox to ensure application safety. However, this sandbox is also a very limited “computer” that has no concept of file system, network, or even a clock or timer. That is very limiting for the Rust programs running inside WebAssembly.
If WASM+WASI existed in 2008, we wouldn't have needed to created Docker. That's how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let's hope WASI is up to the task! – Solomon Hykes, Co-founder of Docker
The WebAssembly Systems Interface (WASI) is a standard extension for WebAssembly bytecode applications to make operating system calls. It is fully supported in WasmEdge. WASI defines a set of function names to perform operating system tasks, such as opening a file. When the WebAssembly VM encounters those function names at runtime, it automatically calls the corresponding operating system standard library function to perform the task and return the result.
In order for WASI to work, we need a compiler toolchain to compile Rust (or other languages) standard library functions, such as opening a file, into bytecode that makes the corresponding WASI calls. The rustwasmc tool uses the wasm32-wasi
compiler backend for Rust. It supports WASI out of the box.
The example source code for this tutorial is here.
In the getting started with Rust functions in Node.js, we showed you how to compile high performance Rust functions into WebAssembly, and call them from Node.js applications.
Prerequisites
Check out the complete setup instructions for Rust functions in Node.js.
Get random number
The WebAssembly VM is a pure software construct. It does not have a hardware entropy source for random numbers. That's why WASI defines a function for WebAssembly programs to call its host operating system to get a random seed. As a Rust developer, all you need is to use the popular (de facto standard) rand
and/or getrandom
crates. With the wasm32-wasi
compiler backend, these crates generate the correct WASI calls in the WebAssembly bytecode. The Cargo.toml
dependencies are as follows.
[dependencies]
rand = "0.7.3"
getrandom = "0.1.14"
wasm-bindgen = "=0.2.61"
The Rust code to get random number from WebAssembly is this.
use wasm_bindgen::prelude::*;
use rand::prelude::*;
#[wasm_bindgen]
pub fn get_random_i32() -> i32 {
let x: i32 = random();
return x;
}
#[wasm_bindgen]
pub fn get_random_bytes() -> Vec<u8> {
let mut vec: Vec<u8> = vec![0; 128];
getrandom::getrandom(&mut vec).unwrap();
return vec;
}
The Javascript code to call the Rust functions from Node.js is as follows.
const { get_random_i32, get_random_bytes } = require('../pkg/wasi_example_lib.js');
console.log( "My random number is: ", get_random_i32() );
console.log( "My random bytes are");
console.hex( get_random_bytes() );
Now, let's run this example in WasmEdge in Node.js.
$ rustwasmc build
$ cd node
$ node app.js
My random number is: 1860036436
My random bytes are
000000 dc 19 e5 85 c9 5f 13 cf 41 d1 8e 2a 0a b2 53 d4 Ü.å.É_.ÏAÑ.*.²SÔ
000010 b3 2a 4b ee d2 cf ae 50 07 c4 c5 9b c5 ff e2 e0 ³*KîÒÏ®P.ÄÅ.Åÿâà
000020 72 b5 c2 3f f7 a6 a3 8c 38 3d e1 9d 6f ed 6a 1e rµÂ?÷¦£.8=á.oíj.
000030 05 64 90 36 82 5d cb 71 19 c3 c7 bf 65 b4 2c ce .d.6.]Ëq.ÃÇ¿e´,Î
000040 dc ca a4 89 64 81 26 d3 68 59 c1 1a d8 20 f8 a3 Üʤ.d.&ÓhYÁ.Ø ø£
000050 7b 7d a2 f4 36 76 0d 8a 07 fd a8 c3 87 0d 4b 16 {}¢ô6v...ý¨Ã..K.
000060 11 b5 8f 4b 80 0c 55 c7 d7 5b b6 e6 ad 30 de 48 .µ.K..UÇ×[¶æ0ÞH
000070 86 1a 6d 7c e6 ad 3a b0 bb 14 88 6e 93 e6 80 31 ..m|æ:°»..n.æ.1
... ...
Printing and debugging from Rust
The Rust println!
marco just works in WASI. The statements print to the STDOUT
of the process that runs the WasmEdge. In Node.js apps, it is the STDOUT
on the Node.js server.
Arguments and environment variables
It is possible to pass arguments and enviornment variables to a WasmEdge instance from JavaScript. You just need to construct a JavaScript object. By default, the rustwasmc generates code to pass in process.argv
and process.env
from the current Node.js environment. Notice there is no need for wasm-bindgen
here. Just pass regular JavaScript objects and strings. We will cover the preopens
later in this article. Ignore them for now.
const path = require('path').join(__dirname, 'wasi_example_lib_bg.wasm');
const wasmedge = require('wasmedge-core');
vm = new wasmedge.VM(path, { args:process.argv, env:process.env, preopens:{'/': __dirname} });
In the Rust program, you can access the argv
and env
using the std::env
API. But first, you need to add a helper crate in Cargo.toml
so that the WASI initialization code can be applied to your exported public library functions.
[dependencies]
... ...
wasmedge-wasi-helper = "=0.2.0"
In the Rust function, we need to call _initialize()
before we access any arguments and enviornment variables.
use wasm_bindgen::prelude::*;
use std::env;
use wasmedge_wasi_helper::wasmedge_wasi_helper::_initialize;
#[wasm_bindgen]
pub fn print_env() -> i32 {
_initialize();
println!("The env vars are as follows.");
for (key, value) in env::vars() {
println!("{}: {}", key, value);
}
println!("The args are as follows.");
for argument in env::args() {
println!("{}", argument);
}
match env::var("PATH") {
Ok(path) => println!("PATH: {}", path),
Err(e) => println!("Couldn't read PATH ({})", e),
};
return 0;
}
The Javascript code to call the Rust functions from Node.js is as follows.
const { print_env } = require('../pkg/wasi_example_lib.js');
print_env();
Build and run the application would print out Node.js environment variables and runtime arguments.
$ rustwasmc build
$ cd node
$ node app.js
... ...
The env vars are as follows.
SELENIUM_JAR_PATH: /usr/share/java/selenium-server-standalone.jar
CONDA: /usr/share/miniconda
GITHUB_WORKSPACE: /home/runner/work/wasm-learning/wasm-learning
JAVA_HOME_11_X64: /usr/lib/jvm/adoptopenjdk-11-hotspot-amd64
GITHUB_ACTION: run6
... ...
The args are as follows.
print_env
/opt/hostedtoolcache/node/14.6.0/x64/bin/node
/home/runner/work/wasm-learning/wasm-learning/nodejs/wasi/node/app.js
Reading and writing files
WASI allows your Rust functions to access the host computer's file system through the standard Rust std::fs
API. A key idea in WASI is “capability-based security” meaning that access to system resources must be explicitly declared. Like argv
and env
access, file system access must be explicitly enabled in the JavaScript option object passed into WasmEdge. That is done through the preopens
option. By default, rustwasmc
generates JavaScript code to map the current directory of the wasm file to the /
directory of the Rust std::fs
file system. You can pass in multiple preopens
mappings here.
const path = require('path').join(__dirname, 'wasi_example_lib_bg.wasm');
const wasmedge = require('wasmedge-core');
vm = new wasmedge.VM(path, { args:process.argv, env:process.env, preopens:{'/': __dirname} });
In the Rust program, add a helper crate in Cargo.toml
so that the WASI initialization code can be applied to your exported public library functions.
[dependencies]
... ...
wasmedge-wasi-helper = "=0.2.0"
In the Rust program, you can now open, write, read, and delete files after calling _initialize()
to prepare for system resources. All paths are relative to the mapped preopens
path.
use wasm_bindgen::prelude::*;
use std::fs;
use std::fs::File;
use std::io::{Write, Read};
use wasmedge_wasi_helper::wasmedge_wasi_helper::_initialize;
#[wasm_bindgen]
pub fn create_file(path: &str, content: &str) -> String {
_initialize();
let mut output = File::create(path).unwrap();
output.write_all(content.as_bytes()).unwrap();
path.to_string()
}
#[wasm_bindgen]
pub fn read_file(path: &str) -> String {
_initialize();
let mut f = File::open(path).unwrap();
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => s,
Err(e) => e.to_string(),
}
}
#[wasm_bindgen]
pub fn del_file(path: &str) -> String {
_initialize();
fs::remove_file(path).expect("Unable to delete");
path.to_string()
}
The Javascript code to call the Rust functions from Node.js is as follows.
const { create_file, read_file, del_file } = require('../pkg/wasi_example_lib.js');
create_file("/hello.txt", "Hello WASI WasmEdge\nThis is in the `pkg` folder\n");
console.log( read_file("/hello.txt") );
del_file("/hello.txt");
Build and run the application would create, write, read, print, and then delete the file in the wasm file's directory.
$ rustwasmc build
$ cd node
$ node app.js
... ...
Hello WASI WasmEdge
This is in the `pkg` folder
Standalone WebAssembly application
With WASI, you can also write a standalone WebAssembly application. That is, a Rust program that has its own main()
function, and can be started from the operating system command line, as opposed to a library in a JavaScript application. Please see this project for an example. The Rust code is as follows.
use rand::prelude::*;
use std::fs;
use std::fs::File;
use std::io::{Write, Read};
use std::env;
fn main() {
println!("Random number: {}", get_random_i32());
println!("Random bytes: {:?}", get_random_bytes());
println!("{}", echo("This is from a main function"));
print_env();
create_file("tmp.txt", "This is in a file");
println!("File content is {}", read_file("tmp.txt"));
del_file("tmp.txt");
}
... ...
You can build the program with rustwasmc
and use the WasmEdge CLI to execute the result wasm
. It will start the WasmEdge runtime, execute the compiled WebAssembly application, and then quit.
$ rustwasmc build
...
# Run it on CLI
$ wasmedge --dir .:. pkg/wasi_example_main.wasm arg1 arg2
...
Summary
In this article, we covered how to use access system resources via WASI in Rust library functions. Your Rust functions in Node.js can now access many Rust crates that require access to random numbers, file system, and host environment variables. Your high performance web applications have the best of both worlds: the Node.js ecosystem and the Rust ecosystem.