🚀 Check it out! 🚀

Previously, I’ve built loiccoyle/phomo a python package to construct photo mosaics, complete with GPU acceleration.

I’ve written about it here.

As an educational project, to improve my rust skills and learn how to use WebAssembly, I’ve built loiccoyle/phomo-rs. It’s a rust port of phomo complete with a binary crate and Wasm bindings. As well as an accompanying web app.

The rusty part

There isn’t much point delving into the inner workings of the rust library itself, as it is essentially a direct port of the phomo python package I’ve previously built. If you’re curious about it, you can read about it.

The web app

What is more interesting is the web app part, and how to use go from Rust, to Wasm, to using it in the browser.

WASM is it?

WebAssembly or Wasm is a low-level, portable binary instruction format designed to run code efficiently across different platforms. It enables high-performance execution of code in web browsers and other environments by allowing developers to compile code written in languages like C, C++, or Rust into a compact binary that can run natively on the browser’s virtual machine. Achieving near native performance with the flexibility of the web.

WASM speed, engage!

Unlike JavaScript, which is parsed and JIT compiled at runtime by the js engine, Wasm code is compiled ahead of time into low-level machine instructions, allowing the compilation process to do more intensive optimizations, reducing execution overhead and increased performance. It is executed by the Wasm virtual machine built into most modern browsers, following the Wasm spec. Wasm also offers more efficient memory management, avoiding the performance costs associated with JavaScript’s dynamic typing and garbage collection systems.

Rust to Wasm

There are number of great resources, explaining how to compile rust to Wasm.

The Rust and WebAssembly book is invaluable.

So I’ll just give an exceedingly brief explainer of the steps required to add Wasm bindings to an existing rust library.

First initialize a new crate:

cargo init --lib mycrate-wasm

Open up its Cargo.toml and add the following:

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

And add the wasm-bindgen dependency with cargo add wasm-bindgen

This tells the compiler to produce a dynamic system library, which can be loaded from another language and built into a Wasm binary.

Next you’ll need to install wasm-pack, its the go to for building Wasm binaries and packaging them into Node.js packages.

Check out the wasm-pack docs!

To install it, just run cargo install wasm-pack.

So all the tooling is installed, next you just need to write your bindings. I’ll use my phomo-wasm package as an example. I’ll walk through the lib.rs file.

First, there is some boilerplate, to enable the log crate to log to the web console and have better stack traces on panics in Rust land, courtesy of console_error_panic_hook.

// ...
use wasm_bindgen::prelude::*;
extern crate wasm_logger;

#[wasm_bindgen(start)]
pub fn init_panic_hook() {
    wasm_logger::init(wasm_logger::Config::default());
    console_error_panic_hook::set_once();
}

To add bindings to a struct, I like to create a wrapper struct with the wasm_bindgen derive macro which wraps the rust struct contained in its inner field. Here I’m wrapping the phomo::Mosaic struct imported as MosaicRs:

// ...
#[wasm_bindgen]
pub struct Mosaic {
    inner: MosaicRs,
}

#[wasm_bindgen]
impl Mosaic {
    #[wasm_bindgen(constructor)]
    pub fn new(
        master_img_data: &[u8],
        tile_imgs_data: js_sys::Array,
        grid_width: u32,
        grid_height: u32,
        tile_resize: Option<ResizeType>,
    ) -> Result<Mosaic, JsValue> {
// ...

Notice the Err in the Result nomad, is this mysterious JsValue, which serves as an intermediary between Rust and JavaScript owned values. wasm-bindgen requires the Err type to implement Into<JsValue>. So all Err values need a bit of help to play well with JsValue, for example:

#[wasm_bindgen(js_name = transferTilesToMaster)]
pub fn transfer_tiles_to_master(&mut self) -> Result<(), JsValue> {
    self.inner.master = MasterRs::from_image(
        self.inner.master.img.match_palette(&self.inner.tiles),
        self.inner.grid_size,
    )
    .map_err(|err| JsValue::from(err.to_string()))?;
    Ok(())

The rest is relatively self-explanatory.

To build the Wasm binary and construct the Node.js packages, just run wasm-pack build. The Node.js package will be built to the pkg/ directory and can be published to the npm registry with npm publish pkg/.

Wasm in Web App

For the phomo web app, source code available here, I’m using the Vite build tool. Which requires some extra configuration to use Wasm. I’m assuming a basic VIte project is already set up.

Let’s go through it, first let’s install our Wasm package:

npm add phomo-wasm

Then we need to add the Wasm and top level await plugins:

npm add vite-plugin-wasm vite-plugin-top-level-await

In my case, I’m using react so I also need the @vitejs/plugin-react package.

And configure Vite to use it, edit the vite.config.js file:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";

// https://vitejs.dev/config/
export default defineConfig({
  // ...
  plugins: [react(), wasm(), topLevelAwait()],
  worker: { plugins: () => [wasm(), topLevelAwait()], format: "es" },
});

With that done we can seamlessly use our Wasm package in the web app and even in web workers.