February 15, 2021

A Rust / WebAssembly setup

This is a very short walk-through to get a Wasm Rust project up and running. No intention is put on giving an in-depth overview or a thorough list of references. The best more detailed guide is the Rust and WebAssembly “book”, in particular the Game of Life tutorial. Helpful is also the wasm-bindgen documentation.

I played around with various setup options and for the time being I liked this one as this integrates with webpack, provides a life reload workflow and makes JS / Rust interop easy.

This has been tested on 2021-02-15 with macOS 11.1 and Rust stable 1.50. I will likely update this over time.

Prerequisites

The setup uses wasm-pack and the webpack wasm-pack-plugin via node.js. In order to follow the steps below you will need:

  1. Wasm-pack, to install: https://rustwasm.github.io/wasm-pack/installer/
  2. Node.js, to install either use the official download, your favorite package manager, or nvm.

Creating the project

The source code of the hello-wasm project assembled here can be found on github.

  1. Create the project folder: cargo new hello-wasm --lib
  2. Your Cargo.toml should look like
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2018"

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

[dependencies]
console_error_panic_hook = "0.1.6" # log panics
console_log = "0.2.0"              # connects log to the js console
js-sys = "0.3.47"                  # bindings to js default objects
log = "0.4.14"                     # logging
wasm-bindgen = "0.2.69"            # js - rust interop

# bindings to DOM objects
[dependencies.web-sys]
version = "0.3.47"
features = ['console', 'Window', 'Document', 'Element', 'HtmlElement']
  1. Create a subdirectory static/ that will contain files that serve as the entry point to webpack and to your app.
  2. Create static/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
  1. Create static/index.js. This file will eventually load files from the static/wasm/ subdirectory which is where the compiled wasm code will be placed into.
import("./wasm").catch(console.error);
  1. Crate a webpack.config.js file in the project root directory
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  plugins: [
    new HtmlWebpackPlugin({ template: "index.html", title: "hello-wasm" }),
    new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, "."), outDir: "static/wasm" }),
    // needed for MS Edge support
    new webpack.ProvidePlugin({
      TextDecoder: ["text-encoding", "TextDecoder"],
      TextEncoder: ["text-encoding", "TextEncoder"],
    }),
  ],
};
  1. Create a package.json file
{
  "scripts": {
    "build": "webpack --mode production --context static",
    "serve": "webpack-dev-server --host 0.0.0.0 --context static",
    "clean": "npx rimraf target static/wasm"
  }
}
  1. Install the npm dependencies. Note that because of some SNAFU we need an older version of webpack right now. JavaScript churn in action.
$ npm install --save-dev \
    @wasm-tool/wasm-pack-plugin \
    html-webpack-plugin@4 \
    text-encoding \
    webpack@4 \
    webpack-cli@3 \
    webpack-dev-server
  1. Lastly, let’s use some Rust code that will use the DOM bindings to modify an element:
use log::{info, Level};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Document, HtmlElement, Window};

pub fn window() -> Window {
    web_sys::window().expect("no global `window` exists")
}

pub fn document() -> Document {
    window().document().expect("no global `document` exists")
}

#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
    console_error_panic_hook::set_once();
    console_log::init_with_level(Level::Debug).expect("init logger");

    info!("Up and running");

    let app = document()
        .query_selector("#app")
        .expect("querySelector call")
        .expect("cannot find app element")
        .dyn_into::<HtmlElement>().expect("app HtmlElement cast");
    app.set_inner_text("Hello from Rust!");

    Ok(())
}

The DOM bindings are provided by the web-sys crate. Note that there is a large set of features that provide control over what DOM bindings will be provided. Compare that to the features added to Cargo.toml in step 2.

  1. Run the development server: npm run serve. At localhost:8080 you should now see:

When modifying the Rust code you should immediately see the page reload. This allows for a very nice development flow.

  1. To package static files for “production” run npm run build. This will compile the Rust code in release mode and minify the JavaScript. It produces files in the dist/ subdirectory. You can take these and server some from a HTTP server like nginx.

© Robert Krahn 2021