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 -->