Web Worker 中的 Wasm
一个使用 `web_sys` 生成 Web Worker、在 Web Worker 中加载 Wasm 代码以及主线程和 Worker 之间交互的并行执行示例。
构建和兼容性
在撰写本文时,只有 Chrome 支持 Web Worker 中的模块,例如 Firefox 不支持。为了实现跨浏览器兼容性,整个示例的设置不依赖于 ES 模块作为目标。因此,我们必须使用 `--target no-modules` 进行构建。完整的命令可以在 `build.sh` 中找到。
Cargo.toml
`Cargo.toml` 启用了与 DOM 交互、将输出日志记录到 JS 控制台、创建 Worker 以及对消息事件做出反应所需的特性。
[package]
name = "wasm-in-web-worker"
version = "0.1.0"
authors = ["The wasm-bindgen Developers"]
edition = "2018"
rust-version = "1.57"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.92"
console_error_panic_hook = { version = "0.1.6", optional = true }
[dependencies.web-sys]
version = "0.3.4"
features = [
'console',
'Document',
'HtmlElement',
'HtmlInputElement',
'MessageEvent',
'Window',
'Worker',
]
src/lib.rs
创建一个结构体 `NumberEval`,其方法充当 Worker 中的状态对象,以及函数 `startup`,该函数将在主线程中启动。还包括内部辅助函数 `setup_input_oninput_callback`,用于将 `wasm_bindgen::Closure` 作为回调附加到输入字段的 `oninput` 事件,以及 `get_on_msg_callback`,用于创建一个在 Worker 返回消息时触发的 `wasm_bindgen::Closure`。
#![allow(unused)] fn main() { 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. let worker_handle = Rc::new(RefCell::new(Worker::new("./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); }) } }
index.html
包括输入元素 `#inputNumber` 用于输入数字,以及 HTML 元素 `#resultField` 用于写入评估结果(奇偶性)。由于我们需要使用 `--target no-modules` 进行构建才能跨浏览器在 Worker 中加载 Wasm 代码,因此 `index.html` 还包括加载 `wasm_in_web_worker.js` 和 `index.js`。
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="wrapper">
<h1>Main Thread/Wasm Web Worker Interaction</h1>
<input type="text" id="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>
index.js
异步加载我们的 Wasm 文件,并调用主线程的入口点 `startup`,该入口点将创建一个 Worker。
// 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
首先通过 `importScripts('./pkg/wasm_in_web_worker.js')` 导入 `wasm_bindgen`,然后加载我们的 Wasm 文件,并等待 `wasm_bindgen(...)` 返回的 Promise。创建一个新对象来进行后台计算,并将该对象的某个方法绑定到 Worker 的 `onmessage` 回调。
// 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();