Edge programming with Rust and WebAssembly

Recently, we launched Fastly Labs, a hub of big, in-progress projects where developers can find inspiration, and play around with next-generation technology. One of our most exciting projects is Terrarium, our edge computing platform based on WebAssembly. Terrarium can compile a couple of different programming languages to WebAssembly, which is then compiled to fast, safe, native code, and used to power a web service.

This post will describe how to program Terrarium using the Rust programming language. The Rust community, most notably the Rust and WebAssembly Working Group, have done a ton of work making Rust compile to fast, efficient WebAssembly — with tools that are easy to learn and use. We’ll show you how you can compile Rust programs to WebAssembly right on your local machine, how to interact with the Terrarium system, and what sort of applications we’ve built with it.

Compiling Rust to WebAssembly

Terrarium already knows how to compile Rust programs to WebAssembly. But if you want to experiment with it on your own machine, here are some steps you can follow.

First, for this tutorial you should use rustup, the Rust toolchain installer. Once you have rustup, installing a Rust toolchain that can target WebAssembly is as easy as:

$ rustup toolchain add nightly
$ rustup target add wasm32-unknown-unknown --toolchain nightly

At this time, the WebAssembly target (wasm32-unknown-unknown) is only supported on the nightly toolchain.

From there, you can make a new crate with cargo new --lib <name>, and then put

[lib]
crate-type=[“cdylib”]

in the Cargo.toml file.

Finally, run rustup override set nightly in your crate’s directory, so that the nightly toolchain is used by default.

Now, when you run cargo build --target wasm32-unknown-unknown, a WebAssembly binary will be created in target/wasm32-unknown-unknown/debug/. This binary contains a single WebAssembly module. You could translate this binary to a text representation using the WebAssembly Binary Toolkit’s wasm2wat tool to inspect its contents, and even upload the text format directly to Terrarium by using the “Empty Wat Project” template.

The Terrarium WebAssembly environment

Nice work! You’ve compiled a simple Rust program to WebAssembly. Unfortunately, you can’t turn just any Rust program into one that compiles to WebAssembly and runs on Terrarium. Much like in browser-based WebAssembly runtimes, Terrarium doesn’t provide the full set of system calls that Windows, macOS, or Linux provides for ordinary Rust programs. This means large parts of the standard library won’t work: for instance, Terrarium doesn’t provide a filesystem for WebAssembly programs to use.

So how does your Terrarium program interact with the outside world? We’ve provided a library, http_guest, that provides an idiomatic Rust interface to the Terrarium environment. Your program can use it through the usual Rust syntax:

#[macro_use]
extern crate http_guest;

Next, we’ll introduce the library, and explain how it works under the hood.

Application entry point

One difference between Terrarium and ordinary Rust executables is the application entry point. In Terrarium, the http_guest crate provides the guest_app! macro, which takes care of creating a run function and initializing the panic handler and memory management. Additionally, these macros provide one more convenience: they structure the user’s program as a function from request to response. The function you pass to guest_app! should have type

Fn(&Request<Vec<u8>>) -> Response<Vec<u8>>

This function type matches the Terrarium programming model: each time the Terrarium server gets an HTTP (or HTTPS or HTTP2) request at <subdomain>.fastly-terrarium.com, it instantiates and runs the WebAssembly module. The WebAssembly code can use any of the Terrarium APIs during its execution, and the response it returns is sent to the originating client.

HTTP requests and responses

In addition to servicing the HTTP request made to the Terrarium server, Terrarium programs can make their own HTTP requests. The Rust interface to these requests and responses is through the types provided by the http crate, which is used throughout the Rust ecosystem. The http_guest crate re-exports the Request and Response structs, and extends Request with methods for sending it synchronously or asynchronously. For asynchronous requests, the program can either use the PendingResponse methods to poll or wait on an individual response, or the select function to wait for the first of many PendingResponses to be available.

For Rust language examples that use this functionality, see the GitHub GraphQL Example and Weather Example.

Key-value store

The Terrarium runtime provides a key-value store for sharing state between instances of the same WebAssembly module. A new, empty store is created for each WebAssembly module deployed through Terrarium. Each instance can read or write only to the store corresponding to its deployment. Terrarium deployments only live for five minutes, after which the web service and store are deleted.

