Issue with `wasm-bindgen`-generated glue for a webworker

Here's my setup:
Worker loader:

importScripts("worker.js")
this.onmessage = async event => {
    console.log("OnMessage");
    // event.data[0] should be the Memory object, and event.data[1] is the value to pass into entry_point
    const { entry_point } = await wasm_bindgen(
        "worker_bg.wasm",
        event.data[0]
    );

    console.log("OnMessage: entry reached");
    entry_point(Number(event.data[1]));
}

invoked from main wasm module via:

use wasm_bindgen::JsValue;

pub fn spawn(f: impl FnOnce() + Send + 'static) -> Result<web_sys::Worker, JsValue> {
    let worker = web_sys::Worker::new("./ww.js").expect("Worker failed to start");
    // Double-boxing because `dyn FnOnce` is unsized and so `Box<dyn FnOnce()>` is a fat pointer.
    // But `Box<Box<dyn FnOnce()>>` is just a plain pointer, and since wasm has 32-bit pointers,
    // we can cast it to a `u32` and back.
    let ptr = Box::into_raw(Box::new(Box::new(f) as Box<dyn FnOnce()>));
    let msg = js_sys::Array::new();
    // Send the worker a reference to our memory chunk, so it can initialize a wasm module
    // using the same memory.
    msg.push(&wasm_bindgen::memory());
    // Also send the worker the address of the closure we want to execute.
    msg.push(&JsValue::from(ptr as u32));
    worker.post_message(&msg).expect("Worker failed to start");
    Ok(worker)
}

Browser console indicates that wasm_bindgen() never returns. Sprinkling additional console.logs in the generated glue code reveals that it gets stuck in wasm.__wbindgen_start(); in finalizeInit.

What gives? I've been fighting this for a few hours and have absolutely no idea what could be going wrong.

I'm gonna page @bjorn3 because I've seen a github issue where they described using this web worker technique to emulate threads.

Are you using the exact same wasm module on both the main browser thread and the web worker? And have you compiled it with the atomics target feature enabled?

I have -Ctarget-feature=+atomics,+bulk-memory,+mutable-globals. No, main and worker are separate modules. Main module seems to work fine, except that it's stuck forever waiting for a worker to start.

wasm32-unknown-unknown uses the static relocation model, which means that the position of global variables is hard coded into every function that uses them, rather than allowing their position to be different between executions like it would for native dynamic libraries (and in case of PIE executables, native executables). The main and worker module likely put different globals in the same location, thus causing data corruption.

2 Likes

Well that fixed it. It'd be nice if it somehow could fail loudly, but oh well.

Now the next issue is that nested workers don't seem to work:

This setup with

spawn(||{spawn_audio_thread()})

only spawns the outer thread.

Some further experiments reveal that the thread's child thread is created, but it's closure doesn't run.

I'm wondering if trunk doesn't do what I expect when I include the same crate both as "main" and as
" worker".

Not directly related, but I'm working on a PR to Burn to make it run in the browser. WIP: burn-train in the browser by AlexErrant · Pull Request #938 · Tracel-AI/burn · GitHub

Most of the effort is being put into making a spawn method that simulates std::thread::spawn, more or less. There might be some learnings there that may help you. I can vouch for web workers spawning web workers as something that "just works" - as long as you compensate for their construction being async.

It seems that in my case the "inner" worker is spawned without errors, but the code passed to it does not run.

I wonder if I could be accidentally blocking the main thread somehow...

As in, this only prints "hello from main worker":

#[cfg(target_arch = "wasm32")]
fn main() -> Result<()> {
    use pcmg::wasm_thread::spawn;

    console_error_panic_hook::set_once();

    _ = spawn(|| {
        web_sys::console::log_1(&"hello from main worker".into());
        _ = spawn(|| {
            web_sys::console::log_1(&"hello from main:child worker".into());
            _ = spawn(|| {
                web_sys::console::log_1(&"hello from main:child:child2 worker".into());
            });
        });
    });

    Ok(())
}

Really can't see any functional differences between my and your code.

Sorry to be a tease, but my spawn seems to work just fine:

Uses this spawn.

I'm using web_sys::WorkerType::Module but I don't expect that to make a difference.

Well yeah, I pretty much copied my approach from Tweag writeup. Weird.

Poking around some more, I can get either just egui or just "threads" to work, but not both at the same time.

@emilk is async the only way to run egui on wasm now?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.