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