Access operating system resources from WebAssembly

• 6 minutes to read

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 the SSVM. 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 ssvmup 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 SSVM in Node.js.

$ ssvmup 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 SSVM. 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 SSVM instance from JavaScript. You just need to construct a JavaScript object. By default, the ssvmup 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 ssvm = require('ssvm');
vm = new ssvm.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]
... ...
ssvm-wasi-helper = "=0.1.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 ssvm_wasi_helper::ssvm_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.

$ ssvmup 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 SSVM. That is done through the preopens option. By default, ssvmup 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 ssvm = require('ssvm');
vm = new ssvm.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]
... ...
ssvm-wasi-helper = "=0.1.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 ssvm_wasi_helper::ssvm_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 SSVM\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.

$ ssvmup build
$ cd node
$ node app.js
... ...
Hello WASI SSVM
This is in the `pkg` folder

What's next

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.

RustWebAssemblyhow-torust-function-in-nodejs
Fast, safe, portable and serverless Rust functions as services