Unix, wasm, and "+ Send"

I'm writing some code which needs to be able to compile to unix and wasm. One of the challenges is the use of the Send trait, as unix needs it, while wasm doesn't support it. So I have lots of code like this:

#[cfg(target_family = "wasm")]
type SubsystemCallback<T> = Box<dyn Fn(Vec<T>) -> BoxFuture<'static, Vec<(Destination, T)>>>;
#[cfg(target_family = "unix")]
type SubsystemCallback<T> =
    Box<dyn Fn(Vec<T>) -> BoxFuture<'static, Vec<(Destination, T)>> + Send>;
#[cfg(target_family = "wasm")]
type SubsystemTranslatorCallback<T> = Box<dyn Fn(Vec<BrokerID>, T) -> BoxFuture<'static, bool>>;
#[cfg(target_family = "unix")]
type SubsystemTranslatorCallback<T> =
    Box<dyn Fn(Vec<BrokerID>, T) -> BoxFuture<'static, bool> + Send>;

I would like to avoid duplicating all these methods. For trait definitions I use the following:

#[cfg(target_family = "wasm")]
pub mod asy {
    pub trait Async {}
    impl<T> Async for T {}
}
#[cfg(target_family = "unix")]
pub mod asy {
    pub trait Async: Sync + Send {}
    impl<T: Sync + Send> Async for T {}
}
use asy::*;

And then I can add the Async to the trait, and it's automatically the correct definition. However, that doesn't work for the above methods.

Is there a way to do that in a more convenient way? Would some kind of macro help?

This sort of thing can be very difficult. Can you say more about exactly why you need Send on Unix? It may be possible to avoid, this simplifying the problem.

wgpu solves this with a WasmNotSend trait, which it conditionally compiles to require Send or not depending on whether you're targetting wasm. Then it can just use WasmNotSend everywhere (though I'm not sure if this works properly with trait objects)

With unix, I use tokio::spawn, so I think Send is necessary. With wasm, I use wasm_bindgen_futures::spawn_local, as tokio::spawn is not available. Or at least it wasn't when I started writing the code 3 years in the past :slight_smile:

I looked here:

I think that is what I'm doing with my definition of the Async trait. Which works for other trait definitions, but not as a marker trait in a Box<> definition.

As I did all of this quite experimentally (change things around until it works), I don't even have the words to describe what works and what not :slight_smile:

Given my Async definition above, I cannot use it as auto trait:

trait TraitOne{}

struct One<T>{
    t: T
}

// This is OK - it's a trait restriction.
impl<T: Async> One<T>{
    fn new(t: T) -> Self {
        Self{t}
    }

    // This doesn't work: "only auto traits can be used as additional traits in a trait object" 
    fn do_something(&self) -> Box<dyn TraitOne + Async>{
        todo!()
    }

    // This works - it's an auto trait.
    fn do_something_else(&self) -> Box<dyn TraitOne + Send>{
        todo!()
    }
}

You can use a LocalSet to run !Send tasks on a single thread you create, instead of a Tokio thread pool. (There are other, potentially simpler, options if you don’t need Tokio in particular.)

OK - that might be another way. I kept the + Send and created some macros to make it a bit nicer. One of the macros is horrible, as it converts the type to a string, adds a + Send 3 characters from the end if it's a non-wasm platform, and then interprets this string back into a rust type. But now I can write something like:

#[target_send]
type Something<T> = Box<SomethingElse<T>>;

And it will add the + Send if it is necessary. Not the nicest thing I can come up with, but it keeps the code cleaner, IMO.

An implementation is here:

Though I could merge the case for the trait and the type. But the trait part is done in a 'correct' way, while the type part just adds a + Send string to the TokenStream.

I'm curious, how exactly does WASM "not support" Send? I'd imagine (and hope) Send just doesn't mean anything, since there are no threads in WASM. Is it an issue of wasm_bindgen using funky trait bounds, or is it another one of those "WASM targets have a fake std" stumbling blocks?

  1. It's not actually true that there are never threads; rather, there were never threads (or rather, shared memory requiring synchronization) when the wasm32-unknown-unknown target was originally defined. So, sometimes you want to to ignore the fact that wasm32-unknown-unknown provides a non-thread-safe Mutex, because it’s incorrect in some cases.
  2. Even if you’re not intending to use Wasm shared memory yourself, you may be using a library which wants to avoid the unsound one-thread assumption and therefore does not implement Send for its types when building for wasm32-unknown-unknown. (In that case, you might choose to use send_wrapper, or that might not suit your application.)