Rust & WebAssembly workshop

This workbook contains the material for a Rust & WebAssembly workshop held by Ferrous Systems and Jan-Erik Rediger.

It is split into 3 parts:

Part 1: Setup

An installation guide for all tooling used throughout this book.

Part 2: Background

A bit of common knowledge & history about WebAssembly and Rust, as well as notable use cases.

Part 3: Tutorial

A hands-on tutorial writing Rust and compiling it to WebAssembly in 3 variations: as a command-line app, as a client-side web app and as an edge computing app in the cloud.

Preparations

This chapter contains information about the course material and an installation guide.

Workshop Materials

Clone the workshop git repository:

git clone https://github.com/ferrous-systems/wasm-training-2022

The workshop repository contains all workshop materials, e.g. code examples, and the source for this workbook.

Required Software

The Setup guide helps you install the required software used throughout this book.

Setup

This section describes how to set up the toolchain for compiling Rust programs to WebAssembly and integrate them with the different environments we will look at.

The Rust Toolchain

You will need the standard Rust toolchain, including rustup, rustc, and cargo.

Follow these instructions to install the Rust toolchain.

Rust and WebAssembly is available on Rust stable. That means we don't require any experimental feature flags. The latest Rust should work best.

WASM targets

Install the WASM targets:

rustup target add wasm32-unknown-unknown
rustup target add wasm32-wasi

Additional tooling

Some of these are optional. They make some tasks easier to handle, but it can be done without them.

wasmtime

A fast and secure runtime for WebAssembly.

Full installation instructions: https://docs.wasmtime.dev/cli-install.html

Linux and macOS users can execute the following:

curl https://wasmtime.dev/install.sh -sSf | bash

Alternatively, on macOS with brew:

brew install wasmtime

This will download a precompiled version of wasmtime and place it in $HOME/.wasmtime, and update your shell configuration to place the right directory in PATH.

Windows users should visit the releases page and download the MSI installer (wasmtime-v2.0.0-x86_64-windows.msi for example) and use that to install.

wasm-bindgen

Tool to generate JavaScript bindings for a wasm file.

cargo install wasm-bindgen-cli

Serving local content over HTTP

"Host These Things Please" (https) is a basic http server for serving files in a folder over HTTP locally.

Install it using cargo:

cargo install https

You can later simply use http to run it.

wasm2wat (optional)

Translate from the binary WebAssembly format back to the text format (also known as a .wat). Part of the WebAssembly Binary Toolkit (WABT).

macOS:

brew install wabt

Others:

Download the release from the WABT release page.

Fastly CLI (optional)

fastly is an open-source command line tool for interacting with the Fastly API. It can be used to create, build and run Compute@Edge projects locally and deploy them on Fastly.

Installation instructions.

For macOS:

brew install fastly/tap/fastly

For Windows and Linux:

Download a release from the fastly GitHub Release page.

Tooling check

Setup check

✅ Fully restart your terminal (not just open a fresh tab).

✅ Let's check that you have installed Rust.

$ rustc --version
rustc 1.64.0 (a55dd71d5 2022-09-19)
$ cargo --version
cargo 1.64.0 (387270bc7 2022-09-16)
$ rustup target list --installed
(cut)
wasm32-unknown-unknown
wasm32-wasi
(cut)

✅ Let's check that you have installed the tools listed in the previous section (Note: not all are required).

$ wasmtime --version
wasmtime-cli 2.0.0
$ wasm-bindgen --version
wasm-bindgen 0.2.83
$ http
Hosting "." on port 8000 without TLS and no authentication...
Ctrl-C to stop.

Note: This will host the current directory over HTTP. Use Ctrl-C to stop it.

$ wasm2wat --version
1.0.30
$ fastly version
Fastly CLI version v4.2.0 (a1e8772)
Built with go version go1.18.6 linux/amd64
Viceroy version: viceroy 0.3.1

What is WebAssembly?

WebAssembly is a technology that allows you to compile application code written in pretty much any language (including Rust, C, C++, JavaScript, and Go) and run it inside sandboxed environments. WebAssembly is often known as just "wasm".

WebAssembly originated as a successor to asm.js, a low-level subset of JavaScript, and Google Native Client (NaCl), a technology to run a subset of native code in a sandboxed environment within the browser.

WebAssembly itself started in 2015, with a first release of the specification in 2017. By 2019 it became an official web standard with implementations across all major browsers.

Since then it became a compilation target for a wide variety of programming languages, gained usage across the web and other execution environments and got several independent runtime implementations inside and outside of browsers

Contrary to what the name might make you believe it is not tied to the web only. But the web is where it originated.

In the next chapters you will learn what WebAssembly looks like and where it is used.

The Hello World of WebAssembly

We will work with Rust throughout this book. The first "Hello World" application is thus a small Rust function to add 2 numbers together and return the result.

#[no_mangle]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
    left + right
}

WebAssembly is a binary format. The above function compiled to a WebAssembly module results in the following binary blob (hexdumped).

00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 01
7f 03 02 01 00 05 03 01 00 10 07 10 02 06 6d 65
6d 6f 72 79 02 00 03 61 64 64 00 00 0a 09 01 07
00 20 00 20 01 6a 0b

Along with the binary format there's also the WebAssembly text format, wat. The above module represented as wat:

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func $add (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "memory" (memory 0))
  (export "add" (func $add))
)

(The wasm2wat tool transforms the binary output to its equivalent text format)

In later chapters of this book you will learn how to write, compile and run these WebAssembly modules in different environments.

Rust & Wasm

The Rust compiler gained proper asm.js support (a wasm predecessor) using Emscripten some time in 2016 and experimental WebAssembly support shortly after the same year with Rust 1.14 (the wasm32-unknown-emscripten target).

The wasm32-unknown-unknown target became available on Rust Nightly in November 2017. wasm32-wasi was added in 2019 (initially as wasm32-unknown-wasi). These are the two main targets you will work with.

A WebAssembly Domain Working Group was started within the Rust project in 2018. Their plan was to drive WebAssembly support in the Rust compiler forward, create tooling and libraries for Rust & WebAssembly and provide learning material.

Early on WebAssembly tooling was written in Rust, often to simplify Rust & WebAssembly development, but sometimes acting as general tooling as well. Tools such as wasm-bindgen or wasm-pack became early examples of what great WebAssembly tooling can provide for the ecosystem.

