[Idea] (Almost) arbitrary trait object to trait object cast using a provider mechanism

I used a ton of #![feature(provide_any)] in my own project, and while reading the recent RFC PR on supertrait upcast (this), I put two and two together and thought, "wait, can't we use provide_any to do trait to trait casts?"

Obviously provide_any in its current form is not going to work, so I whipped something together really quick:

use std::any::TypeId;

#[repr(transparent)]
pub struct Demand(dyn Erased);
pub trait LateralCast: 'static {
    fn cast(self: Box<Self>, demand: &mut Demand);
}
pub struct Receiver<'a, I: LateralCast + ?Sized>(&'a mut TaggedOption<I>);

impl<'a, I: LateralCast + ?Sized> Receiver<'a, I> {
    pub fn provide(&mut self, value: Box<I>) {
        self.0 .0 = Some(value)
    }
}

/// Represents a type-erased but identifiable object.
///
/// This trait is exclusively implemented by the `TaggedOption` type.
unsafe trait Erased {
    /// The `TypeId` of the erased type.
    fn tag_id(&self) -> TypeId;
}

unsafe impl<I: LateralCast + ?Sized> Erased for TaggedOption<I> {
    fn tag_id(&self) -> TypeId {
        std::any::TypeId::of::<I>()
    }
}

impl Demand {
    fn new(erased: &mut (dyn Erased)) -> &mut Demand {
        // SAFETY: transmuting `&mut (dyn Erased)` to `&mut Demand` is safe
        // since `Demand` is repr(transparent).
        unsafe { &mut *(erased as *mut (dyn Erased) as *mut Demand) }
    }
    pub fn maybe_provide<T: LateralCast + ?Sized>(&mut self) -> Option<Receiver<T>> {
        if let Some(res @ TaggedOption(None)) = self.0.downcast_mut::<T>() {
            Some(Receiver(res))
        } else {
            None
        }
    }
}

impl dyn Erased {
    /// Returns some reference to the dynamic value if it is tagged with `I`,
    /// or `None` otherwise.
    #[inline]
    fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<I>>
    where
        I: LateralCast + ?Sized,
    {
        if self.tag_id() == TypeId::of::<I>() {
            // SAFETY: Just checked whether we're pointing to an I.
            Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<I>>() })
        } else {
            None
        }
    }
}

#[repr(transparent)]
struct TaggedOption<I: LateralCast + ?Sized>(Option<Box<I>>);
impl<I: LateralCast + ?Sized> TaggedOption<I> {
    fn as_demand(&mut self) -> &mut Demand {
        Demand::new(self as &mut (dyn Erased))
    }
}

pub fn lateral_cast<A, B>(from: Box<A>) -> Option<Box<B>>
where
    A: LateralCast + ?Sized,
    B: LateralCast + ?Sized,
{
    let mut tagged = TaggedOption::<B>(None);
    from.cast(tagged.as_demand());
    tagged.0
}

#[cfg(test)]
mod test {
    use super::LateralCast;
    trait A: LateralCast {}
    trait B: LateralCast {}
    struct Struct;
    impl A for Struct {}
    impl B for Struct {}
    impl LateralCast for Struct {
        fn cast(self: Box<Self>, demand: &mut crate::Demand) {
            if let Some(mut receiver) = demand.maybe_provide::<dyn A>() {
                receiver.provide(self);
            } else if let Some(mut receiver) = demand.maybe_provide::<dyn B>() {
                receiver.provide(self);
            }
        }
    }
    #[test]
    fn test() {
        let a = Box::new(Struct) as Box<dyn A>;
        let b = crate::lateral_cast::<_, dyn B>(a);
        assert!(b.is_some());
    }
}

This allows you to cast between any 2 traits that has LateralCast as their supertrait. The user has to impl LateralCast to provide the traits they want to be able to cast to, which is the only con here, I think.

Does this make sense? Could this potentially be unsound?

Looks super convoluted :frowning: Correct me if I'm wrong, but that works only for Box<dyn Trait> objects, right? It doesn't look like there is a good way to extend it to arbitrary trait objects. Also, you haven't really implemented casts between trait objects. You have tacked on some complex dynamic typing system on the existing structs, and you only use it in erased form, i.e. weaker than Any. I don't really understand how your code works (and whether it works at all), but the fact that you need to manually implement the conversions for all structs which should support it makes it unsuitable as a generic cast system. Note that the upcasts in the proposed RFC don't require any cooperation from the implementing types (which could be located in some downstream crates we know nothing of).

Note that the upcasts in the proposed RFC don't require any cooperation from the implementing types

I think I didn't make myself clear. This is not intended as an alternative to the RFC. This is a library bandaid for something we don't yet have (trait to non-supertrait trait cast). Compiler support would be best, but this is something would give you something similar with a bit of manual work.

As for how this works, this is modeled after std::any::Provider, have a look, it's really clever.

I was assuming you are proposing it as an addition to the language, e.g. as a stdlib trait. I think that for that purpose it is too limited.

Not necessarily to libstd. Maybe as a crate.

Support for other smart pointers can be add in the same way, and the boilerplates can be generated by a macro. I think this can be useful.

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