How do you all make your dynamic code `Send` agnostic?

Rust seems to have the restriction that any Boxed trait needs to specify whether it has a Send bound or not.

If your library works with user defined types, and you use a lot of Pin<Box<dyn Future...>> and others, it becomes quickly problematic to work on both Send and !Send types.

As an example, actix's send method requires Send on all Message types.

The result it seems would be the need to duplicate almost an entire library in Send and !Send halves, which obviously doesn't keep it DRY.

So far, the solution I have found is kind of hacky, so I'm hoping someone here will have a better one, and otherwise it might still serve others:

You can access the same code through different paths using the module system. The actual code can be symlinked under two different modules, and can then use super::BoxedFuture for any types that might have a Send bound in them. You can use both type and trait aliases in the super modules:

send.rs:
--------
mod inner;
pub use inner::*;

type BoxFuture<T> = Pin<Box< dyn Future<Outcome=T> + Send >>;
trait SomeTrait: OtherTrait + Send {}

not_send.rs:
------------
mod inner;
pub use inner::*;

type BoxFuture<T> = Pin<Box< dyn Future<Outcome=T> >>;
trait SomeTrait: OtherTrait {}

inner.rs
--------
use super::*;

pub struct SomeFunctionality;
// use BoxFuture which might or might not be Send

Now we symlink inner.rs under send/inner.rs and not_send/inner.rs
The super modules can also publicly re-export everything from the inner module. Now you can access your code as:
send::SomeFunctionality vs not_send::SomeFunctionality.

Both will refer to the same code, symlinked in two places.

Warning: I don't think the compiler team wants to make any guarantees on the forward compatibility of such hacks, but it does seem to work for me. I figure if I have to duplicate my entire library, I might as well do it when this breaks in the future rather than now.

Have yet to try it on a realistic sized lib.

1 Like

Yep, this is very hacky; there is a way to make it a little bit less hacky:

  • with path/to/common.rs containing the logic depending on a defined BoxFuture and SomeTrait

  • path/to/send/mod.rs

    pub
    type BoxFuture<T> = Pin<Box< dyn Future<Outcome=T> + Send >>;
    
    pub
    trait SomeTrait: OtherTrait + Send {}
    
    include!("../common.rs");
    
  • path/to/not_send/mod.rs

    pub
    type BoxFuture<T> = Pin<Box< dyn Future<Outcome=T> >>;
    
    pub
    trait SomeTrait: OtherTrait {}
    
    include!("../common.rs");
    
  • path/to/mod.rs or path/to.rs

    pub
    mod send;
    
    pub
    mod not_send;
    

You can also use feature flags for a similar-ish effect

EDIT: As @cuviper pointed out, not a good idea in this case

3 Likes

Features are supposed to be additive, but adding a type constraint is a breaking change. If two crates use yours, but only one enables the "send" feature, the other will be required to meet that constraint too.

1 Like

I didn't see you updated your answer. I deliberately would not want to use features for something like this, since the whole point is that I would like for code to be agnostic to Send, eg, if the user provides a type that is Send, they can send it across threads, and if it is !Send, the compiler won't let them. I would want them to be able to use a messaging system with both message types in the same crate.

The problem is that even with gymnastic like above, it's not entirely agnostic, since even interface traits that define methods that return a BoxFuture, need to be separated over Send and !Send, so they're no longer the same traits, types are no longer the same types, so in case of an Actor model, if the Actor has only one mailbox, the mailbox is either Send or !Send, and it wont be able to take both ever, unless an Actor has several mailboxes attached to it (which is overhead people would pay for even if they don't use it), because the channel it uses will use a boxed dyn trait, so it's either Send or it's not.

I'm really doubting whether this is worth the trouble, but I don't like to shut down user scenarios from the get go. I personally don't have any use case for !Send messages, but I think someday someone (maybe I) will, and it's just another choice locked out.

Since async programming can work perfectly within a single thread, it seems wrong that things require your types to be Send by definition. I feel the language has a bit of an issue here if this can not realistically be expressed.

Code can cast Box<dyn T + Send> to Box<dyn T>, so people with Send values should be able to use the not-Send API without duplication...I would expect. Do you have a more fleshed-out example?

Unfortunately that's not how it works. Rust requires you to be specific about your bounds, and even repeat them where they could be deduced.

In particular if you have trait that accepts a T that is not Send in a method, that method can never send T across threads even if you call it with a T that is Send, just because you didn't constrain the parameter.

If you do constrain it, it can never be called again with a T that is not Send, even if say the receiving end of a channel is on the same thread and in theory it would be fine.

So yes, something can be cast to not Send, but not to send it across threads :wink:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.