radu's blog

A beginner's guide to adding a new WASI syscall in Wasmtime

· Radu Matei

WebAssembly (WASM) is a binary instruction format for a stack-based virtual machine. In familiar terms, WASM is used as a compilation target for various programming languages (C, C++, or Rust, for example), generating a compact binary with a known format. Despite the name, WebAssembly is not a technology that can be used only on the web. While most initial use cases for it came from browsers, the core specification does not assume a browser runtime, but rather describes the binary and text formats for modules, types, values, and instruction types. And as a result, there are multiple runtimes implemented for various scenarios, both in and outside of browser environments.

WASI, the WebAssembly System Interface, is a capability-oriented set of APIs designed to standardize the sandboxed execution of WebAssembly modules outside of browsers. Specifically, WASI aims to be the common layer that WebAssembly modules can use to interface with host runtimes, and get granular access to OS specific objects such as files, environment variables, or networking sockets. Because of the apparent resemblance to OS system calls, the API functions exposed by WASI are often referred to as syscalls, term used by this article as well. For an introduction to the goals and architecture of WASI, head over to the Mozilla blog and read Lin Clark’s announcement post.

In this article we will explore how to add such a function interface to WASI, and how to implement it in Wasmtime, one WebAssembly runtimes that implements the WASI specification. This article is not intended to be a tutorial on using WASI as a compilation target for your applications (see the official tutorial), but document my experience in learning how to add such a system call to WASI and a very simple implementation to Wasmtime. The article is based on the talk Josh Triplett gave in December at the WebAssembly San Francisco meetup, and updated to the significant API changes that happened to the WASI and Wasmtime projects since December, with the very prompt help of Jakub Konka.

To get started, first clone the Wasmtime repository:

$ git clone --recursive https://github.com/bytecodealliance/wasmtime

If you want to avoid any changes made to the Wasmtime repository since writing this article, you can checkout this revision of the repository.

How is the WASI API defined?

The WASI API is declared using witx, an experimental file format based on the WebAssembly Text Format, with added support for module types and annotations. WASI also uses a three-phase process for making changes to the API, with ephemeral, snapshot, and old phases that define the stability for a given API. This document describes how the phases are used in which development cycle.

If you cloned the Wasmtime repository recursively, you should have the WASI repository pulled as a submodule in crates/wasi-common/WASI.

As mentioned, the .witx file format should be familiar if you have worked with .wat files - for example, this is how the args_sizes_get function is declared in the snapshot phase:

;;; Return command-line argument data sizes.
(@interface func (export "args_sizes_get")
  (result $error $errno)
  ;;; The number of arguments.
  (result $argc $size)
  ;;; The size of the argument string data.
  (result $argv_buf_size $size)
)

We note that this is exports a function called args_sizes_get, which does not accept any parameters, and which returns any error and its error code, together with the number of arguments, and the size of the argument string data.

So let’s declare a new WASI API function. We add it to the snapshot phase - and the right .witx file can be found in:

crates/wasi-common/WASI/phases/snapshot/witx/wasi_snapshot_preview1.witx.

We add a very simple function, print_greeting, which does not take any arguments, and returns potential errors:

diff --git a/phases/snapshot/witx/wasi_snapshot_preview1.witx b/phases/snapshot/witx/wasi_snapshot_preview1.witx
index 98cd947..9c74dae 100644
--- a/phases/snapshot/witx/wasi_snapshot_preview1.witx
+++ b/phases/snapshot/witx/wasi_snapshot_preview1.witx
@@ -529,4 +529,9 @@
     (param $how $sdflags)
     (result $error $errno)
   )
+
+  ;;; Print a greeting message.
+  (@interface func (export "print_greeting")
+    (result $error $errno)
+  )
 )

Tip: you can get basic syntax highlighting for .witx files using this VS Code extension for WebAssembly and set the file type as WebAssembly Text Format.

By declaring our print_greeting API we achieved half of our goal. Now we must also provide an actual implementation for it.

How does Wasmtime implement the WASI API?

At this point, if you try to build the main Wasmtime repository with the updated .witx file, you will see the following error:

error[E0046]: not all trait items implemented, missing: `print_greeting`
  --> crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs:13:1
   |
13 |   impl<'a> WasiSnapshotPreview1 for WasiCtx {
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `print_greeting` in implementation
   |
  ::: crates/wasi-common/src/wasi.rs:6:1
   |
6  | / wiggle::from_witx!({
7  | |     witx: ["WASI/phases/snapshot/witx/wasi_snapshot_preview1.witx"],
8  | |     ctx: WasiCtx,
9  | | });
   | |___- `print_greeting` from trait

This means there is a component that automatically generates Rust traits based on the snapshot .witx files - the wiggle crate, used by wasi-common to generate the WasiSnapshotPreview1 trait that needs to be implemented in wasi_snapshot_preview1.rs:

diff --git a/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs b/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
index e4e4a604f..0fc1fa22f 100644
--- a/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
+++ b/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
@@ -1052,4 +1052,9 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     fn sock_shutdown(&self, _fd: types::Fd, _how: types::Sdflags) -> Result<()> {
         unimplemented!("sock_shutdown")
     }
+
+    fn print_greeting(&self) -> Result<()> {
+        println!("Hello World from the new print_greeting syscall in WASI");
+        Ok(())
+    }
 }

So we add a very simple implementation for our print_greeting function which prints to the console. Ultimately, we want this function to get executed by the runtime when a WebAssembly module imports the print_greeting function from WASI.

If we now build the main repository, we should have a version of Wasmtime that contains the print_greet function in the wasi_snapshot_preview1 module.

Using the print_greeting function from a WebAssembly module

At this point, we can write a WebAssembly module that imports the print_greeting function and calls it:

(module
    (import "wasi_snapshot_preview1" "print_greeting" (func $print_greeting (result i32)))

    (memory 1)
    (export "memory" (memory 0))

    (func $main (export "_start")
        (call $print_greeting)
        return
    )
)

If we use our newly built Wasmtime binary and filter the logs, we can see the runtime executing our print_greeting function:

RUST_LOG=wasi_common=trace ./target/debug/wasmtime examples/greeting.wat
TRACE wasi_common::wasi::wasi_snapshot_preview1

> print_greeting()
Hello World from the new print_greeting syscall in WASI

TRACE wasi_common::wasi::wasi_snapshot_preview1
> errno=No error occurred. System call completed successfully. (Errno::Success(0))

This isn’t terribly useful, as our syscall does not take any arguments, and just prints a static message - but it shows how to potentially add an implementation for very common operations that you might have in your runtime context, or extend existing runtimes.

As next steps, one could try adding more complicated logic for the system call, parameters and other return values, and potentially building modules that use the newly added function from programming languages that compile to WASI (such as Rust, or C++). Hopefully, this article gives you a starting point for getting started with WASI and Wasmtime.