Previous example: <-- A Simple Paint Program

web-sys: Wasm in Web Worker

A simple example of parallel execution by spawning a web worker with web_sys, loading Wasm code in the web worker and interacting between the main thread and the worker.
_ wasm-bindgen Guide{target="_blank"}

web-sys: Wasm in Web Worker{target="_blank"}

setup the project

cargo new wasm-in-web-worker --lib
cd wasm-in-web-worker
mkdir -p www/js www/html www/css
cargo add wasm-bindgen console_error_panic_hook --optional
  • Edit Cargo.toml

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

[dependencies]
console_error_panic_hook = { version = "0.1.7", optional = true }
wasm-bindgen = "0.2.89"

[dependencies.web-sys]
version = "0.3.66"
features = [
    'console',
    'Document',
    'HtmlElement',
    'HtmlInputElement',
    'MessageEvent',
    'Window',
    'Worker',
]

The code

  • index.html
<!doctype html>
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <title>Wasm in Web Worker: nobundle</title>
    <link rel="stylesheet" href="../css/style.css">
  </head>
  <body>
<div id="wrapper">
        <h1>Main Thread/Wasm Web Worker Interaction</h1>

        <label for="inputNumber">Input <b>numbers</b> to verify if it is odd or even</label>

        <input type="text" id="inputNumber" name="inputNumber">

        <div id="resultField"></div>
    </div>

    <!-- Make `wasm_bindgen` available for `index.js` -->
    <script src='../pkg/wasm_in_web_worker.js'></script>
    
    <!-- Note that there is no `type="module"` in the script tag -->
    <script src="../index.js"></script>
  </body>
</html>
  • style.css
body {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: "Century Gothic", CenturyGothic, Geneva, AppleGothic, sans-serif; 
    color: white;
    background-color: black;
}

#wrapper {
    width: 50%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

#inputNumber {
    text-align: center;
}

#resultField {
    text-align: center;
    height: 1em;
    padding-top: 0.2em;
}
  • index.js
// We only need `startup` here which is the main entry point
// In theory, we could also use all other functions/struct types from Rust which we have bound with
// `#[wasm_bindgen]`

const {startup} = wasm_bindgen;

async function run_wasm() {
    // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`
    // `wasm_bindgen` was imported in `index.html`
    await wasm_bindgen();

    console.log('index.js loaded');

    // Run main Wasm entry point
    // This will create a worker from within our Rust code compiled to Wasm
    startup();
}

run_wasm();

  • worker.js
// The worker has its own scope and no direct access to functions/objects of the
// global scope. We import the generated JS file to make `wasm_bindgen`
// available which we need to initialize our Wasm code.
importScripts('../pkg/wasm_in_web_worker.js');

console.log('Initializing worker')

// In the worker, we have a different struct that we want to use as in
// `index.js`.
const {NumberEval} = wasm_bindgen;

async function init_wasm_in_worker() {
    // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`.
    await wasm_bindgen('../pkg/wasm_in_web_worker_bg.wasm');

    // Create a new object of the `NumberEval` struct.
    var num_eval = NumberEval.new();

    // Set callback to handle messages passed to the worker.
    self.onmessage = async event => {
        // By using methods of a struct as reaction to messages passed to the
        // worker, we can preserve our state between messages.
        var worker_result = num_eval.is_even(event.data);

        // Send response back to be handled by callback in main thread.
        self.postMessage(worker_result);
    };
};

init_wasm_in_worker();
  • Rust side