To use the store from Rust applications, define the application entry point using the guest_app_kvs! macro in place of the guest_app! macro. Then, your entry point function should have type:

Fn(&mut KVStore, &Request<Vec<u8>>) -> Response<Vec<u8>>

From there, you can use the KVStore methods to read and write to the store.

Other interfaces

Terrarium provides a few more interfaces you can leverage. Your program can get the current wall time, use a random number generator, and make DNS requests. Additionally, we provide a set of common and useful Rust crates for your Rust program to use.

Implementing the Terrarium interfaces

We’ve gone through what sort of interfaces Terrarium provides. In this section, we’ll show you how they are implemented so you can see how things work under the hood.

Import Functions

A WebAssembly module can declare import functions, which it can call like ordinary functions. The Terrarium runtime provides the implementations of these import functions, so all your Rust program has to do is declare them using the C FFI:

extern "C" {
	pub fn hostcall_req_create(
    	method_ptr: *const u8,
    	method_len: usize,
    	url_ptr: *const u8,
    	url_len: usize,
	) -> i32;

	pub fn hostcall_req_send(req: i32) -> i32;

	pub fn hostcall_req_send_async(req: i32) -> i32;

	...
}

We typically call these import functions that are implemented by the runtime “hostcalls”, because it makes it clear that control is yielded from the WebAssembly (guest) program to the host program.

As you can see from the function signature, the hostcalls operate on unsafe, low-level primitives: pointers, lengths, and integers. So, you will also want a idiomatic, safe Rust wrapper around these unsafe functions. This also looks like ordinary use of the Rust C FFI:

#[derive(Debug, PartialEq, Eq, Hash)
pub struct RequestHandle(i32); 

impl RequestHandle {
	/// Create a new request.
	/// If request creation fails within the hostcall, this returns
	/// `None`. This is typically due to a malformed method or URL
	/// string.
	pub fn create(method: &str, url: &str) -> Option<RequestHandle> {
    	let method_bytes = method.as_bytes();
    	let url_bytes = url.as_bytes();
    	let req = unsafe {
        	raw::hostcall_req_create(
            	method_bytes.as_ptr(),
            	method_bytes.len(),
            	url_bytes.as_ptr(),
            	url_bytes.len(),
        	)
    	};
    	let req = RequestHandle(req);
    	if req.is_error() {
        	None
    	} else {
        	Some(req)
    	}
	}
	...
}

We provide this nicely wrapped up API as the http_guest crate. The source is available here and the docs are available here.

Export functions

A WebAssembly module may declare export functions. These declarations give an externally-visible name to functions that may be called by the host. Terrarium expects each module to have an export function named “run”.

Your Rust program needs to implement the run function using the following syntax to ensure it is compiled to a WebAssembly export function:

#[no_mangle]
pub extern “C” fn run() {
	...
}

This function is created by the guest_app! macro in the empty and example projects.

In the start of the run function, we want to do two things: install a panic handler, so that Rust panic messages are reported to the Terrarium runtime instead of just causing it to halt, and install memory management functions with Terrarium. We provide implementations of these in http_guest::panic_set_once and http_guest::init_mm_default.

There’s a lot to delve into with the memory management scheme, so we’ll be covering it in the future here on the blog.

The panic handler passes the panic message to a special Terrarium hostcall. If this hostcall occurs, or division by zero, or another WebAssembly fault such as invalid memory access (which shouldn’t be possible using the safe Rust interfaces), the Terrarium server will terminate the WebAssembly instance and send the error message as the HTTP response.

Putting it all together

We’ve shown you how to build your own Rust programs to WebAssembly, use the Terrarium interfaces, and how those interfaces are implemented. You now have all of the pieces you need to create your own Rust applications that run on Terrarium. And as always, if you have any questions you can comment below.

Look for future posts where we will introduce a command-line tool that automates building and uploading programs to Terrarium, and a deep dive on memory management between the Terrarium server and WebAssembly programs. Until then, enjoy hopping into the world of Terrarium, and let us know your thoughts.

Pat Hickey
Principal Software Engineer
Published

6 min read

Want to continue the conversation?
Schedule time with an expert
Share this post
Pat Hickey
Principal Software Engineer

Pat Hickey is a principal software engineer on Fastly's isolation team. Previously, he worked on operating systems and compilers for safety-critical systems.

Ready to get started?

Get in touch or create an account.