Updates on WAGI, the WebAssembly Gateway Interface

A few months ago, our team introduced WAGI, or the WebAssembly Gateway Interface, a simple way of writing and executing HTTP response handlers as WebAssembly modules. Since we open sourced the project, the community has been adding critical features that make WAGI one of the easiest ways to build WebAssembly microservices, and in this article we explore some of the new features.

A primer on WAGI

WAGI is for writing HTTP handlers. It uses WASI, the WebAssembly System Interface, and exposes HTTP request information to a WebAssembly module as environment variables and through standard input and output. This means that for any language that compiles to WASI, writing an HTTP handler that runs on WAGI is as easy as reading from stdin and writing to stdout.

Echoing the body of a request can be done by copying the standard input stream directly to standard output (or it can be read as a byte array and further processed), and in Rust it can be done in the following way:

use std::io::{copy, stdin, stdout};

fn main() {
    println!("Content-Type: text/plain\n");

    println!("Server received body:");
    copy(&mut stdin(), &mut stdout()).unwrap();
}

After compiling the module to wasm32-wasi, create a new WAGI configuration file that sets the /echo route to execute the compiled WebAssembly module as an HTTP handler:

[[module]]
route = "/echo"
module = "target/wasm32-wasi/release/hello-wagi.wasm"

Then start WAGI by running wagi --config wagi.toml. At this point, WAGI can receive requests which will be delegated to the built module:

$ curl localhost:3000/echo -X POST -d 'Testing WAGI'
Server received body:
Testing WAGI

Any language that compiles to WASI can be used to write WAGI handlers, together with any external library for these languages that is compilable to wasm32-wasi. So far, our team has been testing with Rust, C/C++, AssemblyScript, Swift, Grain, and Zig, but more and more compiler toolchains are adding support for emitting WebAssembly.

Outbound HTTP connections

Networking in WASI is one scenario that doesn’t have a stable API yet. This restricts the types of workloads that can be executed in WASI runtimes for now, which is why we released an experimental outbound HTTP library for WASI that allows modules to create outbound HTTP connections.

Specifically, the published NPM package and crate can be used to send HTTP requests from AssemblyScript and Rust guest modules running in WAGI - for example, making a GET request and reading the response body as a string in AssemblyScript:

import { Console } from "as-wasi";
import { Method, RequestBuilder } from "@deislabs/wasi-experimental-http";

export function _start(): void {
  Console.write("Content-Type: text/plain\n");

  let res = new RequestBuilder("https://api.brigade.sh/healthz")
    .method(Method.GET)
    .send();
  let str = String.UTF8.decode(res.bodyReadAll().buffer);
  Console.write("Result from request: " + str);
}

Notice how the module will attempt to make an HTTP request to https://api.brigade.sh/healthz - before starting WAGI with this module, we have to explicitly allow the guest module to make a request to this host using the allowed_hosts field in the WAGI configuration for this module:

[[module]]
route = "/as-http"
module = "as/build/optimized.wasm"
allowed_hosts = ["https://api.brigade.sh"]

Keep in mind that this library is experimental, and breaking changes could occur in the future.

Pulling modules from OCI registries

Given the widespread adoption of the container ecosystem, a natural choice for distributing WebAssembly modules intended to run outside the browser is OCI registries, and WAGI now supports referencing and pulling modules from supporting OCI registries (for a list of registries that have support for distributing Wasm modules, check the wasm-to-oci repository).

We can take any compiled WebAssembly module and push it to one of the compatible registries using wasm-to-oci, a tool that leverages the ORAS and OCI artifacts projects - for example, pushing the module we just built to the GitHub Package Registry:

$ wasm-to-oci push as/build/optimized.wasm ghcr.io/radu-matei/as-http:v1
INFO[0003] Pushed: ghcr.io/radu-matei/as-http:v1
INFO[0003] Size: 9004
INFO[0003] Digest: sha256:994c8adcef53e93f1b817d6d16367ed3897376378cad88eb31cfdfabbcb6b253

We can now modify the WAGI configuration to point to the newly pushed repository:

[[module]]
route = "/as-http"
module = "oci://ghcr.io/radu-matei/as-http:v1"
allowed_hosts = ["https://api.brigade.sh"]

After the first pull from the registry, WAGI will cache the module so the instantiation overhead is minimal.

Currently, the repositories have to enable anonymous pulls for WAGI to work, but we are tracking the request to add authentication for registries. As a workaround, wasm-to-oci pull can be used before starting WAGI to pull modules from authenticated repositories.