#![allow(unused)]
fn main() {
// src/lib.rs
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use web_sys::{console, HtmlElement, HtmlInputElement, MessageEvent, Worker};

/// A number evaluation struct
///
/// This struct will be the main object which responds to messages passed to the
/// worker. It stores the last number which it was passed to have a state. The
/// statefulness is not is not required in this example but should show how
/// larger, more complex scenarios with statefulness can be set up.
#[wasm_bindgen]
pub struct NumberEval {
    number: i32,
}

#[wasm_bindgen]
impl NumberEval {
    /// Create new instance.
    pub fn new() -> NumberEval {
        NumberEval { number: 0 }
    }

    /// Check if a number is even and store it as last processed number.
    ///
    /// # Arguments
    ///
    /// * `number` - The number to be checked for being even/odd.
    pub fn is_even(&mut self, number: i32) -> bool {
        self.number = number;
        self.number % 2 == 0
    }

    /// Get last number that was checked - this method is added to work with
    /// statefulness.
    pub fn get_last_number(&self) -> i32 {
        self.number
    }
}

/// Run entry point for the main thread.
#[wasm_bindgen]
pub fn startup() {
    // Here, we create our worker. In a larger app, multiple callbacks should be
    // able to interact with the code in the worker. Therefore, we wrap it in
    // `Rc<RefCell>` following the interior mutability pattern. Here, it would
    // not be needed but we include the wrapping anyway as example.
    // to avoid cross-site error on the local server we don't use relative path `./js/workewr.js`
    let worker_handle = Rc::new(RefCell::new(Worker::new("http://localhost:8000/js/worker.js").unwrap()));
    console::log_1(&"Created a new worker from within Wasm".into());

    // Pass the worker to the function which sets up the `oninput` callback.
    setup_input_oninput_callback(worker_handle);
}

fn setup_input_oninput_callback(worker: Rc<RefCell<web_sys::Worker>>) {
    let document = web_sys::window().unwrap().document().unwrap();

    // If our `onmessage` callback should stay valid after exiting from the
    // `oninput` closure scope, we need to either forget it (so it is not
    // destroyed) or store it somewhere. To avoid leaking memory every time we
    // want to receive a response from the worker, we move a handle into the
    // `oninput` closure to which we will always attach the last `onmessage`
    // callback. The initial value will not be used and we silence the warning.
    #[allow(unused_assignments)]
    let mut persistent_callback_handle = get_on_msg_callback();

    let callback = Closure::new(move || {
        console::log_1(&"oninput callback triggered".into());
        let document = web_sys::window().unwrap().document().unwrap();

        let input_field = document
            .get_element_by_id("inputNumber")
            .expect("#inputNumber should exist");
        let input_field = input_field
            .dyn_ref::<HtmlInputElement>()
            .expect("#inputNumber should be a HtmlInputElement");

        // If the value in the field can be parsed to a `i32`, send it to the
        // worker. Otherwise clear the result field.
        match input_field.value().parse::<i32>() {
            Ok(number) => {
                // Access worker behind shared handle, following the interior
                // mutability pattern.
                let worker_handle = &*worker.borrow();
                let _ = worker_handle.post_message(&number.into());
                persistent_callback_handle = get_on_msg_callback();

                // Since the worker returns the message asynchronously, we
                // attach a callback to be triggered when the worker returns.
                worker_handle
                    .set_onmessage(Some(persistent_callback_handle.as_ref().unchecked_ref()));
            }
            Err(_) => {
                document
                    .get_element_by_id("resultField")
                    .expect("#resultField should exist")
                    .dyn_ref::<HtmlElement>()
                    .expect("#resultField should be a HtmlInputElement")
                    .set_inner_text("");
            }
        }
    });

    // Attach the closure as `oninput` callback to the input field.
    document
        .get_element_by_id("inputNumber")
        .expect("#inputNumber should exist")
        .dyn_ref::<HtmlInputElement>()
        .expect("#inputNumber should be a HtmlInputElement")
        .set_oninput(Some(callback.as_ref().unchecked_ref()));

    // Leaks memory.
    callback.forget();
}

/// Create a closure to act on the message returned by the worker
fn get_on_msg_callback() -> Closure<dyn FnMut(MessageEvent)> {
    Closure::new(move |event: MessageEvent| {
        console::log_2(&"Received response: ".into(), &event.data());

        let result = match event.data().as_bool().unwrap() {
            true => "even",
            false => "odd",
        };

        let document = web_sys::window().unwrap().document().unwrap();
        document
            .get_element_by_id("resultField")
            .expect("#resultField should exist")
            .dyn_ref::<HtmlElement>()
            .expect("#resultField should be a HtmlInputElement")
            .set_inner_text(result);
    })
}

}

build and serve

This example requires to not create ES modules, therefore we pass the flag --target no-modules

wasm-pack build --target no-modules --no-typescript --out-dir www/pkg

http www

open index.html

firefox http://localhost:8000/html/

NOTE:

You may need to Enable SharedArrayBuffers in Firefox

In early 2018, the Spectre and Meltdown vulnerabilities were discovered. In response, browser manufacturers disabled SharedArrayBuffer by default.

  • Open Firefox.
  • Navigate to about:config.
  • Click I accept the risk!
  • Search for shared.
  • Double-click javascript.options.shared_memory.
  • This option should now have the value true:

What's next?

Next example: threads: Parallel Raytracing -->