Skip to content

Commit 78a3721

Browse files
authored
Add wasm in web worker example (#2556)
This commit adds a parallel execution example in which we spawn a web worker with `web_sys`, run WASM code in the web worker and interact between the main thread and the web worker. It is intended to add an easy starting point for parallel execution with WASM, complementing the more involved parallel raytrace example. Related to GitHub issue #2549 Co-authored-by: Simon Gasse <[email protected]>
1 parent 27c7a4d commit 78a3721

File tree

11 files changed

+349
-1
lines changed

11 files changed

+349
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ members = [
8080
"examples/todomvc",
8181
"examples/wasm-in-wasm",
8282
"examples/wasm-in-wasm-imports",
83+
"examples/wasm-in-web-worker",
8384
"examples/wasm2js",
8485
"examples/webaudio",
8586
"examples/webgl",

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ jobs:
178178
- script: mv _package.json package.json && npm install && rm package.json
179179
displayName: "run npm install"
180180
- script: |
181-
for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler | grep -v websockets | grep -v webxr | grep -v deno`; do
181+
for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler | grep -v wasm-in-web-worker | grep -v websockets | grep -v webxr | grep -v deno`; do
182182
(cd examples/$dir &&
183183
ln -fs ../../node_modules . &&
184184
npm run build -- --output-path $BUILD_ARTIFACTSTAGINGDIRECTORY/exbuild/$dir) || exit 1;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "wasm-in-web-worker"
3+
version = "0.1.0"
4+
authors = ["The wasm-bindgen Developers"]
5+
edition = "2018"
6+
7+
[lib]
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
wasm-bindgen = "0.2.74"
12+
console_error_panic_hook = { version = "0.1.6", optional = true }
13+
14+
[dependencies.web-sys]
15+
version = "0.3.4"
16+
features = [
17+
'console',
18+
'Document',
19+
'HtmlElement',
20+
'HtmlInputElement',
21+
'MessageEvent',
22+
'Window',
23+
'Worker',
24+
]

examples/wasm-in-web-worker/build.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
set -ex
4+
5+
# This example requires to *not* create ES modules, therefore we pass the flag
6+
# `--target no-modules`
7+
wasm-pack build --out-dir www/pkg --target no-modules
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use std::cell::RefCell;
2+
use std::rc::Rc;
3+
use wasm_bindgen::prelude::*;
4+
use wasm_bindgen::JsCast;
5+
use web_sys::{console, HtmlElement, HtmlInputElement, MessageEvent, Worker};
6+
7+
/// A number evaluation struct
8+
///
9+
/// This struct will be the main object which responds to messages passed to the
10+
/// worker. It stores the last number which it was passed to have a state. The
11+
/// statefulness is not is not required in this example but should show how
12+
/// larger, more complex scenarios with statefulness can be set up.
13+
#[wasm_bindgen]
14+
pub struct NumberEval {
15+
number: i32,
16+
}
17+
18+
#[wasm_bindgen]
19+
impl NumberEval {
20+
/// Create new instance.
21+
pub fn new() -> NumberEval {
22+
NumberEval { number: 0 }
23+
}
24+
25+
/// Check if a number is even and store it as last processed number.
26+
///
27+
/// # Arguments
28+
///
29+
/// * `number` - The number to be checked for being even/odd.
30+
pub fn is_even(&mut self, number: i32) -> bool {
31+
self.number = number;
32+
match self.number % 2 {
33+
0 => true,
34+
_ => false,
35+
}
36+
}
37+
38+
/// Get last number that was checked - this method is added to work with
39+
/// statefulness.
40+
pub fn get_last_number(&self) -> i32 {
41+
self.number
42+
}
43+
}
44+
45+
/// Run entry point for the main thread.
46+
#[wasm_bindgen]
47+
pub fn startup() {
48+
// Here, we create our worker. In a larger app, multiple callbacks should be
49+
// able to interact with the code in the worker. Therefore, we wrap it in
50+
// `Rc<RefCell>` following the interior mutability pattern. Here, it would
51+
// not be needed but we include the wrapping anyway as example.
52+
let worker_handle = Rc::new(RefCell::new(Worker::new("./worker.js").unwrap()));
53+
console::log_1(&"Created a new worker from within WASM".into());
54+
55+
// Pass the worker to the function which sets up the `oninput` callback.
56+
setup_input_oninput_callback(worker_handle.clone());
57+
}
58+
59+
fn setup_input_oninput_callback(worker: Rc<RefCell<web_sys::Worker>>) {
60+
let document = web_sys::window().unwrap().document().unwrap();
61+
62+
// If our `onmessage` callback should stay valid after exiting from the
63+
// `oninput` closure scope, we need to either forget it (so it is not
64+
// destroyed) or store it somewhere. To avoid leaking memory every time we
65+
// want to receive a response from the worker, we move a handle into the
66+
// `oninput` closure to which we will always attach the last `onmessage`
67+
// callback. The initial value will not be used and we silence the warning.
68+
#[allow(unused_assignments)]
69+
let mut persistent_callback_handle = get_on_msg_callback();
70+
71+
let callback = Closure::wrap(Box::new(move || {
72+
console::log_1(&"oninput callback triggered".into());
73+
let document = web_sys::window().unwrap().document().unwrap();
74+
75+
let input_field = document
76+
.get_element_by_id("inputNumber")
77+
.expect("#inputNumber should exist");
78+
let input_field = input_field
79+
.dyn_ref::<HtmlInputElement>()
80+
.expect("#inputNumber should be a HtmlInputElement");
81+
82+
// If the value in the field can be parsed to a `i32`, send it to the
83+
// worker. Otherwise clear the result field.
84+
match input_field.value().parse::<i32>() {
85+
Ok(number) => {
86+
// Access worker behind shared handle, following the interior
87+
// mutability pattern.
88+
let worker_handle = &*worker.borrow();
89+
let _ = worker_handle.post_message(&number.into());
90+
persistent_callback_handle = get_on_msg_callback();
91+
92+
// Since the worker returns the message asynchronously, we
93+
// attach a callback to be triggered when the worker returns.
94+
worker_handle
95+
.set_onmessage(Some(persistent_callback_handle.as_ref().unchecked_ref()));
96+
}
97+
Err(_) => {
98+
document
99+
.get_element_by_id("resultField")
100+
.expect("#resultField should exist")
101+
.dyn_ref::<HtmlElement>()
102+
.expect("#resultField should be a HtmlInputElement")
103+
.set_inner_text("");
104+
}
105+
}
106+
}) as Box<dyn FnMut()>);
107+
108+
// Attach the closure as `oninput` callback to the input field.
109+
document
110+
.get_element_by_id("inputNumber")
111+
.expect("#inputNumber should exist")
112+
.dyn_ref::<HtmlInputElement>()
113+
.expect("#inputNumber should be a HtmlInputElement")
114+
.set_oninput(Some(callback.as_ref().unchecked_ref()));
115+
116+
// Leaks memory.
117+
callback.forget();
118+
}
119+
120+
/// Create a closure to act on the message returned by the worker
121+
fn get_on_msg_callback() -> Closure<dyn FnMut(MessageEvent)> {
122+
let callback = Closure::wrap(Box::new(move |event: MessageEvent| {
123+
console::log_2(&"Received response: ".into(), &event.data().into());
124+
125+
let result = match event.data().as_bool().unwrap() {
126+
true => "even",
127+
false => "odd",
128+
};
129+
130+
let document = web_sys::window().unwrap().document().unwrap();
131+
document
132+
.get_element_by_id("resultField")
133+
.expect("#resultField should exist")
134+
.dyn_ref::<HtmlElement>()
135+
.expect("#resultField should be a HtmlInputElement")
136+
.set_inner_text(result);
137+
}) as Box<dyn FnMut(_)>);
138+
139+
callback
140+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<html>
2+
3+
<head>
4+
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
5+
<link rel="stylesheet" href="style.css">
6+
</head>
7+
8+
<body>
9+
<div id="wrapper">
10+
<h1>Main Thread/WASM Web Worker Interaction</h1>
11+
12+
<input type="text" id="inputNumber">
13+
14+
<div id="resultField"></div>
15+
</div>
16+
17+
<!-- Make `wasm_bindgen` available for `index.js` -->
18+
<script src='./pkg/wasm_in_web_worker.js'></script>
19+
<!-- Note that there is no `type="module"` in the script tag -->
20+
<script src="./index.js"></script>
21+
22+
</body>
23+
24+
</html>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// We only need `startup` here which is the main entry point
2+
// In theory, we could also use all other functions/struct types from Rust which we have bound with
3+
// `#[wasm_bindgen]`
4+
const {startup} = wasm_bindgen;
5+
6+
async function run_wasm() {
7+
// Load the wasm file by awaiting the Promise returned by `wasm_bindgen`
8+
// `wasm_bindgen` was imported in `index.html`
9+
await wasm_bindgen('./pkg/wasm_in_web_worker_bg.wasm');
10+
11+
console.log('index.js loaded');
12+
13+
// Run main WASM entry point
14+
// This will create a worker from within our Rust code compiled to WASM
15+
startup();
16+
}
17+
18+
run_wasm();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
body {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
display: flex;
8+
flex-direction: column;
9+
align-items: center;
10+
justify-content: center;
11+
font-family: "Century Gothic", CenturyGothic, Geneva, AppleGothic, sans-serif;
12+
color: white;
13+
background-color: black;
14+
}
15+
16+
#wrapper {
17+
width: 50%;
18+
display: flex;
19+
flex-direction: column;
20+
align-items: center;
21+
justify-content: center;
22+
}
23+
24+
#inputNumber {
25+
text-align: center;
26+
}
27+
28+
#resultField {
29+
text-align: center;
30+
height: 1em;
31+
padding-top: 0.2em;
32+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// The worker has its own scope and no direct access to functions/objects of the
2+
// global scope. We import the generated JS file to make `wasm_bindgen`
3+
// available which we need to initialize our WASM code.
4+
importScripts('./pkg/wasm_in_web_worker.js');
5+
6+
console.log('Initializing worker')
7+
8+
// In the worker, we have a different struct that we want to use as in
9+
// `index.js`.
10+
const {NumberEval} = wasm_bindgen;
11+
12+
async function init_wasm_in_worker() {
13+
// Load the wasm file by awaiting the Promise returned by `wasm_bindgen`.
14+
await wasm_bindgen('./pkg/wasm_in_web_worker_bg.wasm');
15+
16+
// Create a new object of the `NumberEval` struct.
17+
var num_eval = NumberEval.new();
18+
19+
// Set callback to handle messages passed to the worker.
20+
self.onmessage = async event => {
21+
// By using methods of a struct as reaction to messages passed to the
22+
// worker, we can preserve our state between messages.
23+
var worker_result = num_eval.is_even(event.data);
24+
25+
// Send response back to be handled by callback in main thread.
26+
self.postMessage(worker_result);
27+
};
28+
};
29+
30+
init_wasm_in_worker();

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- [web-sys: WebRTC DataChannel](./examples/webrtc_datachannel.md)
2626
- [web-sys: `requestAnimationFrame`](./examples/request-animation-frame.md)
2727
- [web-sys: A Simple Paint Program](./examples/paint.md)
28+
- [web-sys: WASM in Web Worker](./examples/wasm-in-web-worker.md)
2829
- [Parallel Raytracing](./examples/raytrace.md)
2930
- [web-sys: A TODO MVC App](./examples/todomvc.md)
3031
- [Reference](./reference/index.md)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# WASM in Web Worker
2+
3+
[View full source code][code]
4+
5+
[code]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples/wasm-in-web-worker
6+
7+
A simple example of parallel execution by spawning a web worker with `web_sys`,
8+
loading WASM code in the web worker and interacting between the main thread and
9+
the worker.
10+
11+
## Building & compatibility
12+
13+
At the time of this writing, only Chrome supports modules in web workers, e.g.
14+
Firefox does not. To have compatibility across browsers, the whole example is
15+
set up without relying on ES modules as target. Therefore we have to build
16+
with `--target no-modules`. The full command can be found in `build.sh`.
17+
18+
## `Cargo.toml`
19+
20+
The `Cargo.toml` enables features necessary to work with the DOM, log output to
21+
the JS console, creating a worker and reacting to message events.
22+
23+
```toml
24+
{{#include ../../../examples/wasm-in-web-worker/Cargo.toml}}
25+
```
26+
27+
## `src/lib.rs`
28+
29+
Creates a struct `NumberEval` with methods to act as stateful object in the
30+
worker and function `startup` to be launched in the main thread. Also includes
31+
internal helper functions `setup_input_oninput_callback` to attach a
32+
`wasm_bindgen::Closure` as callback to the `oninput` event of the input field
33+
and `get_on_msg_callback` to create a `wasm_bindgen::Closure` which is triggered
34+
when the worker returns a message.
35+
36+
```rust
37+
{{#include ../../../examples/wasm-in-web-worker/src/lib.rs}}
38+
```
39+
40+
## `index.html`
41+
42+
Includes the input element `#inputNumber` to type a number into and a HTML
43+
element `#resultField` were the result of the evaluation even/odd is written to.
44+
Since we require to build with `--target no-modules` to be able to load WASM
45+
code in in the worker across browsers, the `index.html` also includes loading
46+
both `wasm_in_web_worker.js` and `index.js`.
47+
48+
```html
49+
{{#include ../../../examples/wasm-in-web-worker/www/index.html}}
50+
```
51+
52+
## `index.js`
53+
54+
Loads our WASM file asynchronously and calls the entry point `startup` of the
55+
main thread which will create a worker.
56+
57+
```js
58+
{{#include ../../../examples/wasm-in-web-worker/www/index.js}}
59+
```
60+
61+
## `worker.js`
62+
63+
Loads our WASM file by first importing `wasm_bindgen` via
64+
`importScripts('./pkg/wasm_in_web_worker.js')` and then awaiting the Promise
65+
returned by `wasm_bindgen(...)`. Creates a new object to do the background
66+
calculation and bind a method of the object to the `onmessage` callback of the
67+
worker.
68+
69+
```js
70+
{{#include ../../../examples/wasm-in-web-worker/www/worker.js}}
71+
```

0 commit comments

Comments
 (0)