wasm-futures-executor 0.2.0

Executor for asynchronous task based on wasm web workers
Documentation

wasm-futures-executor

License Cargo Documentation

This crate provides an executor for asynchronous task with the same API as futures_executor::ThreadPool targeting the web browser environment. Instead of using spawning threads via std::thread, web workers are created. This crate tries hard to make this process as seamless and painless as possible.

Sample Usage

use futures::channel::mpsc;
use futures::StreamExt;
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_futures_executor::ThreadPool;

#[wasm_bindgen]
pub async fn start() -> Result<JsValue, JsValue> {
    let pool = ThreadPool::max_threads().await?;
    let (tx, mut rx) = mpsc::channel(10);
    for i in 0..20 {
        let mut tx_c = tx.clone();
        pool.spawn_ok(async move {
            tx_c.start_send(i * i).unwrap();
        });
    }
    drop(tx);
    let mut i = 0;
    while let Some(x) = rx.next().await {
        i += x;
    }
    Ok(i.into())
}

.. and using it:

import init, { start } from './sample.js';

async function run() {
  await init();

  const res = await start();
  console.log("result", res);
}
run();

And build your project with:

RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
  cargo +nightly build --target wasm32-unknown-unknown --release -Z build-std=std,panic_abort

wasm-bindgen \
  ./target/wasm32-unknown-unknown/release/sample.wasm \
  --out-dir . \
  --target web \
  --weak-refs

.. or if you want to use wasm-pack build -t web, make sure to set up the nightly toolchain and the proper RUSTFLAGS (for example by providing creating the rust-toolchain.toml and .cargo/config files like done in this repo).

Please have a look at the sample for a complete end-to-end example project without bundlers, and sample-webpack using Webpack 5.

Note: This crate requires usage of the web target of wasm-bindgen/wasm-pack. Given the broad standardization on ES Modules, this should be mostly fine.

How it works

Similar to the async executor provided by futures-executor, multiple worker "threads" are spawned upon instantation of the ThreadPool. Each thread is a web worker, which loads some js glue code. The glue code is provided as a js snippet, which is linked from the wasm-bindgen generated js glue code. This way, it's transparent to any bundlers. Each web worker is constructed with the following arguments:

  1. WebAssembly module initialization and its shared memory (SharedArrayBuffer).
  2. The third one is a pointer to some shared state, including a channel, where the async tasks are passed in. The library provides the worker_entry_point function for this purpose.

Once the ThreadPool is dropped, all channels are closed and the web workers are terminated.

Unfortunately, this requires a nightly compilers because Rust's standard library needs to be recompiled with the following unstable features:

  • atomics: (rust feature) supports for wasm atomics, see https://github.com/rust-lang/rust/issues/77839
  • bulk-memory: (llvm feature) generation of atomic instruction, shared memory, passive segments, etc.
  • mutable-globals: (llvm feature)

A note: When workers are destroyed, some memory might be leaked (for example thread local storage or the thead's stack). I recommend passing the --weak-ref option to wasm-bindgen in order to let wasm-bindgen create user defined finalizers according to the WeakRef proposal.

In general, implementing wasm threads in Rust seems to have lost some of its traction ..

Browser support

Sharing memory via SharedArrayBuffer will soon (starting with Chrome 92) require setting the right headers to mark the website as "cross-origin isolated", meaning that the following headers need to be sent with the main document:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

For more information check out this link.

Loading modules in web workers is currently only supported in Chromium. There seems the be finally some progress in Firefox. As a workaround, you can include a workers-polyfill.

If you're targeting older browser, you might want to do some feature detection and fall back gracefully -- but that's out of scope for this documentation.

Is it worth it?

There is a significant overhead of sending and spawning futures across the thread boundary. It makes most sense for long-lived tasks (check out the factorial demo, which is a rough 3x performance increase). As always, make sure to profile your use case.

Related crates and further information

  • The first and foremost resource is the great raytracing demo of the wasm-bindgen docs and its background blog post.

  • The wasm-bindgen-rayon crate provides extensive documentation and a nice end-to-end example.

  • The wasm_thread crate is quite similar in its approach as this crate. The main difference is the usage of ES modules.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.