Wasm-bindgen: Different behavior when spawning task between spawn_local(from wasm_bindgen_futures) and spawner.spawn_local()

Created on 4 Aug 2019  路  6Comments  路  Source: rustwasm/wasm-bindgen

Summary

Hi! I'm trying to write async-await compatible code within wasm.
Currently I'm trying to create a future that will execute it's context later in a specific time duration.

Here's the async function that returns the future:

use log::info;
use web_logger;
use wasm_timer::Delay;
use std::time::Duration;

async fn call_later()  {
    web_logger::init();
    Delay::new(Duration::from_secs(3)).await.unwrap();
    info!("hey its me");
}

I used the wasm_timer crate so it gets compiled to wasm target.

Following the wasm_bindgen_futures twitter, I've tried calling execute this future in spawn_local()


#[wasm_bindgen]
fn test_async{
    spawn_local(call_later());
}

Which works! I can even chain futures together so I can execute multiple futures at the same time.

#[wasm_bindgen]
fn test_async{
    let multi_future = (0..10).map(|_| call_later()).collect::<Vec<_>>();
    let multi_future = futures::future::join_all(multi_future);
    spawn_local(fut_wrapper(multi_future));
}

And all of it completed at the same time, which means they are not blocking together.

I also tried to execute it via a localpool:

use futures::executor;

fn exec_by_pool() {
    let mut pool = executor::LocalPool::new();
    let mut spawner = pool.spawner();
    spawner.spawn_local(fut_wrapper(test_fn())).unwrap();
    pool.run();
}

Which compiles, but failed at runtime:

panicked at 'can't block with web assembly', src/libstd/sys/wasm/condvar.rs:23:9
...
...
panicked at 'cannot recursively acquire mutex', src/libstd/sys/wasm/mutex.rs:22:9

Since I'm guessing it's having to do with how the wasm_timer crate got its future polled by the executor, I tried other kinds of operation.
If not using the wasm_timer, I can chain other types of futures just fine inside the localpool.

async fn call_later()  {
    web_logger::init();
    // Delay::new(Duration::from_secs(3)).await.unwrap();
    info!("hey its me");
}

And this works. Chaining works too. Which is started to confuse me what made the difference.


What I'm confused, is that since chaining futures and other operation might work and can be executed under the localpool, why this specific one wasm_timer didnt?
If there are some blocker, what cause these operation to fail?

question

Most helpful comment

@extraymond LocalPool doesn't work with Wasm, because Wasm does not have blocking threads, and LocalPool is designed for blocking threads. That's what the can't block with web assembly panic means.

This is a limitation in Wasm itself, not Rust, so there's not much we can do about it right now.

The reason your call_later works is because it doesn't actually use await, so LocalPool doesn't block.

The reason why calling spawn_local inside of spawner.spawn_local works is for the same reason: you're not using await, so LocalPool doesn't block, so it works.

But as soon as you use await and it blocks, then you'll get the panic.

P.S. Using wasm_bindgen_futures::futures_0_3::spawn_local is basically the same as LocalPool, so you don't need LocalPool.

All 6 comments

Additional discovery:

If I wrap the future inside spawn_local from the wasm_bindgen_futures crate, spawning task using spawner.spawn_local(fut) works now.

fn exec_by_pool_nested() {
    let mut pool = executor::LocalPool::new();
    let mut spawner = pool.spawner();
    spawner
        .spawn_local(async {
            spawn_local(call_later());
        })
        .unwrap();
    pool.run();
}

My gut feeling is that the spawn_local from wasm_bindgen_futures will queue the future into some outer resource managed by js, and if that's the case, futures managed by the pool will get executed because polling is by not directly polling on it via some waker.

@extraymond LocalPool doesn't work with Wasm, because Wasm does not have blocking threads, and LocalPool is designed for blocking threads. That's what the can't block with web assembly panic means.

This is a limitation in Wasm itself, not Rust, so there's not much we can do about it right now.

The reason your call_later works is because it doesn't actually use await, so LocalPool doesn't block.

The reason why calling spawn_local inside of spawner.spawn_local works is for the same reason: you're not using await, so LocalPool doesn't block, so it works.

But as soon as you use await and it blocks, then you'll get the panic.

P.S. Using wasm_bindgen_futures::futures_0_3::spawn_local is basically the same as LocalPool, so you don't need LocalPool.

@Pauan Thx for your reply. Didn't know spawn_local works the same as loop.spawn.

Actually I tried awaiting on multiple types of futures and they all seem to work, so I'm kinda loss to find the pattern. Things I've tried:

  1. Guarding a resource using Rc>, and let two futures lock it and mutate it --> works.
  2. Using futures::channel::mpsc to send messages between two futures, and it works too.
  3. Awaiting a JsFuture from fetch api, it works too. I can await it after awaiting a Delay first too.

All of the above example will work concurrently which seems that the loop(if any) are executing them in the right order.

For what I've tried, can I say that, rust in the context of wasm, since it have no concept of the kernel, if the polling and waking is notified by os signals, they might fail. And if that future is resolved in the user space, or via some generator style that chained them together, they can work?

I'm still confused at what might work what might fail. Sorry if I'm learning slowly.

Thanks for the report @extraymond! I think there's probably not a bug with wasm-bindgen so I'm going to close this, but feel free to comment if there's more!

The idioms of futures on wasm are somewhat different in the execution phase than for other targets, so executors that work on other platforms won't work for wasm. Many crates built for other targets for futures also won't work on wasm since they likely rely on some form of system support or something like that.

All Rust futures get translated to JS promises one way or another, and JS is largely responsible for executing futures. All JS promises can never block, so we inherit that restriction in wasm/Rust.

Thanks for the report @extraymond! I think there's probably not a bug with wasm-bindgen so I'm going to close this, but feel free to comment if there's more!

The idioms of futures on wasm are somewhat different in the execution phase than for other targets, so executors that work on other platforms won't work for wasm. Many crates built for other targets for futures also won't work on wasm since they likely rely on some form of system support or something like that.

All Rust futures get translated to JS promises one way or another, and JS is largely responsible for executing futures. All JS promises can never block, so we inherit that restriction in wasm/Rust.

Thx for your reply! Totally agree that this is not a bug in wasm-bindgen.

I saw in your post about wasm-bindgen-futures that we are indeed spawning promises into microtask queue in the js event loop.

So does it means that before wasm has real thread representation, if we want to lock resources, we will have to execute the synchronize primitives via js side maybe like wrapping something like await-mutex module. Then wait fot the js event loop to call us that this resource can be used or not?

Correct yes, you'll need an async locking-like primitive for now. Eventually wasm will have threads but the main thread will still need to do async locking.

Was this page helpful?
0 / 5 - 0 ratings