2023.07.10

Wasm and Rust

こんにちは、次世代システム研究室のN.M.です。

Wasm and Rust

This article explores the use of Rust and WebAssembly (Wasm) for web development, discussing Wasm’s structure, the advantages of using Rust, tools for Wasm development in Rust, debugging techniques, and applications of Wasm beyond the web, including use in server-side technologies with WASI, WAGI, and Krustlets.

Introduction to Wasm

WebAssembly, often abbreviated as Wasm, defines a portable binary code format, along with a corresponding text format, for executable programs. It also provides a set of software interfaces to facilitate interactions between these programs and their host environment.

Wasm language is almost human-readable and has an S-expression based variant that is slightly less difficult to understand.

Let’s illustrate this with a simple “hello world” example in C and its
corresponding Wasm text

A sample hello world C program
#include <stdio.h>

int main() {
  printf("hello, world!\n");
  return 0;
}
… and using the wasm-decompile cmd tool to get Wasm
human-readable text code:
wasm-decompile hello.wasm
export memory c(initial: 256, max: 256);

global g_a:int = 67760;

export table f:funcref(min: 4, max: 4);

data d_helloworld(offset: 1024) = "hello, world!";
data d_b(offset: 1040) = "\05";
data d_c(offset: 1052) = "\01";
data d_d(offset: 1076) =
"\02\00\00\00\03\00\00\00\a8\04\00\00\00\04";
data d_e(offset: 1100) = "\01";
data d_f(offset: 1116) = "\ff\ff\ff\ff\0a";

import function a_a(a:int, b:int, c:int, d:int):int;

import function a_b(a:int, b:int, c:int);

export function d() {
  nop
}

function f_d(a:int):int {
  var d:int;
  var e:int;
  var b:int;
  var g:int;
  var f:int;
I omitted the rest of this Wasm code since it was quite verbose, but this is what Wasm in text format looks like. Not very similar to the
original c source code since it has been heavily optimized.

Why Rust for Wasm?

Rust brings numerous advantages when it comes to writing Wasm code. Its memory safety features without garbage collection are especially
suited for Wasm’s low-level operations. Rust’s performance is comparable to that of C and C++, but it offers better safety guarantees, which
makes it an ideal choice for web development where security is paramount. ### Sandbox By default Wasm VMs have no access to IO, random
number generation, File System, or network.

Memory Access: Wasm programs have access to a linear memory space, which allows them to read from and write to a contiguous block of
memory. However, direct access to the host’s memory or arbitrary memory addresses is not allowed for security reasons.

These limitations can be overcome by the use of JavaScript functions for the web and explicitly granting access in the case of WASI.

Exports and Imports: Wasm modules can define functions and data that are accessible to the host environment. These are called “exports.”
Conversely, Wasm modules can also import functions and data from the host environment, such as JavaScript functions or Web APIs, to interact
with the outside world.

Basic Numeric Operations: Wasm supports basic numeric operations like arithmetic, bitwise operations, and comparisons.

Limited System Interaction: Wasm programs cannot directly access the host’s file system, make network requests, or manipulate the DOM
(Document Object Model) in a web browser environment. Instead, they rely on the host environment’s APIs and interfaces, which can be exposed via
JavaScript or other language bindings.

Wasm on the Web with
JavaScript

This is like defining language bindings between languages.

wasm_bindgen

This is Rust macro to export code to JavaScript You can use the wasm_bindgen macro to export functionality from your Rust program. For example to export a struct:
#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: FixedBitSet,
}
and to export an impl:
#[wasm_bindgen]
impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }

    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;
Then to access code from JavaScript:
import { Universe } from "wasm-game-of-life";

const universe = Universe.new();
const width = universe.width();
const height = universe.height();
As you can see we just import the objects into JavaScript and use them as might be expected. Actually passing data between JavaScript and Wasm is more involved, more about that later.

Other Useful Tools

cargo-generate