Some WebAssembly runtimes were written in Rust, most notably wasmtime. The community started developing libraries and frameworks for WebAssembly development, e.g. Yew, a framework for making client-side single-page apps.

From the get-go Rust was a first-class citizen in the WebAssembly world, both as a language targeting WebAssembly as well as the language tools and libraries for WebAssembly were written in.

Use cases

WebAssembly originated as a successor to browser technology like asm.js and Google Native Client (NaCl). Naturally WebAssembly gained popularity across a wide range of use cases on the web.

WebAssembly on the web allows to build existing software written in a variety of languages and run them as part of ordinary web applications.

The following is a list of interesting existing web applications using WebAssembly.

Pyodide

A full Python distribution running in your browser. It comes with builtin packages as well as support to install pure-Python packages from PyPi.

An in-browser REPL is available at https://pyodide.org/en/stable/console.html.

Datasette Lite

Datasette is an open source multi-tool for exploring and publishing data. It provides an interface to SQLite databases.

Datasette Lite is based on Pyodide and brings the full application to the browser. You can open remote database and CSV files, execute queries and browse through the loaded database.

squoosh.app

squoosh is an image compression web app, fully client-side. It provides an interactive interface to resize an image and supports different output codecs. Everything is happening client-side and images never leave the browser.

Source code is available on GitHub.

Photoshop on the Web

Photoshop on the Web is the nearly-complete Photoshop experience running in the browser. It's currently in beta and not yet fully supported in all browsers.

Tailscale SSH Console

Tailscale is a VPN service that allows you to make your devices accessible within an overlay network, no matter where those devices are physically located. It recently started to support SSH over their service with next to no setup. They now offer an SSH console directly in the browser. Their VPN client and networking code has been compiled to WebAssembly and (encrypted) traffic goes directly to relay servers, but not through additional proxies.

WASM & JavaScript

WebAssembly is available to every website now through the JavaScript web API. It is supported in all recent versions of all major browsers1.

The WebAssembly web API is available on the WebAssembly JavaScript object. The available API allows to compile and instantiate WebAssembly modules, access exported functionality and access the shared memory block used to share data between the WebAssembly module and the JavaScript environment. The web tutorial chapter will guide you through some of the usage later.


1

See caniuse.com.

Use cases everywhere else

WebAssembly is also supported outside of the browser environment. There it can be used for a wide variety of applications, making use of its sandboxing and security functionality.

Some possible use cases include:

Plugins

User-facing native applications can safely support user-contributed plugins. These plugins are compiled to WebAssembly modules and the application can run them in a restricted environment within the application, allowing access to only a small part of the application.

Serverless

"Serverless" can describe a wide variety of concepts. In recent times it became known as a cloud computing execution model, where cloud providers allocate machine resources on demand, managing it for their users and executing the user's application code on request.

Most commonly this is offered as a Function as a Service (FaaS) platform, where small application logic is executed on incoming requests, using limited resources (CPU, time, memory).

WebAssembly allows that users can write this logic in a language of their choice and the provider supports a general WebAssembly execution environment, often accompanied with a provider-specific SDK. The provider can leverage the WebAssembly sandbox mechanism to provide per-request isolation & performance.

We look at one of these serverless offerings: Fastly's Compute@Edge.

Docker

Docker recently announced a Technical Preview of their WebAssembly support. Docker containers can be used to build and distribute WebAssembly applications. The Docker engine can then extract and run this WebAssembly application in a wasm runtime, all while using the familiar Docker tooling.

Third-party library sandboxing

RLBox is a toolkit for sandboxing third party C libraries. This allows to run third-party libraries within an existing application, but restricting the access to only what is directly provided to the library as input, thus reducing the attack surface of this part of the code. It is in use in Mozilla Firefox.

WASI

The WebAssembly specification describes a very limited interface that the environment a WebAssembly module runs in need to provide. It essentially has 3 important parts:

  • Imports. Functionality provided by the environment for use within the WebAssembly code.
  • Exports. The functions the WebAssembly module exports, making them callable from the outside.
  • Linear Memory. The WebAssembly module has access to a block of linear memory, which it potentially can expand on request. This memory can be read by the host environment as well.

Therefore WebAssembly code is limited to self-contained computation, calls to imported functions and reading and writing from memory. No default imports are provided and how data is laid out in the linear memory is also unspecified.

And this is where WASI comes in:

WASI is a modular system interface for WebAssembly. As described in the initial announcement, it’s focused on security and portability.

(via wasi.dev)

WASI is a specification of the interfaces a program can use to communicate with the host environment. It is up to the host environment how these interfaces are implemented and if additional security mechanisms are enforced.

Rust supports the wasm32-wasi target and the Rust standard library is implemented for this target, allowing for most Rust programs and libraries to just work with this target. The WebAssembly runtime wasmtime implements the required WASI interfaces in a capability-based security model

The initial announcement for WASI has a lot more details on how it works.

Fastly's Compute@Edge

Fastly is a cloud computing provider and content delivery network (CDN).

Earlier this year they released their Compute@Edge platform. This platform allows to run WebAssembly code at the Fastly edge. They chose WebAssembly for exactly the reasons we listed in the previous chapter: lightweight sandboxing, per-request isolation and performance.

They released a Rust SDK (fastly), which provides the necessary integration to read an incoming request and generate an appropriate response. The host runtime uses WASI to provide the necessary system interfaces. This means that you can write normal Rust code, using most of the Rust standard library and a large number of available Rust crates without issues. They also support JavaScript and Go as languages on this platform.

You can find the Fastly Compute@Edge documentation at https://docs.fastly.com/products/compute-at-edge.

You will learn how to write a small application for this platform in the Edge computing tutorial later in this book.

Idea

We now want to build a slightly more complex application.

The idea is to use an existing image manipulation library to apply filters to a given image1. This example will show us how to use an existing Rust crate, how to handle input and output and how to interact with the different environments.

We start off with building a command-line tool run using wasmtime, then build a web application running completely client-side, and last as an edge computing API that processes images posted to it.

We will work with the following example image (but really any image will work). Right-click it and save it to disk for later use.

When applying the filter named "1977", this is the result:

Several more filters are available in the library.

1

The image filters are inspired by Instagram. The implementation is based on CSSgram, which was ported to Rust by @ha-shine. The example image was taken on 2022-10-28 by Jan-Erik Rediger.

Command-line interface

In this tutorial you'll get familiar with:

  • Building Rust code for the wasm32-wasi target
  • Running applications on the command-line using wasmtime
  • Re-using existing crates in a WASM application
  • wasmtime's capability-based system

