Building and loading WASM

WASM builds have some requirements. For example, you need to define the library type as cdylib.

It's probably easiest to look at a working Cargo.toml:

[package]
name = "building_and_loading_wasm"
version = "0.1.0"
authors = ["Geoffrey Irons <sdfgeoff@gmail.com>"]
edition = "2018"

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

[dependencies]
wasm-bindgen="0.2.63"
js-sys="0.3.40"

[dependencies.web-sys]
version = "0.3.4"
features = []

Rust has a great tool called wasm-pack which makes the process of building and deploying WASM code nice and simple. It's designed to work with bundlers, but to avoid the gigabyte of dependencies that webpack pulls in, I decided to go for the simplest output type: "web".

My invocation of wasm-pack is:

wasm-pack build --out-dir $(OUT_DIR) --target web --dev
# OR
wasm-pack build --out-dir $(OUT_DIR) --target web --release

When invoked, this will create a bunch of files: core_bg.wasm, core.js, core_bg.d.ts, core.d.ts and package.json. The only files we need are core_bg.wasm (the actual webassembly) and core.js (code that loads the WASM).

Now you need to load it from HTML/js. For all the examples in this book, loading is an invocation of the function:

"use strict"

function load(canvas, module_path, options) {
    console.log("Loading", module_path)
    canvas.className = "loading"
    
    import(module_path)
    .then((module) => {
        module.default().then(function(obj){
            let core = new module.Core(canvas.id, options)
            core.start()
            canvas.core = core
        }).catch(function(e){
            console.error("Failed to init module:", e)
            canvas.className = "error"
        })
    }).catch(function(e) {
        console.error("Failed to load:", e)
        canvas.className = "error"
    });
}

function setup_canvas() {
    const canvases = document.querySelectorAll("canvas");
    for (let canvas of canvases) {
        let options = canvas.getAttribute("options") || ""
        let id = canvas.id.split("-")[0] // So we can have multiple canvas' with the same app and different options
        let module_path = './' + id + '/pkg/'+ id +'.js' // Path to WASM JS bindings
        canvas.tabIndex = 1
        canvas.addEventListener("click", function() {
            load(canvas, module_path, options)
        }, {'once':true})
    }
}
setup_canvas()

using an element like:

<canvas id="building_and_loading_wasm"></canvas>

A very simple rust webassembly program looks like:


#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::wasm_bindgen;

// Pull in the console.log function so we can debug things more easily
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

// This struct will be accessible from JS as a JS object that can be
// created using `new Core()`
#[wasm_bindgen]
pub struct Core {}

#[wasm_bindgen]
impl Core {
    #[wasm_bindgen(constructor)]
    pub fn new(canvas_id: String) -> Self {
        log(&format!("WASM Started for canvas {}", canvas_id));
        Self {}
    }

    #[wasm_bindgen]
    pub fn start(&mut self) {
        log("App Started");
    }
}
}

All up this creates:

You'll notice when you click on it plays a loading animation. That's done in in CSS. Normally this would get cancelled from inside the WASM binary, but this example doesn't.

To check if this example is working, you have to look at the browser console. You should see something like:

Loading ../games/trivial/core.js
WASM Started for canvas trivial
App Started

The first message comes from the javascript. The other two come from the WASM. The message will only appear once, as the javascript prevents the WAS loading twice.