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();