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]
authors = ["The wasm-bindgen Developers"]
edition = "2021"
name = "wasm-in-web-worker"
publish = false
version = "0.0.0"

[lib]
crate-type = ["cdylib"]

[dependencies]
console_error_panic_hook = { version = "0.1.6", optional = true }
wasm-bindgen = { path = "../../" }

[dependencies.web-sys]
features = [
  'console',
  'Document',
  'HtmlElement',
  'HtmlInputElement',
  'MessageEvent',
  'Window',
  'Worker',
]
path = "../../crates/web-sys"

[lints]
workspace = true

src/lib.rs

建立一個結構體 NumberEval,其中包含作為 worker 中有狀態物件的方法,以及在主執行緒中啟動的函式 startup。還包括內部輔助函式 setup_input_oninput_callback,用於將 wasm_bindgen::Closure 作為回呼附加到輸入欄位的 oninput 事件,以及 get_on_msg_callback,用於建立一個 wasm_bindgen::Closure,當 worker 返回訊息時會觸發它。


# #![allow(unused_variables)]
#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 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.jsindex.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_bindgen(...) 返回的 Promise 來載入我們的 Wasm 檔案。建立一個新物件來執行背景計算,並將該物件的方法繫結到 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();