Besides using the local filesystem and OCI registries as sources for modules, WAGI can also pull artifacts from Bindle, although support for it is still early.

Declaring sub-routes in the module

WAGI now also allows modules to define their own sub-routes - specifically, in cases when we want a single module to handle more than one sub-route, by implementing and exporting a _routes() function in the module that maps routes to custom handler functions:

fn main() {
    println!("Content-Type: text/plain\n\n Hello from main()");
}

// Use no_mangle so we can call this from WAGI or other external tools.
#[no_mangle]
/// A provider function that can be called directly
pub fn hello() {
    println!("Content-Type: text/plain\n\n Hello")
}

#[no_mangle]
/// Another provider function that can be called directly.
pub fn goodbye() {
    println!("Content-Type: text/plain\n\n Goodbye")
}

// This maps a few routes:
// '/hello' will result in the `hello()` function being called.
// '/goodbye' and all subpaths of '/goodbye' will call the `goodbye()` function.
//
// Note that when compiled, the `main` function is named `_start()`. So if you want
// to map to that function, it is `/main _start`.
#[no_mangle]
pub fn _routes() {
    println!("/hello hello");
    println!("/goodbye/... goodbye");
    println!("/main _start");
}

Then, defining the top-level route for the module as /example in the WAGI configuration file:

[[module]]
route = "/example"
module = "/PATH/TO/hello_wagi.wasm"

After WAGI starts and build the routes, this module will map /example to _start(), /example/hello to hello(), and /example/goodbye/... to goodbye() (including wildcards).

You can find a full routing example here.

Using Azure services from Rust modules

Built on top of the outbound HTTP support, we have been experimenting with using various Azure services from WASI modules running in WAGI, using the Azure SDK for Rust - specifically, using Azure Blob Storage (reading and writing blobs), Cosmos DB (reading and writing documents and collections), and EventGrid (sending messages and handling event subscriptions through HTTP webhooks).

We are excited by the prospect of having fully portable modules running in a Wasm sandbox that use Azure services, packaged at less than 3 MB (with room for further optimization). An initial set of samples for WAGI modules using Azure services can be found on GitHub, and we would love to hear your feedback about building and running them.

Optimizing compiled modules and caching

We highly recommend using optimization tools to shrink the size of the resulting binary or speed-up the instantiation time. The following represents a non-exhaustive list of tools and techniques our team has been experimenting with:

  • wasm-opt from Binaryen - by removing debug symbols and unused module elements, together with an entire suite of instruction optimizers, wasm-opt is perhaps the most widespread tool, and depending on the toolchain used to compile modules, it can have exceptional results in shrinking the module size.
  • wizer from the Bytecode Alliance is a tool that pre-initializes WebAssembly modules - it executes their initialization function, and then snapshots the initialized state out into a new WebAssembly module. If a module spends significant time initializing, it might be a good candidate for Wizer, and for specific types of workloads, it can yield startup times of up to 6x faster.
  • Wasmtime caching in WAGI - by default, Wasmtime, the runtime used by WAGI, will compile a WebAssembly module just-in-time (JIT) for the current architecture it is running on. Wasmtime caching writes the compiled module to a temporary cache and when instantiating a module, it checks whether an already compiled version of it already exists in the cache, resulting in significant startup speed improvements for large modules. WAGI provides a --cache flag pointing to a Wasmtime cache file, but keep in mind that for small modules, the overhead introduced by the filesystem access might increase the instantiation time.
  • ahead-of-time compilation (AOT) with wasmtime compile - as opposed to JIT compilation, with AOT compilation, Wasmtime creates a new module that is already compiled for the current architecture (or cross-compile). This means that before instantiating, Wasmtime will not have to perform a JIT compilation, nor access the filesystem cache to check if an already compiled version exists. This requires an extra step after pulling the module, but for large modules, the performance improvements are significant.

These are just a few potential optimization techniques that can be performed with WAGI, and real world usage indicates that, depending on the workload, language, and runtime, a combination of these could improve the binary size or startup time for a given module. If there are other optimization techniques we can enable for WAGI, let us know about them in WAGI’s issue queue.

We would love to hear your feedback

WAGI is still in its early stages, but we are excited about the ways it could be used. We welcome all contributions that adhere to our code of conduct, and we are looking forward to your pull requests, issues, and suggestion.