We start with a command-line tool that takes an image and a filter name as input. It applies the given filter to the image and produces an output.png.

Hello World on the command line

✅ Create a new Rust project

cargo new rustagram
cd rustagram

✅ To start the tool will only print a message. Open src/main.rs and add

fn main() {
    println!("Hello World from wasmtime!");
}

Next, read how to build and run the application.

Building and running with wasmtime

✅ You can build for the wasm32-wasi manually like this:

cargo build --target wasm32-wasi

This should create a file target/wasm32-wasi/debug/rustagram.wasm.

✅ Now that the application is built you can run it using wasmtime:

wasmtime target/wasm32-wasi/debug/rustagram.wasm

You should see the message printed:

$ wasmtime target/wasm32-wasi/debug/rustagram.wasm
Hello World from wasmtime!

✅ (Optional) You can transform the generated WebAssembly code into its text representation using wasm2wat

wasm2wat target/wasm32-wasi/debug/rustagram.wasm

Caution: this produces a lot of output.

You should see something like this:

(module
  (type (;0;) (func))
  (type (;1;) (func (result i32)))
  (type (;2;) (func (param i32)))
...

Try to identify your "Hello World" code.

Image filter application

Now that you can build and run an application compiled to WebAssembly, it's time to build some functionality into it.

The goal is:

  • Take an input file, a filter name and, optionally, an output file (or "output.jpg" as the default).
  • Load the input file, apply the given filter to this image, then write the resulting image to the output file.

You can continue with the previously created project.

✅ Open src/main.rs again and replace the println! line with code to parse the arguments.

fn main() {
    let mut args = std::env::args().skip(1);
    let input = args.next().expect("INPUT required");
    let filter = args.next().expect("FILTER required");
    let output = args.next().unwrap_or_else(|| "output.jpg".to_string());

    dbg!((input, filter, output));
}

✅ Build and run this to make sure it works as expected.

✅ Now add a dependency to handle image manipulation. The image filters are readily available in the rustagram2 crate. Add the rustagram2 crate as a dependency in rustagram/Cargo.toml

[dependencies]
rustagram2 = "2.0.0"

The documentation is available on docs.rs/rustagram2.

✅ You need a FilterType to apply later. rustagram2 shows the available filters in the FilterType documentation. It also has FromStr from the standard library implemented for it, so you can parse strings into the filter type by calling parse() on the string.

let filter_type = filter.parse().expect("can't parse filter name");

An unknown filter name would cause an error. For now you don't need to handle that. Your application can just panic and exit.

If you compile everything at this point you will probably hit a type annotation error. You can try to resolve that now. You can also continue and observe how this error will be resolved once you add more code in the next steps.

Now comes the main part of the application: load the image, apply the filter and save the resulting file. This is a small challenge for you to write, but the next steps guide you through it.

✅ You need to read the file from disk and turn it into an object you can work with. image::open does that for you easily. Don't worry about error handling and just unwrap.

✅ The image type you get is able to represent a wide variety of image types. For this tutorial you want an RgbaImage. You can convert your image using the to_rgba8 method.

✅ Last but not least you need to apply the selected filter on this image. The rustagram2 crate implements that as the apply_filter method on a trait. This trait is automatically implemented for the RgbaImage type you got from to_rgba8.

✅ Save back to the file output by using the save method available on the image.

With the help of the documentation this should be achievable in a couple of lines of code.

Try it for yourself!

✅ Once you wrote the code, build it again and try to run it.

Expected output when you don't pass any arguments:

$ wasmtime target/wasm32-wasi/debug/rustagram.wasm
thread 'main' panicked at 'INPUT required', src/main.rs:7:29
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error: failed to run main module `target/wasm32-wasi/debug/rustagram.wasm`

Caused by:
    0: failed to invoke command default
[...]

Expected output when you pass a file path and a filter name:

$ wasmtime target/wasm32-wasi/debug/rustagram.wasm kongelige-slott.jpg 1977
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: IoError(Custom { kind: Uncategorized, error: "failed to find a pre-opened file descriptor through which \"kongelige-slott.jpg\" could be opened" })', src/main.rs:12:34
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error: failed to run main module `target/wasm32-wasi/debug/rustagram.wasm`

Caused by:
    0: failed to invoke command default
[...]

What did just happen?

wasmtime ran your code up until it tried to read the image from disk. By default wasmtime blocks all filesystem access. You need to explicitly give permission to specific directories in order to be able to read and writes files within.

$ wasmtime --dir . target/wasm32-wasi/debug/rustagram.wasm kongelige-slott.jpg 1977
$

This should now have created output.jpg.

Final application

You should have this file tree layout:

$ tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

To recap your final code should look something like this:

use rustagram::{image, RustagramFilter};

fn main() {
    let mut args = std::env::args().skip(1);
    let input = args.next().expect("INPUT required");
    let filter = args.next().expect("FILTER required");
    let output = args.next().unwrap_or_else(|| "output.jpg".to_string());

    let filter_type = filter.parse().expect("can't parse filter name");
    let img = image::open(input).unwrap();
    let out = img.to_rgba8().apply_filter(filter_type);
    out.save(output).unwrap();
}

You can build your code like this:

cargo build --target wasm32-wasi

And run it with wasmtime:

wasmtime --dir . target/wasm32-wasi/debug/rustagram.wasm skyline.jpg 1977

Some ideas on what to do next:

  • Run the application natively: cargo run. Any complications or differences?
  • Inspect the built wasm module using wasm2wat. Can you spot the parts of the code that you've written? Can you find the names of all available filters?
  • Try some other crate you know. Does it work as-is on WebAssembly/with Wasi?

Web

In this tutorial you'll get familiar with:

  • Building Rust code for the wasm32-unknown-unknown target
  • Interacting with a WASM application from JavaScript
  • The wasm-bindgen tool to handle more complex types passed over the boundary

Next we build a web application that processes images client-side in the browser. No server processing involved.

We re-use the same Rust crate to apply the image filter, but this time load the image directly from a binary blob. That binary blob is passed in from the JavaScript side.

Hello World on the web

You already saw the "Hello World of WebAssembly" earlier. You will now run this on the web without additional tools.

✅ Create a new crate

cargo new --lib hello-world
cd hello-world

✅ Set the crate type to cdylib in Cargo.toml

[lib]
crate-type = ["cdylib"]

✅ Write the add function.

#[no_mangle]
pub extern "C" fn add(left: i32, right: i32) -> i32 {
    left + right
}

The no_mangle attribute ensures that the function name lands in the binary as is, otherwise you couldn't later call it by name. extern "C" ensures it uses the C-compatible ABI, and thus what WebAssembly (and JavaScript) expects.

✅ Compile it to WebAssembly.

cargo build --target wasm32-unknown-unknown

This will create target/wasm32-unknown-unknown/debug/hello_world.wasm.

✅ Next create an HTML file index.html in your crate's directory (next to Cargo.toml):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust WASM Demo</title>
  </head>
  <body>
    <script type="module">
      <!-- to be filled in -->
    </script>
  </body>
</html>

✅ Now you need to load, compile and instantiate the WebAssembly module. All of this is part of the web API. fetch can load data from URLs, WebAssembly.instantiate() compiles and instantiates the WebAssembly module.

      let response = await fetch('target/wasm32-unknown-unknown/debug/hello_world.wasm');
      let bytes = await response.arrayBuffer();
      let result = await WebAssembly.instantiate(bytes, {});

The result of this is an instance that has accessors for the exported functions of the module.

✅ Call the add method on the Wasm module instance.

      const sum = result.instance.exports.add(1, 2);
      console.log(`1 + 2 = ${sum}`);

✅ Serve your HTML file and the WebAssembly over HTTP.

http

Open http://localhost:8000 in your web browser and open the Developer Tools. In the console you should now see the result:

1 + 2 = 3

Short introduction to wasm-bindgen

WebAssembly is limited to basic integer and float types, but does not itself support rich types like strings, objects, enums or closures. However an instantiated WebAssembly module has access to memory where it can place more data. This block of memory is also accessible by the host side, e.g. the JavaScript environment of a website. Both sides, the WebAssembly code and the host side, need to agree what bytes in that memory block mean in order to work with them.

wasm-bindgen is a tool that can generate the necessary code on both sides to handle more rich types. It supports a variety of Rust types, including String, Vec, Result and slices, and allows to export Rust types for use in JavaScript (see wasm-bindgen's Supported Rust types).


How to use

The wasm-bindgen CLI utility works on the compiled .wasm file. It supports several different output targets. For this tutorial we focus only on JavaScript and the web target.

wasm-bindgen path/to/module.wasm --out-dir app --target web --no-typescript

#[wasm_bindgen(start)]

This annotation should be put on a public function. That function essentially becomes your start function, which gets automatically called when you instantiate the WebAssembly module.

You would for example use this to set up a panic handler and logging.

#[wasm_bindgen(start)]
pub fn main() {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(log::Level::Debug).unwrap();
}

#[wasm_bindgen] on a function

If used without additional attributes this wraps the annotated function to be exported. You can use the supported Rust types and wasm-bindgen will ensure the conversion happens on either side.

#[wasm_bindgen]
pub fn say_hello(name: String) -> String {
    format!("Hello, {}", name)
}

#[wasm_bindgen] on a struct

Annotating a Rust struct makes that struct available on the JavaScript side as an object.

#[wasm_bindgen]
struct Country {
    string shortcode;
}

Methods of that struct need to be annotated to be available in JavaScript, too.

impl Country {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Country {
        Country { shortcode: "NO".to_string() }
    }

    #[wasm_bindgen(getter)]
    pub fn shortcode(&self) -> String {
        self.shortcode.clone()
    }
}

The tutorial won't use this, but feel free to play around.

See On Rust exports in the wasm-bindgen documentation for more.


In the next chapter you will start building the image filter application for the web, using wasm-bindgen to help with the rich types.

Basic setup

✅ Create a new Rust project.

cargo new --lib image-filter
cd image-filter

✅ Set the crate type to cdylib in Cargo.toml

[lib]
crate-type = ["cdylib"]

✅ To simplify the build later on you can use make to build the Rust crate and call wasm-bindgen to generate the JavaScript shim. Create a Makefile and add this:

.PHONY: build
build:
	cargo build --release --target=wasm32-unknown-unknown
	wasm-bindgen target/wasm32-unknown-unknown/release/image_filter.wasm \
	    --out-dir app \
	    --target web \
	    --no-typescript

You can also use a shell script to do the same or simply run these commands manually.

✅ Add wasm-bindgen and rustagram2 dependencies to Cargo.toml: `

[dependencies]
rustagram2 = "2"
wasm-bindgen = "0.2.83"

✅ To help with debugging and logging add these 3 dependencies to Cargo.toml

[dependencies]
console_error_panic_hook = "0.1.7"
console_log = "0.2.0"
log = "0.4.17"

console_error_panic_hook ensures that you get Rust's panic message & stack trace in your browser's console. console_log ensures you can use Rust's log crate for logging as you are used to.

✅ It's time to set up the above mentioned crates in the module's start function. Annotate your main function with wasm_bindgen(start).

use std::panic;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main() {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(log::Level::Debug).unwrap();
}

Note: The name of this function actually doesn't matter. The annotation is what tells wasm-bindgen that this becomes the setup function.

✅ You should now be able to compile the Rust code to WebAssembly and use wasm-bindgen to generate the JavaScript shim. If you are using the Makefile as above you can now run

make

Otherwise run the commands directly:

cargo build --release --target=wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/release/image_filter.wasm --out-dir app --target web --no-typescript

You should find 2 new files in the app directory: image_filter.js and image_filter_bg.wasm.


In the next chapter you will write the few Rust pieces necessary for the image filter application. After that you build the web frontend to load and run the WebAssembly module.

Image filter application

With the basic setup for the Rust code done you can now write a function that applies the image filter to a given image.

✅ Start by importing the necessary modules and structs from the rustagram2 crate and the standard library in your src/lib.rs.

use rustagram::image::io::Reader;
use rustagram::image::ImageOutputFormat;
use rustagram::RustagramFilter;

✅ Next create a new function. It will get a slice of bytes representing the image and a filter name as a string. It should return a Vec<u8>, a vector of bytes representing the modified image in PNG format.

#[wasm_bindgen]
pub fn apply_filter(img: &[u8], filter: &str) -> Vec<u8> {
    log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);
    // (to be filled in)
}

The code from the next steps will go in this function.

✅ You previously set up logging, use that and log something to ensure you get the data that you expect.

    log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);

✅ The image data needs to be read from the buffer. The application allows multiple file formats, luckily the image format can guess the format and then decode it. The documentation for the Reader type goes into some detail.

    let img = Reader::new(Cursor::new(img))
        .with_guessed_format()
        .unwrap()
        .decode()
        .unwrap();

For now just unwrap on errors. As you have set up the panic handler you should see it in the browser's console if you hit an error.

✅ As you have done in the CLI application parse the filter name into a FilterType.

    let filter_type = filter.parse().unwrap();

Again if you compile everything at this point you will probably hit a type annotation error. That is expected and you can observe how this changes as you fill in the rest of the code in the next steps.

✅ You now have everything you need to apply the filter to the decoded image. This is exactly the same as in the previous tutorial.

    let out = img.to_rgba8().apply_filter(filter_type);

✅ But now instead of saving that changed image to a file you should store it in a buffer and return that buffer. ImageBuffer#write_to does just that. Don't forget to specify its format as PNG.

    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), ImageOutputFormat::Png)
        .unwrap();

    bytes

And that is already all the code you need to be able to apply an image filter to a passed in image.

✅ Again build all code and run wasm-bindgen to generate the JavaScript shim. If you are using the Makefile as above you can now run

make

Otherwise run the commands directly:

cargo build --release --target=wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/release/image_filter.wasm --out-dir app --target web --no-typescript

The JavaScript shim (image_filter.js) and the wasm file (image_filter_bg.wasm) in your app directory should be updated.


In the next chapter you will work on the other side of this application: First the HTML frontend and then the necessary JavaScript code to load and run the WebAssembly module.

HTML Frontend

Your image filter application needs some basic UI to allow a user to specify an image on disk and select the image filter to be applied.

✅ Start with a new file app/index.html with a basic HTML structure.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust Image filter</title>
  </head>
  <body>
  </body>
</html>

✅ To upload a picture the frontend needs a file selector, so add the following in between the <body> tags.

    <input type="file" id="files" name="file" accept="image/png, image/jpeg" />

The accept attribute limits what files a user can select. As this application is for images it's enough to limit it to PNG and JPEG files for now.

✅ Additionally the user should be able to select a filter. List out all available ones manually.

    <select name="filter">
      <option value="None">none</option>
      <option value="1977" selected>1977</option>
      <option value="Aden">Aden</option>
      <option value="Brannan">Brannan</option>
      <option value="Brooklyn">Brooklyn</option>
      <option value="Clarendon">Clarendon</option>
      <option value="Earlybird">Earlybird</option>
      <option value="Gingham">Gingham</option>
      <option value="Hudson">Hudson</option>
      <option value="Inkwell">Inkwell</option>
      <option value="Kelvin">Kelvin</option>
      <option value="Lark">Lark</option>
      <option value="Lofi">Lofi</option>
      <option value="Maven">Maven</option>
      <option value="Mayfair">Mayfair</option>
      <option value="Moon">Moon</option>
      <option value="Nashville">Nashville</option>
      <option value="Reyes">Reyes</option>
      <option value="Rise">Rise</option>
      <option value="Slumber">Slumber</option>
      <option value="Stinson">Stinson</option>
      <option value="Toaster">Toaster</option>
      <option value="Valencia">Valencia</option>
      <option value="Walden">Walden</option>
    </select>

In case of None no filter should be applied and the user should see the image they selected unchanged.

✅ To show that an upload is in progress add a <span> where you can show a message.

    <span></span>

✅ You also need a place to display the resulting image.

    <img />

✅ And last but not least include the JavaScript frontend code.

    <script type="module" src="app.js"></script>

The JavaScript file does not exist yet. You will create that in the next chapter.

✅ To ensure everything is working as expected for now serve the files over HTTP using http

cd app
http

Your application should be reachable at http://127.0.0.1:8000/. It should look something like this:


In the next chapter you will finally write the JavaScript code to load and run the WebAssembly module.

JavaScript

JavaScript is used to handle events from the HTML form and pass the data over to the WebAssembly module, which first needs to be loaded and instantiated of course.

✅ Start by creating an empty app/app.js file. This is where all the code will go now.

✅ You already have the JavaScript shim and the wasm file ready to go, so you can start by importing it.

import init, { apply_filter } from './image_filter.js';

✅ You need to call and await init to actually load, compile and instantiate the WebAssembly module. Once you have done that imported apply_filter will be a function you can call.

await init();

✅ Whenever the user selects an image or changes the filter you should load the image and apply the filter. Hook up the onchange events of both the file input and the select now.

document.querySelector('input[type=file]').onchange = (evt) => {
  imageFilter();
};
document.querySelector('select').onchange = (evt) => {
  imageFilter();
};

imageFilter will be the function that handles all the logic.

✅ But first add a small helper typedArrayToURL. JavaScript's TypedArray is an array-like view of a binary data buffer. Your converted image will be in such a buffer. To display that in the browser you need to turn it into an object url. It's enough to only handle the PNG image format.

function typedArrayToURL(typedArray) {
  return URL.createObjectURL(
    new Blob([typedArray.buffer], { type: "image/png" })
  );
}

✅ Now start writing your imageFilter function.

async function imageFilter() {
  // (to be filled in)
}

✅ You should start by checking for the file the user selected.

  let files = document.getElementById('files').files;
  if (!files.length) {
    return;
  }
  let file = files[0];

✅ This is a good time to let the user now that the application is working. You can also display the original image without a filter applied yet.

  let span = document.querySelector('span');
  span.innerText = "working...";

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);
  imgEl.width = "500";

✅ Next fetch the selected image filter name. If it's "none" you don't need to do any work!

  let filter = document.querySelector("select").value.toLowerCase();
  if (filter == "none") {
    span.innerText = "done.";
    return;
  }

✅ Reading the file to then pass it to your WebAssembly function requires to read it and turn it into an array buffer first. That's available directly on the file object you already have.

  let img = await file.arrayBuffer();

✅ The apply_filter function expects an array of u8 and the filter name as a string. To get that array from our img you can call new Uint8Array, passing your image data. A string is automatically handled by the wasm-bindgen shim.

  let result = apply_filter(new Uint8Array(img), filter);

And that is all you need to call a function in the WebAssembly module already.

✅ What's left to do is turn the image into a blob URL you can display and inform the user that the work is done.

  let blobUrl = typedArrayToURL(result);
  imgEl.src = blobUrl;
  imgEl.width = "500";
  span.innerText = "done.";

The next chapter will tell you again how to build and run the application locally.

Running it locally

✅ First build your Rust code to WebAssembly and run wasm-bindgen to generate the JavaScript shim. If you are using the Makefile you can now run:

make

Otherwise run the commands directly:

cargo build --release --target=wasm32-unknown-unknown
wasm-bindgen target/wasm32-unknown-unknown/release/image_filter.wasm --out-dir app --target web --no-typescript

You should find 2 additional files in the app directory: image_filter.js and image_filter_bg.wasm.

✅ Serve your application locally using http:

cd app
http

Your application should be reachable at http://127.0.0.1:8000/.

Play around with it, upload an image and change filters.

Final application

You should have this file tree layout:

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── app
│   ├── app.js
│   ├── image_filter.js       <-- generated file
│   ├── image_filter_bg.wasm  <-- generated file
│   └── index.html
└── src
    └── lib.rs

To recap your final Rust code should look something like this:

use std::io::Cursor;
use std::panic;
use wasm_bindgen::prelude::*;

use rustagram::image::io::Reader;
use rustagram::image::ImageOutputFormat;
use rustagram::RustagramFilter;

#[wasm_bindgen(start)]
pub fn main() {
    panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(log::Level::Debug).unwrap();
}

#[wasm_bindgen]
pub fn apply_filter(img: &[u8], filter: &str) -> Vec<u8> {
    log::debug!("image: {} bytes, filter: {:?}", img.len(), filter);

    let img = Reader::new(Cursor::new(img))
        .with_guessed_format()
        .unwrap()
        .decode()
        .unwrap();
    let filter_type = filter.parse().unwrap();
    let out = img.to_rgba8().apply_filter(filter_type);
    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), ImageOutputFormat::Png)
        .unwrap();

    bytes
}

The frontend in HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust Image filter</title>
  </head>
  <body>
    <input type="file" id="files" name="file" accept="image/png, image/jpeg" />
    <select name="filter">
      <option value="None">none</option>
      <option value="1977" selected>1977</option>
      <option value="Aden">Aden</option>
      <option value="Brannan">Brannan</option>
      <option value="Brooklyn">Brooklyn</option>
      <option value="Clarendon">Clarendon</option>
      <option value="Earlybird">Earlybird</option>
      <option value="Gingham">Gingham</option>
      <option value="Hudson">Hudson</option>
      <option value="Inkwell">Inkwell</option>
      <option value="Kelvin">Kelvin</option>
      <option value="Lark">Lark</option>
      <option value="Lofi">Lofi</option>
      <option value="Maven">Maven</option>
      <option value="Mayfair">Mayfair</option>
      <option value="Moon">Moon</option>
      <option value="Nashville">Nashville</option>
      <option value="Reyes">Reyes</option>
      <option value="Rise">Rise</option>
      <option value="Slumber">Slumber</option>
      <option value="Stinson">Stinson</option>
      <option value="Toaster">Toaster</option>
      <option value="Valencia">Valencia</option>
      <option value="Walden">Walden</option>
    </select>
    <span></span>
    <br>
    <img />
    <script type="module" src="app.js"></script>
  </body>
</html>

The JavaScript frontend code:

import init, { apply_filter } from './image_filter.js';

await init();

document.querySelector('input[type=file]').onchange = (evt) => {
  imageFilter();
};
document.querySelector('select').onchange = (evt) => {
  imageFilter();
};

function typedArrayToURL(typedArray) {
  return URL.createObjectURL(
    new Blob([typedArray.buffer], { type: "image/png" })
  );
}

async function imageFilter() {
  let files = document.getElementById('files').files;
  if (!files.length) {
    return;
  }
  let file = files[0];
  let span = document.querySelector('span');
  span.innerText = "working...";

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);
  imgEl.width = "500";

  let filter = document.querySelector("select").value.toLowerCase();
  if (filter == "none") {
    span.innerText = "done.";
    return;
  }

  let img = await file.arrayBuffer();
  let result = apply_filter(new Uint8Array(img), filter);
  let blobUrl = typedArrayToURL(result);
  imgEl.src = blobUrl;
  imgEl.width = "500";
  span.innerText = "done.";
}

A demo deployment is available at:

https://tmp.fnordig.de/wasm/image-filter/


Some ideas on what to do next:

  • The code unwraps a lot. Introduce some error handling. Can you return an error from your wasm module?

Edge

In this tutorial you'll get familiar with:

  • Building Rust code for Fastly's Compute@Edge platform
  • Handling and responding to a web request
  • A little bit of HTML and JavaScript for the frontend

We build a web API that applies a given filter to an image posted to it. It returns the produced image over HTTP. Additionally we also serve a bare-bones HTML form that allows us to use this API.

Application specification

Our image filter application provides 3 endpoints:

GET /

The HTML frontend to load, post and display images.

GET /app.js

The JavaScript frontend code to handle the logic of sending and displaying the image.

POST /image

The actual image filter API. It receives the image in the request body and returns the converted image.


In the next chapters we go through creating this project step by step.

New compute project

Initialize a new package locally using fastly

No Fastly account required for local development. For the manual way see below.

✅ Create a new compute project from a starter kit.

mkdir edge-image-filter
cd edge-image-filter
fastly compute init

Give it a name of your choice.
When asked for the language to use select "Rust".
When asked for the Starter kit, use "[5] Empty starter for Rust".

✅ Finally run the project locally

fastly compute serve

Your application should be reachable at http://127.0.0.1:7676/.


Initialize a new package locally.

The fastly CLI handles creation of a new package. It essentially does the below steps.

✅ Create a new project using cargo

cargo new edge-image-filter
cd edge-image-filter

✅ Add the fastly dependency

cargo add fastly

Alternatively add it to your Cargo.toml under [dependencies]:

fastly = "0.8.6"

✅ Add the scaffolding to src/main.rs:

use fastly::http::StatusCode;
use fastly::{Error, Request, Response};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    Ok(Response::from_status(StatusCode::OK))
}

✅ You also need a fastly.toml file with some configuration. Create that file and add this content:

authors = ["your@email.com"]
description = ""
language = "rust"
manifest_version = 2
name = "edge-image-filter"
service_id = ""

✅ Finally run the project locally

fastly compute serve

Your application should be reachable at http://127.0.0.1:7676/.

Handling requests

A Compute@Edge application follows a simple request-response model: The main function of the application receives a Request object as an argument, and produces a Response object or an Error.

✅ Write a basic handler that returns "Hello World" when / is accessed.

use fastly::http::{Method, StatusCode};
use fastly::{Error, Request, Response};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    match (req.get_method(), req.get_path()) {
        (&Method::GET, "/") => {
            Ok(Response::from_status(StatusCode::OK).with_body_text_plain("Hello World!\n"))
        }

        _ => Ok(Response::from_status(StatusCode::NOT_FOUND)
            .with_body_text_plain("The page you requested could not be found\n")),
    }
}

This uses the fastly crate. Documentation is available at docs.rs/fastly.

✅ Run the project locally:

fastly compute serve

Your application should be reachable at http://127.0.0.1:7676/.

Backend

You will now implement the actual logic of this API: the image filter.

✅ Start of with a new import at the top of your src/main.rs file. You later need to specify the image's mime type:

use fastly::mime;

✅ In your main function match for a POST request on the /image path and call a handler function.

match (req.get_method(), req.get_path()) {
    // (cut)

    (&Method::POST, "/image") => convert_image(req),

✅ Create this new handler function, taking in the request and returning a response or an error.

pub fn convert_image(mut req: Request) -> Result<Response, Error> {
    // (to be filled in)
}

✅ Next you need to get the required data from the request. Start with the filter name from the query.

    let filter_str = req.get_query_parameter("filter").unwrap();
    let filter = filter_str.parse().unwrap();

✅ Now you can check and read the body from the request.

    if !req.has_body() {
        return Ok(
            Response::from_status(StatusCode::BAD_REQUEST)
                .with_body_text_plain("missing image")
        );
    }

    let body = req.take_body();
    let body = body.into_bytes();

✅ You can decode the image data using the image crate, which is re-exported from rustagram. The documentation is available at docs.rs/image.

Import the modules using the following lines on the top of your src/main.rs file.

use rustagram::image;
use rustagram::image::io::Reader;
use rustagram::RustagramFilter;

✅ Now use the Reader type to load the image from the buffer.

    let img = Reader::new(Cursor::new(body))
        .with_guessed_format()
        .unwrap();

    let img = img.decode().unwrap();

✅ Currently Fastly enforces very small resource limits (memory usage, computation time), so you need to limit the work the application does if you want to deploy it. The easiest is to scale down the image before applying an image filter.

    let img = img.thumbnail(500, 500);

Locally you can skip this if you want. Larger images just take longer to process.

✅ Now that you have the image and a filter you can apply this filter as before. Instead of writing the result to a file it should be written to a buffer in PNG format.

    let out = img.to_rgba8().apply_filter(filter);
    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Png)?;

✅ The buffer containing the final image can now be returned as the response. Don't forget to set the correct content type.

    Ok(Response::from_status(StatusCode::OK)
        .with_body(bytes)
        .with_content_type(mime::IMAGE_PNG))

✅ Run the project locally:

fastly compute serve

Your application should be reachable at http://127.0.0.1:7676/.

✅ In another terminal you can use curl to send an image and save the converted file.

curl http://127.0.0.1:7676/image?filter=valencia -X POST -H "Content-Type: application/octet-stream" -T skyline.jpg -o result.png

In the next chapter you learn how to build a small web frontend and serve that along your image filter application.

HTML Frontend

If your image filter API is working now you can already use that using curl from the command line. To make it easier to use and test you will now build a small web frontend and serve that along the API.

Note: The HTML used here is the same that was used in the previous tutorial.

✅ In your main function match / and /app.js and serve the respective files. To simplify deployment you can embed the files directly into the binary using include_str!.

match (req.get_method(), req.get_path()) {
    (&Method::GET, "/") => Ok(Response::from_status(StatusCode::OK)
        .with_content_type(mime::TEXT_HTML_UTF_8)
        .with_body(include_str!("index.html"))),

    (&Method::GET, "/app.js") => Ok(Response::from_status(StatusCode::OK)
        .with_content_type(mime::APPLICATION_JAVASCRIPT)
        .with_body(include_str!("app.js"))),

    // (cut)
}

✅ Create a src/index.html file with a basic HTML structure.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust WASM Demo</title>
  </head>
  <body>
  </body>
</html>

✅ To upload a picture the frontend needs a file selector, so add the following in between the <body> tags.

    <input type="file" id="files" name="file" accept="image/png, image/jpeg" />

✅ Additionally the user should be able to select a filter. List out all available ones manually.

    <select name="filter">
      <option value="None">none</option>
      <option value="1977" selected>1977</option>
      <option value="Aden">Aden</option>
      <option value="Brannan">Brannan</option>
      <option value="Brooklyn">Brooklyn</option>
      <option value="Clarendon">Clarendon</option>
      <option value="Earlybird">Earlybird</option>
      <option value="Gingham">Gingham</option>
      <option value="Hudson">Hudson</option>
      <option value="Inkwell">Inkwell</option>
      <option value="Kelvin">Kelvin</option>
      <option value="Lark">Lark</option>
      <option value="Lofi">Lofi</option>
      <option value="Maven">Maven</option>
      <option value="Mayfair">Mayfair</option>
      <option value="Moon">Moon</option>
      <option value="Nashville">Nashville</option>
      <option value="Reyes">Reyes</option>
      <option value="Rise">Rise</option>
      <option value="Slumber">Slumber</option>
      <option value="Stinson">Stinson</option>
      <option value="Toaster">Toaster</option>
      <option value="Valencia">Valencia</option>
      <option value="Walden">Walden</option>
    </select>

✅ To show that an upload is in progress add a <span> where you can show a message.

    <span></span>

✅ You also need a place to display the resulting image.

    <img />

✅ And last but not least include the JavaScript frontend code.

    <script src="app.js"></script>

The next chapter will guide you through writing the JavaScript frontend code.

JavaScript

In the previous chapter you already created a handler in your application returning an app.js file and also referenced that in your HTML code. Time to write the JavaScript code now.

The plan is to:

  • load the image data from the selected file
  • post this image data with the selected filter name to the backend
  • display the resulting file on the web page

Note: A lot of this JavaScript code is similar to the one from the Web tutorial. The important difference is in the last step where instead of calling into WebAssembly you send the image to a server.

✅ If there's a change on the file selector ("the user selected a file") or a new filter is selected you should send the image to the backend.

document.querySelector('input[type=file]').onchange = (evt) => {
  postImage();
};
document.querySelector('select').onchange = (evt) => {
  postImage();
};

✅ The above calls a new function.

async function postImage() {
  // (to be filled in)
}

✅ First grab the selected file and let the user know the application is working.

  let files = document.getElementById('files').files;
  if (!files.length) {
    return;
  }
  let file = files[0];
  let span = document.querySelector('span');
  span.innerText = "working...";

✅ Start by displaying the image. The JavaScript web API lets you turn the file object into an object URL that can be displayed.

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);
  imgEl.width = "500";

✅ Next fetch the selected image filter name. If it's none you don't need to do any work!

  let span = document.querySelector('span');
  span.innerText = "working...";

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);

✅ Reading the file to then submit it requires to read it and turn it into an array buffer first. That's available directly on the file object you already have.

  let img = await file.arrayBuffer();

✅ Now you can create a POST request to the /image API endpoint using the fetch API, using the image data as the body.

  let url = `/image?filter=${filter}`;
  let response = await fetch(url, {
    method: "POST",
    body: img,
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

✅ The response can be turned back into an object URL, that you can then display again as before.

  let blob = await response.blob();
  imgEl.src = URL.createObjectURL(blob);
  span.innerText = "done.";

And that's it for the frontend. Next you can run the full application locally.

Running it locally

✅ Use fastly to serve the application locally.

fastly compute serve

Your application should be reachable at http://127.0.0.1:7676/.

Play around with it, upload an image and change filters.

Final application

You should have this file tree layout:

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── fastly.toml
└── src
    ├── app.js
    ├── index.html
    └── main.rs

To recap your final Rust code should look something like this:

use std::io::Cursor;

use fastly::http::{Method, StatusCode};
use fastly::{mime, Error, Request, Response};
use rustagram::image;
use rustagram::image::io::Reader;
use rustagram::RustagramFilter;

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    // Pattern match on the path...
    match (req.get_method(), req.get_path()) {
        // If request is to the `/` path...
        (&Method::GET, "/") => Ok(Response::from_status(StatusCode::OK)
            .with_content_type(mime::TEXT_HTML_UTF_8)
            .with_body(include_str!("index.html"))),
        (&Method::GET, "/app.js") => Ok(Response::from_status(StatusCode::OK)
            .with_content_type(mime::APPLICATION_JAVASCRIPT)
            .with_body(include_str!("app.js"))),

        (&Method::POST, "/image") => convert_image(req),

        // Catch all other requests and return a 404.
        _ => Ok(Response::from_status(StatusCode::NOT_FOUND)
            .with_body_text_plain("The page you requested could not be found\n")),
    }
}

pub fn convert_image(mut req: Request) -> Result<Response, Error> {
    let filter_str = req.get_query_parameter("filter").unwrap();
    let filter = filter_str.parse().unwrap();

    if !req.has_body() {
        return Ok(
            Response::from_status(StatusCode::BAD_REQUEST)
                .with_body_text_plain("missing image")
        );
    }

    let body = req.take_body();
    let body = body.into_bytes();

    let img = Reader::new(Cursor::new(body))
        .with_guessed_format()
        .unwrap();

    let img = img.decode().unwrap();

    let img = img.thumbnail(500, 500);
    let out = img.to_rgba8().apply_filter(filter);
    let mut bytes: Vec<u8> = Vec::new();
    out.write_to(&mut Cursor::new(&mut bytes), image::ImageOutputFormat::Png)?;

    Ok(Response::from_status(StatusCode::OK)
        .with_body(bytes)
        .with_content_type(mime::IMAGE_PNG))
}

The frontend in HTML:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Rust WASM Demo</title>
  </head>
  <body>
    <input type="file" id="files" name="file" accept="image/png, image/jpeg" />
    <select name="filter">
      <option value="None">none</option>
      <option value="1977" selected>1977</option>
      <option value="Aden">Aden</option>
      <option value="Brannan">Brannan</option>
      <option value="Brooklyn">Brooklyn</option>
      <option value="Clarendon">Clarendon</option>
      <option value="Earlybird">Earlybird</option>
      <option value="Gingham">Gingham</option>
      <option value="Hudson">Hudson</option>
      <option value="Inkwell">Inkwell</option>
      <option value="Kelvin">Kelvin</option>
      <option value="Lark">Lark</option>
      <option value="Lofi">Lofi</option>
      <option value="Maven">Maven</option>
      <option value="Mayfair">Mayfair</option>
      <option value="Moon">Moon</option>
      <option value="Nashville">Nashville</option>
      <option value="Reyes">Reyes</option>
      <option value="Rise">Rise</option>
      <option value="Slumber">Slumber</option>
      <option value="Stinson">Stinson</option>
      <option value="Toaster">Toaster</option>
      <option value="Valencia">Valencia</option>
      <option value="Walden">Walden</option>
    </select>
    <span></span>
    <br>
    <img />
    <script src="app.js"></script>
  </body>
</html>

And the JavaScript frontend code:

document.querySelector('input[type=file]').onchange = (evt) => {
  postImage();
};
document.querySelector('select').onchange = (evt) => {
  postImage();
};

async function postImage() {
  let files = document.getElementById('files').files;
  if (!files.length) {
    return;
  }
  let file = files[0];
  let span = document.querySelector('span');
  span.innerText = "working...";

  let imgEl = document.querySelector('img');
  imgEl.src = URL.createObjectURL(file);
  imgEl.width = "500";

  let filter = document.querySelector("select").value.toLowerCase();
  if (filter == "none") {
    span.innerText = "done.";
    return;
  }

  let img = await file.arrayBuffer();
  let url = `/image?filter=${filter}`;
  let response = await fetch(url, {
    method: "POST",
    body: img,
  });

  if (!response.ok) {
    throw new Error(`HTTP error! Status: ${response.status}`);
  }

  let blob = await response.blob();
  imgEl.src = URL.createObjectURL(blob);
  span.innerText = "done.";
}

You can build and serve your application locally like this:

fastly compute serve

Some ideas on what to do next:

  • Did you even notice that this was compiled to WebAssembly?
  • What happens if you compile it natively?
  • Can you return different image formats? Different sizes?
  • What other task could be suitable for edge computing?

Optional: Deployment

So far the image filter application has been running locally only. Of course this can now be deployed to Fastly's servers.

Note: This requires a Fastly account. You can create an account for free.

✅ You can now deploy this application:

fastly compute deploy

The first time you run this it will ask you if you want to create a new service. Follow the instructions, give it a name, define a domain to use (or use the suggested one). You don't need to define any backends.

Subsequent runs will deploy your code as a new version.

When finished this will print the full URL of your new service. A demo deployment is available at:

https://forcibly-advanced-eft.edgecompute.app/

References