Is it possible to take a JsFuture as Send?

Getting error because an asynchronous function returned by tokio is Send and JsFuture from wasm_bindgen_futures has a !Send bound:

error: future cannot be sent between threads safely
   --> src\util\src\timing\mod.rs:564:17
    |
564 |       exec_future({
    |  _________________^
565 | |         let stopped = Arc::clone(&mut stopped);
566 | |         async move {
567 | |             wait(duration).await;
...   |
571 | |         }
572 | |     });
    | |_____^ future created by async block is not `Send`
    |
    = help: within `[async block@src\util\src\timing\mod.rs:566:9: 571:10]`, the trait `std::marker::Send` is not implemented for `Rc<RefCell<wasm_bindgen_futures::Inner>>`
note: future is not `Send` as it awaits another future which is not `Send`
   --> src\util\src\timing\platform\browser_runtime.rs:51:5
    |
51  |     wasm_bindgen_futures::JsFuture::from(wait_in_js_promise(ms.into())).await.unwrap();
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here on type `JsFuture`, which is not `Send`
note: required by a bound in `exec_future`
   --> src\util\src\futures\mod.rs:45:30
    |
43  | pub fn exec_future<F>(future: F)
    |        ----------- required by a bound in this function
44  | where
45  |     F: Future<Output = ()> + Send + 'static,
    |                              ^^^^ required by this bound in `exec_future`


How to unsafely cast that JsFuture to a Send one?

I'm wondering if I could wrap JsFuture in some way:

use std::{future::Future, sync::Arc};
use wasm_bindgen::JsValue;

pub struct JsFuture {
    inner: Arc<wasm_bindgen_futures::JsFuture>,
}

impl From<js_sys::Promise> for JsFuture {
    fn from(value: js_sys::Promise) -> Self {
        JsFuture {
            inner: Arc::new(wasm_bindgen_futures::JsFuture::from(value)),
        }
    }
}

impl Future for JsFuture {
    type Output = Result<JsValue, JsValue>;
    fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
        std::pin::pin!(*self.inner).poll(cx)
    }
}

This still causes a !Send bound

This won't be possible to do safely.

The Send bound is for transferring a value to another thread and a core principle of JavaScript is that it is single-threaded. That means no objects from JavaScript will implement Send.

Of course, you can probably work under the assumption that objects from your WebAssembly code will only ever stay on the main JavaScript thread, so it's fine to create a wrapper with an unsafe impl Send. However that unsafe assumption is now your responsibility to maintain.

struct Sendable<T>(pub T);

// Safety: WebAssembly will only ever run in a single-threaded context.
unsafe impl<T> Send for Sendable<T> {}

For example, if you decided you'd like to implement multi-threaded WebAssembly by spinning up web workers and using a common SharedArrayBuffer for their backing memory, then you'd need to go back and rethink whether your wrapper is still valid.

2 Likes

Didn't know about unsafe impl, useful! For workers, I'll never expose that "private" sendable future (I'm also planning to have a rialight::javascript API for communicating with the host browser, which will just alias wasm_bindgen, js_sys and other crates). (Rialight won't take care of workers, so if the developer wants... they'd need rialight::javascript and use them carefully)

Now I've this issue:

pub async fn wait(duration: Duration) {
    let ms: u32 = duration.as_millis().try_into().expect("Developer has given too large period for wait duration");
    crate::futures::browser::SendableJsFuture::from(wait_in_js_promise(ms.into())).await.unwrap();
}

At my background_timeout function, my async block is still not Send due to the created Promise in the browser implementation. Full diagnostic:

error: future cannot be sent between threads safely
   --> src\util\src\timing\mod.rs:564:17
    |
564 |       exec_future({
    |  _________________^
565 | |         let stopped = Arc::clone(&mut stopped);
566 | |         async move {
567 | |             wait(duration).await;
...   |
571 | |         }
572 | |     });
    | |_____^ future created by async block is not `Send`
    |
    = help: within `[async block@src\util\src\timing\mod.rs:566:9: 571:10]`, the trait `std::marker::Send` is not implemented for `*mut u8`
note: future is not `Send` as this value is used across an await
   --> src\util\src\timing\platform\browser_runtime.rs:51:84
    |
51  |     crate::futures::browser::SendableJsFuture::from(wait_in_js_promise(ms.into())).await.unwrap();
    |                                                     -----------------------------  ^^^^^         - `wait_in_js_promise(ms.into())` is later dropped here
    |                                                     |                              |
    |                                                     |                              await occurs here, with `wait_in_js_promise(ms.into())` maybe used later
    |                                                     has type `Promise` which is not `Send`
note: required by a bound in `exec_future`
   --> src\util\src\futures\mod.rs:45:30
    |
43  | pub fn exec_future<F>(future: F)
    |        ----------- required by a bound in this function
44  | where
45  |     F: Future<Output = ()> + Send + 'static,
    |                              ^^^^ required by this bound in `exec_future`

Here's background_timeout (which is equivalent to setTimeout and can be cancelled):

This can be trouble, because for one target export, some functions return a Send future, and for the browser export, these same functions would return a !Send future.

Hmm...

Anyway, I was forced to define exec_future as this:

pub fn exec_future<F>(future: F)
where
    F: Future<Output = ()> + Send + 'static,
{
    #[cfg(not(any(feature = "rialight_default_export", feature = "rialight_browser_export")))] {
        panic!("Incorrect Rialight runtime configuration");
    }
    #[cfg(feature = "rialight_default_export")] {
        tokio::task::spawn(future);
    }
    #[cfg(feature = "rialight_browser_export")] {
        wasm_bindgen_futures::spawn_local(future);
    }
}

I replaced tokio::task::spawn by tokio::task::spawn_local inside exec_future and that solved the issue (I mean, it compiles, but it doesn't work in the playground). Can anyone say whether this is good or bad?

Also, is it worriable that something like rialight::util::timing::wait returns a future with Send on some platforms and, in the browser, a future with !Send?

How to do this?

async fn f() {
    not_sendable();
}
// f() always returns a !Send future

Rc::new(()) mightn't be optimal. Doing:

struct NotSendable;
impl !Send for NotSendable {}
NotSendable;

Gives syntax error

Put a PhantomData<*const ()> in it. Raw pointers aren't Send or Sync.

2 Likes

Makes sense! If I .await on a future that takes a ready PhantomData of this type, then it adds !Send + !Sync to the future:

use futures::future::Future;

async fn f1() {
    f2(f3()).await;
}

async fn f2(given_future: impl Future<Output = ()> + Send + Sync + 'static) {
    given_future.await;
}

async fn f3() {
    // !Send and !Sync
    futures::future::ready(std::marker::PhantomData::<*const ()>::default()).await;

    println!("f3() called");
}

Is there still a way to not having to .await into such for extra performance gain? For a wait, the .await isn't critical, but it may be critical for intervals.

I don't get what you mean by that. Async code is not inherently slower than sync code. You can't magically gain performance by converting async code to sync (or vice versa) alone.

What I mean is that waiting for an useless ready future is useless and it has at least some operation.

Probably not, since there's no reason to return from the current future if the awaited one immediately returns Poll::Ready. And functions with constant output are generally optimized away - e.g. this function -

pub async fn test() -> i32 {
    futures::future::ready(42).await
}

...is compiled down to exactly two movs and ret, i.e. only one instruction more then the equivalent sync code.

2 Likes

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.