DEV Community

Jeremy Likness ⚡️
Jeremy Likness ⚡️

Posted on • Originally published at blog.jeremylikness.com on

Explore WebAssembly System Interface (WASI for Wasm) From Your Browser

WebAssembly is a powerful virtual machine implemented by all the popular modern browsers. It enables significant performance benefits over pure JavaScript and empowers developers to build web experiences using the language of their choice. Projects like Blazor that take advantage of WebAssembly are experiencing a tremendous surge of interest and popularity. Although WebAssembly has created a new frontier on the web, it has recently broken free from the constraints of the browser to run almost anywhere.

WebAssembly System Interface, or WASI for short, is a new standard for running WebAssembly (or “Wasm” for short) outside the web. To better understand the what and why I recommend reading the Standardizing WASI blog post. If Wasm represents a conceptual machine, WASI represents a conceptual operating system. I find many technology topics are easier done than said, so let's go hands on to explore what this means. The only prerequisite is your browser!

An Online Studio

The first step is to generate the byte code WebAssembly runs on, called Wasm. The easiest way to do this is to navigate to WebAssembly.studio, where you can create, build, and run projects online. Let's do a simple exercise. Create an empty “Wat” project.

Empty Wat

“Wat” is short for “WebAssembly text” and is a visual/textual way of representing the Wasm byte code. Navigate to main.html and note that it is straightforward HTML with some JavaScript. Open main.js:

fetch('../out/main.wasm').then(response =>
   response.arrayBuffer()
).then(bytes => WebAssembly.instantiate(bytes)).then(results => {
   instance = results.instance;
   document.getElementById("container").textContent = instance.exports.add(1,1);
}).catch(console.error);

This code fetches the byte code and passes it to the WebAssembly API to construct a new instance. The instance contains a property called exports that exposes a method named add. Let's see where those come from by opening main.wat:

(module
   (func $add (param $lhs i32) (param $rhs i32) (result i32)
      get_local $lhs
      get_local $rhs
      i32.add)
   (export "add" (func $add))
)

This is the low-level code WebAssembly compiles to. By itself, WebAssembly is a “black box” that cannot interact with anything external. The only way for it to interface with the DOM is to either expose exports that are called from JavaScript, or imports that can be called from WebAssembly. This is a model of “least privilege” security. Here, the function $add is defined as taking two 32-bit integer parameters and returning a 32-bit integer result. The get_local instructions place values on the stack, and i32.add simply pops those values off, adds them, and pushes the result to the stack. The function is exported as add.

Build the project and notice that a new file is generated called main.wasm. This is the byte code. If you click on it, you'll see the textual representation that looks similar to the source.

(module
   (type $t0 (func (param i32 i32) (result i32)))
   (func $add (export "add") (type $t0) (param $lhs i32) (param $rhs i32) (result i32)
      get_local $lhs
      get_local $rhs
      i32.add))

Go ahead an run the project. OK, great. So now what? Let's try one more example, this time compiling from a higher-level language. Open a new tab or refresh the current page. Create a new project, this time choosing AssemblyScript. Based on TypeScript, AssemblyScript provides a set of type definitions to enable compiling from TypeScript to Wasm. Open main.ts:

declare function sayHello(): void;
sayHello();
export function add(x: i32, y: i32): i32 {
   return x + y;
}

A few things are going on. The declare indicates a method named sayHello is going to be imported. It is immediately called. A function named add is also exported and does essentially the same thing as the previous example. When you build this project, the main.wasm is a little larger:

(module
   (type $t0 (func))
   (type $t1 (func (param i32 i32) (result i32)))
   (import "main" "sayHello" (func $main.sayHello (type $t0)))
   (func $add (export "add") (type $t1) (param $p0 i32) (param $p1 i32) (result i32)
      get_local $p0
      get_local $p1
      i32.add)
   (func $f2 (type $t0)
      call $main.sayHello)
   (memory $memory (export "memory") 0)
   (start 2))

There are now two “types” for the two calls. One is imported as a function named $main.sayHello and the other is defined as $add and exported as add. An anonymous function $f2 is created to call the “hello” method and the start instruction ensures this will be called. Open main.js to see how the sayHello import is passed to the WebAssembly API with JSON configuration.

WebAssembly.instantiateStreaming(fetch("../out/main.wasm"), {
   main: {
      sayHello() {
         console.log("Hello from WebAssembly!");
      }
   },
   env: {
      abort(_msg, _file, line, column) {
         console.error("abort called at main.ts:" + line + ":" + column);
      }
   },
}).then(result => {
   const exports = result.instance.exports;
   document.getElementById("container").textContent = "Result: " + exports.add(19, 23);
}).catch(console.error);

Build and run the project see the console “hello” message and the “add” result. This example illustrates:

  • WebAssembly cannot directly interact with the DOM, but can call methods that are explicitly imported
  • WebAssembly can run code when instantiated, but must explicitly export functions to be called externally
  • WebAssembly is a suitable compile target for high level languages like C, C#, Go, Rust and even TypeScript

WASI and Wasm

Let's expand our example a bit. First, a general note:

All WASI is Wasm, but not all Wasm is WASI

In other words, a WASI module has byte code with imports and exports like any other WebAssembly modules. There are two things that distinguish WASI modules from “ordinary” Wasm:

  1. WASI modules may import one or many pre-defined WASI interfaces
  2. Although the Wasm start instruction is valid, WASI modules export by convention a function as _start to be called by the host runtime