cargo-generate is a developer’s tool for instantiating a new project from a template. It’s particularly useful for Rust and Wasm
development because it allows you to quickly bootstrap a project with a pre-configured setup. This way, you can focus on writing your Wasm code
in Rust, instead of dealing with boilerplate setup code.

This tool is useful for quickly starting a project using a Rust template.

Installed with:
cargo install cargo-generate
In this case I used:
cargo generate --git https://github.com/rustwasm/wasm-pack-template
To get up and running quickly with a Rust Wasm web project

wasm-pack build tool

I built my rust wasm code with wasm-pack

wasm-pack build

wasm-pack can be used to build for Wasm web Javascript integration or for running inside Node.js. It also allows for pushing wasm files to the npm registry.

Example: RustWasm: Game of Life



This is the Rust Wasm starter tutorial. It guides us through the creation of a browser-based version of Conway’s Game of Life. Rust code implements the logic and rules of the game. It exposes methods such as tick(), which moves the game to move to the next state. The game is rendered by JavaScript in the Canvas object.

The rules are contained in the tick() method:
pub fn tick(&mut self) {
    let mut next = self.cells.clone();

    for row in 0..self.height {
        for col in 0..self.width {
            let idx = self.get_index(row, col);
            let cell = self.cells[idx];
            let live_neighbors = self.live_neighbor_count(row, col);

            next.set(idx, match (cell, live_neighbors) {
                (true, x) if x < 2 => false,
                (true, 2) | (true, 3) => true,
                (true, x) if x > 3 => false,
                (false, 3) => true,
                (otherwise, _) => otherwise
            });
        }
    }

    self.cells = next;
}
You can see here the basic rules of Life.
  • a living cell that has less then 2 neighbors dies
  • a living cell that has 2 or 3 neighbors survives
  • a living cell that has more than 3 neighbors dies from overcrowding
  • a dead cell that has 3 neighbors is spawned
This state is saved into a memory-efficient FixedBitSet data structure, only storing 1 bit of information per cell.

The code has already been shown a little above and is too long to show completely but another area of interest is the Universe’s cell
initialization. This uses the JavaScrpt Math.random() function since this is something that Wasm code is restricted from doing itself.
for i in 0..size {
            cells.set(i,
                if js_sys::Math::random() < 0.5 {
                    true
                } else {
                    false
                }
            )
        }
js_sys is a crate that gives us the ability to call JavaScript. It is an important library for accessing JavaScript standard built-in objects and code.

Debugging Rust and Wasm

Debugging is a crucial part of development. Fortunately, debugging Rust compiled to Wasm has improved significantly. Tools like wasm-pack provide built-in support for source maps, enabling a seamless debugging experience in browsers’ developer tools.

You need to build with debug symbols enabled to get meaningful stack traces. When using a debug build, this is the default behavior, if building for a release, include the following inCargo.toml for readable stack traces:
[profile.release]
debug = true
Note that creating human readable stack-traces can incur a slight penalty in terms of Wasm size and speed.

We can output to the console in developer tools, from our Rust Wasm by using the following:
extern crate web_sys;

web_sys::console::log_1(&"Hello, world!".into());
or alternatively console.error which has the same
signature but includes stack trace information.

Wasm Outside the Web: Understanding WASI

The WebAssembly System Interface, or WASI, is a crucial aspect of WebAssembly programming. Its primary purpose is to define the capabilities of a WebAssembly program. You can find more specific details about the proposals on the official GitHub page. Among the elements in Phase 2, there are features like IO streams, file system reading and writing, random number generation, and even machine learning. Phase 1, on the other hand, includes networking and threads.

However, the journey doesn’t stop there. For a proposal to be finalized, it has to reach Phase 5. This ensures that the standards are thoroughly vetted and mature enough for stable use.

Delving into WAGI

Switching gears, let’s look at the WebAssembly Gateway Interface or WAGI. This operates using the Common Gateway Interface (CGI). An interesting feature of WAGI is that it enables a WebAssembly program to operate without network access. This is a strategic move, especially considering that network access is dependent on WASI threads and networking standards. With these standards likely a year away from completion, WAGI provides a practical workaround.

