Unsafe Send impl for local async trait, is it sound?

It bugged me that using the trait-variant crate, once a Send version is provided, you can not use its local variant without Send bounds anymore. This is my attempt to address this issue:

use ::core::{
    future::Future,
    pin::Pin,
    task::{Context, Poll},
};

pub trait LocalFoo {
    type Output;

    fn foo(&self) -> impl Future<Output = Self::Output>;
}

pub trait Foo: LocalFoo {
    fn foo(&self) -> impl Future<Output = Self::Output> + Send;
}

impl<T> Foo for T
where
    T: LocalFoo + Sync,
    <T as LocalFoo>::Output: Send,
{
    fn foo(&self) -> impl Future<Output = Self::Output> + Send {
        // Considering the given trait bounds,
        // is forcing Send on this Future sound?
        // Are there any other issues?
        UnsafeSendFuture(<Self as LocalFoo>::foo(self))
    }
}

// Helper struct to force Send on a Future.
// I also tried core::mem::transmute but
// it can not infer its impl dst type.
struct UnsafeSendFuture<T>(T);

unsafe impl<T> Send for UnsafeSendFuture<T> where Self: Future {}

impl<T> Future for UnsafeSendFuture<T>
where
    T: Future,
    <T as Future>::Output: Send,
{
    type Output = T::Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        unsafe { self.map_unchecked_mut(|UnsafeSendFuture(future)| future) }.poll(cx)
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s

AFAICT this is unsound, because I can return a !Send future from LocalFoo::foo, regardless of whether Self::Output is Send and Self is Sync or not:

use std::sync::Mutex;
use std::time::Duration;

struct X;

static M: Mutex<u8> = Mutex::new(0);

impl LocalFoo for X {
    type Output = u8;
    
    fn foo(&self) -> impl Future<Output = Self::Output> {
        async {
            let lock = M.lock().unwrap(); // makes Future !Send even though Self::Output is Send
            do_something_async().await;
            *lock
        }
    }
}

async fn do_something_async() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

Playground.

2 Likes

Is it possible to guard against !Send somehow to address this issue?

You could implement Foo only for types whose LocalFoo::foo future is Send to begin with:

pub trait LocalFoo {
    type Output;
    type Fut: Future<Output = Self::Output>;

    fn foo(&self) -> Self::Fut;
}

pub trait Foo {
    type Output;
    type Fut: Future<Output = Self::Output> + Send;
    
    fn foo(&self) -> Self::Fut;
}

impl<T> Foo for T
where
    T: LocalFoo,
    <T as LocalFoo>::Fut: Send,
{
    type Output = <T as LocalFoo>::Output;
    type Fut = <T as LocalFoo>::Fut;
    
    fn foo(&self) -> Self::Fut {
        <Self as LocalFoo>::foo(self)
    }
}

Playground.

That's not true. From the documentation:

If you want to preserve an original trait untouched, make can be used to create a new trait with bounds on async fn and/or -> impl Trait return types.

#[trait_variant::make(IntFactory: Send)]
trait LocalIntFactory {
    async fn make(&self) -> i32;
    fn stream(&self) -> impl Iterator<Item = i32>;
    fn call(&self) -> u32;
}

The example causes a second trait called IntFactory to be created. Implementers of the trait can choose to implement the variant instead of the original trait. The macro creates a blanket impl which ensures that any type which implements the variant also implements the original trait.

Notice the last sentence. That blanket implementation does exactly what you want. Demo with your particular trait definition:

#[trait_variant::make(Foo: Send)]
pub trait LocalFoo {
    type Output;

    async fn foo(&self) -> Self::Output;
}

pub struct Implementor;

impl Foo for Implementor {
    type Output = u8;
    async fn foo(&self) -> u8 {
        1
    }
}

async fn uses_local_foo<T: LocalFoo>(value: T) -> T::Output {
    value.foo().await
}

pub async fn example() {
    let a = uses_local_foo(Implementor).await;
    println!("{a}");
}

No unsafe code is required.

1 Like

Let me give a little more context on where this is coming from. The issue I was facing is when you provide a generic impl of Foo with some trait bounds which, because of Foo's Send requirement, need to be Send as well. So far so good. Unfortunately, the local trait-variant version of that impl keeps the Send bounds even though in principle the very same impl block could work without Send bounds and support many more types.

So my idea was to flip the approach by providing a broad, not Send bounded, impl first and then reuse that very same impl in the more restrict world of Send.

Can you provide sample code that tries to do the thing you want to just work, but doesn't compile? Not trying to work around the problem at all, but demonstrating it?

Unfortunately dropping the ergonomic impl Future in favor of an associated type makes the whole trait very unpractical to work with.

Are there some other ways to guard against !Send?

Sure! I want a generic impl of the LocalFoo trait (in this case core::ops::Not) to be sound for Send, with the trivially same impl.

fn main() {
    let cell = Cell::new(false);
    let local_foo = !Wrapper(&cell);
    _ = LocalFoo::foo(&local_foo);
    // Does not compile because of the Cell in local_foo.
    // Thats what we want!
    // _ = Foo::foo(local_foo);
    
    let foo = !Wrapper(false);
    _ = LocalFoo::foo(&foo);
    // Does compile, bool is Send.
    // And its reusing the impl from LocalFoo!
    _ = Foo::foo(&foo);
}

(Playground)

   Compiling playground v0.0.1 (/playground)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/playground`

I understand the problem now. You can't use trait_variant's strategy because you can't make which trait appears in the generic implementation conditional on whether the future is Send.

But your unsafe-using impl is not sound, because in

impl<T> Foo for T
where
    T: LocalFoo + Sync,
    <T as LocalFoo>::Output: Send,
{
    fn foo(&self) -> impl Future<Output = Self::Output> + Send {
        UnsafeSendFuture(<Self as LocalFoo>::foo(self))
    }
}

the Sendness of the output has nothing to do with the Sendness of the future, which is what you're unsafely asserting. Unfortunately, the syntax for doing this correctly, return type notation, is unstable and incomplete, but it does compile on nightly today:

#![feature(return_type_notation)]

impl<T> Foo for T
where
    T: LocalFoo<foo(..): Send, Output: Send> + Sync,
{
    fn foo(&self) -> impl Future<Output = Self::Output> + Send {
        <Self as LocalFoo>::foo(self)
    }
}

(I'm also using “associated type bounds”, LocalFoo<Output: Send>, which is stable as of 1.79.0. RTN is the extension of this sort of thing to naming the return type of a function on the left side of a bound.)

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.