Implementation conditional on Send

Imagine the following: You have a message passing channel, and it implements Send only if the message type is Send. We might want to use an implementation without atomics for non-Send message types. To do that, we can use the following abstraction:

use core::marker::PhantomData;
use core::mem::ManuallyDrop;

use self::is_send_impl::is_send;
mod is_send_impl {
    use core::cell::Cell;
    use core::marker::PhantomData;
    struct IsSend<'a, T: ?Sized> {
        is_send: &'a Cell<bool>,
        _marker: PhantomData<T>,
    }

    impl<T: ?Sized> Clone for IsSend<'_, T> {
        fn clone(&self) -> Self {
            self.is_send.set(false);
            IsSend {
                is_send: self.is_send,
                _marker: PhantomData,
            }
        }
    }
    impl<T: Send + ?Sized> Copy for IsSend<'_, T> {}

    pub(super) fn is_send<T: ?Sized>() -> bool {
        let is_send = Cell::new(true);
        let _ = [IsSend::<T> {
            is_send: &is_send,
            _marker: PhantomData,
        }]
        .clone();
        is_send.get()
    }
}

pub struct ImplConditionalOnSend<T: ?Sized, IfSend, IfNotSend> {
    inner: Inner<IfSend, IfNotSend>,
    _t: PhantomData<T>,
}

// SAFETY: There are two cases:
//
// * If T is not Send, then the contents is `IfNotSend`. In this case, we conservatively do not impl Send.
// * If T is Send, then the contents is `IfSend`. In this case, we are Send if the contents are.
unsafe impl<T: ?Sized, IfSend, IfNotSend> Send for ImplConditionalOnSend<T, IfSend, IfNotSend>
where
    T: Send,
    IfSend: Send,
{
}

// SAFETY: There are two cases:
//
// * If T is not Send, then the contents is `IfNotSend`. In this case, we conservatively do not impl Sync.
// * If T is Send, then the contents is `IfSend`. In this case, we are Sync if the contents are.
unsafe impl<T: ?Sized, IfSend, IfNotSend> Sync for ImplConditionalOnSend<T, IfSend, IfNotSend>
where
    T: Send,
    IfSend: Sync,
{
}

union Inner<A, B> {
    send: ManuallyDrop<A>,
    not_send: ManuallyDrop<B>,
}

impl<T: ?Sized, IfSend, IfNotSend> ImplConditionalOnSend<T, IfSend, IfNotSend> {
    pub fn new(if_send: impl FnOnce() -> IfSend, if_not_send: impl FnOnce() -> IfNotSend) -> Self {
        if is_send::<T>() {
            let value = if_send();
            Self {
                inner: Inner {
                    send: ManuallyDrop::new(value),
                },
                _t: PhantomData,
            }
        } else {
            let value = if_not_send();
            Self {
                inner: Inner {
                    not_send: ManuallyDrop::new(value),
                },
                _t: PhantomData,
            }
        }
    }

    pub fn call_mut<U>(
        &mut self,
        if_send: impl FnOnce(&mut IfSend) -> U,
        if_not_send: impl FnOnce(&mut IfNotSend) -> U,
    ) -> U {
        if is_send::<T>() {
            if_send(unsafe { &mut self.inner.send })
        } else {
            if_not_send(unsafe { &mut self.inner.not_send })
        }
    }

    pub fn call_ref<U>(
        &self,
        if_send: impl FnOnce(&IfSend) -> U,
        if_not_send: impl FnOnce(&IfNotSend) -> U,
    ) -> U {
        if is_send::<T>() {
            if_send(unsafe { &self.inner.send })
        } else {
            if_not_send(unsafe { &self.inner.not_send })
        }
    }
}

impl<T: ?Sized, IfSend, IfNotSend> Drop for ImplConditionalOnSend<T, IfSend, IfNotSend> {
    fn drop(&mut self) {
        if is_send::<T>() {
            unsafe { ManuallyDrop::drop(&mut self.inner.send) };
        } else {
            unsafe { ManuallyDrop::drop(&mut self.inner.not_send) };
        }
    }
}

fn main() {
    let val = ImplConditionalOnSend::<u32, _, _>::new(|| 0i32, || "foo".to_string());
    val.call_ref(|val| println!("{}", val), |val| println!("{}", val));

    let val2 = ImplConditionalOnSend::<*mut u32, _, _>::new(|| 0i32, || "foo".to_string());
    val2.call_ref(|val| println!("{}", val), |val| println!("{}", val));
}

playground

Is this sound?

How reliable is the is_send trick? The abstraction as-written is unsound if it can return false for a type that is Send (since that makes a non-Send type Send), but the opposite failure case doesn't cause unsoundness as far as I can tell (the type is just unconditionally !Send+!Sync in that case.)

The is_send trick is taken from this thread.

5 Likes

How reliable is the is_send trick? The abstraction as-written is unsound if it can return false for a type that is Send (since that makes a non-Send type Send)

A type can conditionally implement Send if it has a 'static lifetime.

pub struct VeryStrangeType<'a> {
    inner: &'a (),
    not_send: *const (),
}

unsafe impl Send for VeryStrangeType<'static> {}
unsafe impl Sync for VeryStrangeType<'static> {}

// ... example from above omitted for brevity ... //

fn with_static(x: VeryStrangeType<'static>) {
    let val = ImplConditionalOnSend::<VeryStrangeType<'static>, _, _>::new(|| 0i32, || "foo".to_string());
    val.call_ref(|val| println!("{}", val), |val| println!("{}", val));
}

fn with_not_static<'a>(x: VeryStrangeType<'a>) {
    let val = ImplConditionalOnSend::<VeryStrangeType<'a>, _, _>::new(|| 0i32, || "foo".to_string());
    val.call_ref(|val| println!("{}", val), |val| println!("{}", val));
}

fn main() {
    with_static(VeryStrangeType { inner: &(), not_send: std::ptr::null() });
    with_not_static(VeryStrangeType { inner: &(), not_send: std::ptr::null() });
}

This writes 0 for both of them. It thinks VeryStrangeType<'a> is Send, which I suppose is fine according to what you said about how "the opposite failure case doesn't cause unsoundness," but I'm pretty sure the types team hasn't promised to maintain this behavior.

3 Likes

Interesting.

The API as written suffers from unsoundness due to variance & subtyping coercion.

fn main() {
    trait MyTrait<'a> {}
    trait MyTrait2<'a> {}
    struct Wrapper<T: ?Sized>(PhantomData<(*const (), T)>);
    // sound! just contains a PhantomData
    unsafe impl Send for Wrapper<dyn for<'a> MyTrait<'a>> {}

    let val = ImplConditionalOnSend::<Wrapper<dyn for<'a> MyTrait<'a>>, usize, &'static u8>::new(|| 0, || unreachable!());
    let val: ImplConditionalOnSend<Wrapper<dyn MyTrait>, usize, &'static u8> = val;
    val.call_ref(|_| unreachable!(), |r| println!("{r}"));
}
4 Likes

Nice catch.

Here is a version that is invariant in T: playground

We do not guarantee its behavior and it relies on an implementation detail based unstable and unsafe features.
So please don't. It's a toy example, not something to use in production code.

I have warned about this before and yet people keep spreading it without attaching warnings...

3 Likes

Could the compiler perhaps warn about the offending Copy/Clone combination?

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.