The WebAssembly Gateway Interface (WAGI) allows Wasm programs to interact with the web without needing direct network access. This is
especially relevant given the current limitations of WASI regarding networking. An example use case for WAGI might be creating a small, fast, and secure microservice that processes HTTP requests and produces responses.

A Rust WAGI program looks like this:
fn main() {
    println("content-type: text/plain");
    println("");
    println("Hello World");
}
Note, the content-type and blank line output. For those of you not around in the ’90s, this is a requirement of CGIs, which only take stdin, environment variables, and arg parameters. The WAGI server sends HTTP parameters to the arg parameters, the POST body to the stdin, and the WAGI request context to the environment variables.

There is no network code or threading code, that is taken care of by the WAGI server, which is based on hyper. WAGI allows you to define routes mapped to wasm files.
[[module]]
route = "/hello"
module = "./target/wasm32-wasi/debug/hello.wasm"
Here you see a route defined for my hello world program above, that references it’s Wasm file.

Krustlets: A Brief Introduction

Krustlets, which stands for “Kubernetes Rust Kubelets”, is a project that aims to enable Kubernetes to run WebAssembly modules. This is an example of how Wasm’s portability can extend beyond the browser and into the realm of server-side applications, specifically those running in Kubernetes. Creating a Krustlet is quite similar to creating the other Rust Wasm programs. The Wasm is then pushed to a container registry. Then apply a Kubernetes yaml file with a registry URI referencing the Wasm. A sample registry push command using the wasm-to-oci tool.
wasm-to-oci push demo.wasm mycontainerregistry007.azurecr.io/krustlet-tutorial:v1.0.0
A sample Pod yaml resource referencing the Wasm in the registry:
apiVersion: v1
kind: Pod
metadata:
  name: krustlet-tutorial
spec:
  containers:
    - name: krustlet-tutorial
      image: mycontainerregistry007.azurecr.io/krustlet-tutorial:v1.0.0
  imagePullSecrets:
    - name: <acr-secret>
  tolerations:
    - key: "kubernetes.io/arch"
      operator: "Equal"
      value: "wasm32-wasi"
      effect: "NoExecute"
    - key: "kubernetes.io/arch"
      operator: "Equal"
      value: "wasm32-wasi"
      effect: "NoSchedule"
Of course, Kubernetes takes care of the actual deployment.

The Role of Wasm on the Server

Ideally suited to Server hosting environments where sandboxing is important, and for use in cases where containers can be further shrunk into a Wasm.

Looking at the evolution of server-side technologies, we’ve seen a consistent trend toward lighter, more efficient, and safer execution environments.

Virtual Machines (VMs) offered a way to isolate applications and their dependencies into self-contained units, but with significant overhead in terms of resources. The Java Virtual Machine (JVM) brought us closer to the ideal of “write once, run anywhere”, but still required a heavy run-time environment.

Containers took this a step further by providing lightweight isolation while sharing the host system’s kernel, thus reducing the overhead. Docker, Kubernetes, and similar technologies revolutionized the way we deploy and manage applications at scale.

Now, with WebAssembly, we might be seeing the next step in this evolution. Wasm offers a way to run code at near-native speed, regardless of the platform, in a compact and secure manner. It’s akin to a ‘universal binary’ that is capable of running anywhere, be it in the browser, on a server, or on IoT devices.

Conclusion

The combination of Wasm and Rust offers a powerful tool set for web development. It provides a way to write high-performance, safe and secure code that runs at near-native speed in the browser. With the continuing advancement of WASI, we can also imagine a future where Wasm modules, written in Rust, become commonplace on the server side.

Projects like WAGI and Krustlets are already showing the potential impact of this technology outside of the browser context. As web technology continues to evolve, the powerful combination of Rust and Wasm will likely find increasingly diverse applications.

 

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事