Whoa. Let's back up a second. Did I say, “runtime?” I did. Remember I mentioned earlier that Wasm is a conceptual machine, and WASI describes a conceptual OS? The same way browsers implement the Wasm “machine”, there must be a host process the provides the “hooks” for the WASI interface. In other words, when a WASI module calls __wasi_fd_write() there needs to be a host to pass that imported function so it is able to actually do something.

There are several runtimes available, including Wasmer and Wasmtime. They can be hosted on various platforms and even integrate with other languages and runtimes (for example, it is entirely possible to call a WASI module from inside a Go or C# application). The runtimes solve some interesting problems such as security and filesystem access. WASI cannot access the host machine (a good thing) but some WASI modules read and write files. This is possible because they interact with a file descriptor that is passed in from the host process. This means they can only access files or folders on an opt-in basis. If you don't allow the access, the module simply cannot perform the operation.

To keep things simple, however, I'm going to keep you in your browser and introduce you to a browser-based runtime called the WebAssembly shell (https://webassembly.sh).

The WebAssembly Shell

The WebAssembly Shell is an implementation of Wasmer in your browser. It provides support for managing modules via the WebAssembly Package Manager (WAPM). You can even install it as a Progressive Web App (PWA) and run it offline. Let's start with a simple example. Open the shell and install the qr2text module:

wapm install qr2text

Now you can run the installed module like this:

qr2text https://blog.jeremylikness.com

Running qr2text

The module itself is written in Rust that is compiled to WebAssembly with WASI. If you're curious, the source code is available in this repository:

wapm-packages/qr2text

This module will run without modification on any platform that has a WASI-capable host, including Linux, Windows, MacOS, and even ARM-based devices. There are some other cool packages you can play with like cowsay (quote text from an ASCII cow) and rustpython that provides an interactive Python interpreter (written in Rust, compiled to Wasm). If you're like me, however, you're ready to build your own module. There are different tool chains available based on the language you use, so I'll stick with pure WebAssembly for this example.

Your First WASI Module

Go back to the WebAssembly Studio and create a new, empty Wat project. Don't build it yet. Instead, open main.wat and delete everything then overwrite it with the following code:

(module $hello
   (import "wasi_unstable" "fd_write"
   (func $fd_write (param i32 i32 i32 i32) (result i32)))
   (memory 1)
   (export "memory" (memory 0))
   (data (i32.const 8) "My first WASI module!\n")
   (func $main (export "_start")
   (i32.store (i32.const 0) (i32.const 8))
   (i32.store (i32.const 4) (i32.const 22))
   (call $fd_write
      (i32.const 1)
      (i32.const 0)
      (i32.const 1)
      (i32.const 30)
   )
   drop))

(The code is based on this “Hello, World” project).

Click on the Save button in the upper right and build it. The generated code is simple. At this stage, the code builds fine even though you didn't include any WASI package. This is because the imports are always provided by the host, so no compile-time checking is necessary. Let's look at the code:

  • A WASI module is imported called fd_write that takes four (4) 32-bit integers and returns an integer
  • Some memory is defined and populated with a string constant. It is offset by 8 bytes so there is room for two 32-bit (4-byte) integers before it
  • The memory is exported (so it can be read by external processes)
  • The 32-bit (4-byte) offset of the string constant (8) is stored in memory at location 0
  • The 32-bit length of the string constant (22) is stored in memory at location 4, or immediately after the previous 4 bytes
  • The fd_write function is called with four parameters:
    • Where to write (1 is for stdout or standard output)
    • The location in memory with the offset of the string constant (0) and its length
    • The number of strings to expect
    • The location in memory to store the number of bytes written (we ignore this)
  • drop does nothing but unlike nop it clears unneeded values from the stack (see here for clarification)

Conceptually, the memory looks like this:

  • 0..3 = 8
  • 4..7 = 22
  • 8..29 = text
  • 30 = a value we ignore

Fortunately, you don't have to deal with manually counting the length of a string or computing memory offsets when you use higher level languages like Rust. If you try to run this example from the studio, you'll get an error. This is because the import isn't implemented. No problem! Click the “Download” button to download a package. Unzip the package, navigate to the out directory, and you'll find the compiled main.wasm file.

Next, go back to your WebAssembly Shell and type wapm upload. This will open a file dialog. Navigate to the Wasm file you just downloaded and select it. After it is uploaded, type main and hit enter.

First WASI

I don't know about you, but seeing it working made me dance.

Jeremy Dance

Summary

Now you have created your first WASI module without having to leave the comfort of your web browser. Of course, it is possible to install the runtimes locally on your machine and work from there. I have it in my Ubuntu instance running via Windows Subsystem for Linux (WSL) on my Windows 10 machine. If you're interested in digging deeper, any of the links in this blog post should provide a solid point to start from. As always, I welcome your feedback and if you end up building your own WASI module, feel free to post it here!

Happy coding,

Jeremy Likness

Top comments (1)

Collapse
 
jwp profile image
John Peters

Just dialed in to .Net 6 and VS2022. My first task was to explore Blazor. All I can say is Wow, simply amazing. So much flexibility, including desktop apps. Please send best regards to the team.