Upcasting a trait behind a smart pointer (Rc, etc)

A subtle fact about CoerceUnsized coercions is that they cannot upcast something that is already a trait object.

use std::any::Any;

struct Foo;
trait Trait: Any { }
impl Trait for Foo { }

let _: &Any = &Foo;  // ok

let x: &Trait = &Foo;
let _: &Any = x;   // ERROR: mismatched types

This StackOverflow post has both an explanation and a solution, but the solution only goes halfway:

  • The explanation is that the vtable for Derived doesn't actually necessarily contain anywhere inside of it a full vtable for Base, as that would require duplicating some parts of the vtable or adding layers of indirection.
  • The solution is to slip a conversion method into the trait, so that it becomes "just another vtable method."
trait Trait: TraitAsAny { }
trait TraitAsAny: Any {
    fn as_any(&self) -> &Any;
}

impl<T: Trait> TraitAsAny for T {
    fn as_any(&self) -> &Any { self } // let CoerceUnsized do its magic
}

fn main() {
    let x: &Trait = &Foo;
    let _: &Any = x.as_any();   // "just another method"
}

This works great for any of the three fundamental pointer types; &Trait, &mut Trait, Box<Trait>. But it doesn't work so well for Rc<Trait>.

// Illegal; Rc<Self> is not a valid self type
impl<T: Trait> TraitToAny for T {
    fn rc_any(self: Rc<Self>) -> Rc<Any> { self }
}

// Not object safe; now Rc<Trait> can't even exist!
impl<T: Trait> TraitToAny for T {
    fn rc_any(this: Rc<Self>) -> Rc<Any> { this }
}

// Defeats the purpose because TraitToAny would no longer
// be a supertrait of Trait!
impl<T: Trait> TraitToAny for Rc<T> {
    fn rc_any(self) -> Rc<Any> { self }
}

tl;dr: How do I turn my Rc<Trait> into an Rc<Any>?

...oh, and, uh, it'd be great if I could downcast that Rc<Any> into an Rc<KnownType>, afterwards, too.

2 Likes

By the way, I just had a terrible idea.

// same as above
trait Trait: TraitAsAny { }
trait TraitAsAny: Any {
    fn as_any(&self) -> &Any;
}
impl<T: Trait> TraitAsAny for T {
    fn as_any(&self) -> &Any { self }
}

// and this is... uh oh.
fn downcast_rc<B: Trait>(rc: Rc<Trait>) -> Option<Rc<B>> {
    rc.as_any().downcast_ref::<B>()
        .map(|_| ()) // end the borrow
        .map(move |_| unsafe { Rc::from_raw(Rc::into_raw(rc) as *const B) })
}

fn main() {
    struct Foo;
    struct Bar;
    impl Trait for Foo { }
    impl Trait for Bar { }
    
    let foo: Rc<Trait> = Rc::new(Foo);
    assert!(downcast_rc::<Bar>(foo.clone()).is_none());
    assert!(downcast_rc::<Foo>(foo.clone()).is_some());
}

Speak now or forever hold your peace.

1 Like

This will probably become possible with arbitrary self types

You can with Rc::downcast

1 Like

Does fat pointer break it. (not completely sure.)
assert_ne!(::std::mem::size_of::<*const Trait>() , ::std::mem::size_of::<*const Foo>());

Rc:downcast only works from Rc<Any + 'static> so no use from Trait.

If you add to each structure a field;
self_rc: Weak< _ <Self>>; // _ RefCell if you want
Make all constructor calls return an Rc. Have no code to take ownership out of Rc.
You can then add any launch pad to the trait.

2 Likes

Ah, I think I'll be able to do something similar to this. (I think I can put an Rc<T> alongside the Rc<Trait>). If it doesn't work out I'll come back with more details about the problem I'm trying to solve.

Edit: on further thought this probably won't work, for the same reason I had to store Rc<Trait> to begin with. (There was a reason I did not just store Rc<T>!)

FWIW, I think your “uh oh” approach is fine :slight_smile:

Okay, so slipping in an extra Rc didn't work.

Also, I'm beginning to think my overall design is just too difficult to implement anyways and that I need to rethink the whole thing over.


What I had was something like this:

struct Context { /* stuff */ }

// Represents an expensive computation
trait Analyze {
    type Data: Data;

    fn analyze(&self, ctx: &Context) -> Self::Data;
}

// An intermediate form from which various types of reports may be produced
trait Data {
    // a variety of output functions...
}

There could be data dependencies between some of the Analyze objects, and so I wanted to allow one to take another (statically typed) during construction.

struct AnalysisA;
struct AnalysisB<'a>(&'a AnalysisA, OtherStuff);

...well, actually, some of the computations were expensive, so I wanted each one to only run once.

struct CachedAnalysis<A: Analysis + ?Sized> {
    cache: Rc<RefCell<Option<Rc<A::Data>>>>,
    analysis: Rc<A>,
}

struct AnalysisA;
struct AnalysisB(CachedAnalysis<AnalysisA>, OtherStuff);

But I also wanted to make the analyses composable. The idea was I could build up one "composite" Analysis from all the other analyses, and have it produce some grand Data value whose output functions synthesize the results from each of the individual Datas.

But in order to have a homogenous collection of Analyses, I needed to erase the types of not just the analyses themselves, but also their associated types.

pub type ErasedAnalysis = CachedAnalysis<Analyze<Data=Data>>;
pub struct CompositeAnalysis(Vec<ErasedAnalysis>);
pub struct CompositeData(Vec<Rc<Data>>);

And here's the pincher: Of course, we would want the type-erased analyses to be tied to the same caches as the originals, right? But we can't just turn an Rc<RefCell<Option<Rc<A>>>> into an Rc<RefCell<Option<Rc<Data>>>> without building a new RefCell (because the size of its contents must change to accommodate the vtable).

Therefore, the Data in CachedAnalysis must always have its type erased, even when its actual type is already known. And because AnalysisB doesn't have much use for storing AnalysisA if it can't see that analysis' data, this is why downcasting was necessary.

pub struct ErasedData(Rc<Data>);
pub struct CachedAnalysis<A: ?Sized> {
    cache: Rc<RefCell<Option<ErasedData>>>  // <---- !!
    analysis: Rc<A>,
}

pub type ErasedAnalysis = CachedAnalysis<Analyze<Data=ErasedData>>;

pub struct CompositeAnalysis(Vec<ErasedAnalysis>);
pub struct CompositeData(Vec<ErasedData>);

impl ErasedData {
    pub fn downcast<D: Data>(&self) -> Option<Rc<D>>;
}

impl<A: Analysis> Analyze for CachedData

impl<A: Analysis> CachedAnalysis<A> {
    pub fn get_or_compute(&self, ctx: &Context) -> Rc<A::Data>
    { /* compute if necessary, then use `downcast()` */ }

    pub fn erase_type(self) -> ErasedAnalysis
    { /* wrap `analysis` in a newtype whose Data is ErasedData */ }
}

Anyways, after getting over the Rc speedbump, I had to fix several bugs where I didn't deref enough times and had the wrong type get upcast into a trait object, and even now there's still one bug left that I'm too tired to figure out. You can see what I have here, but I can tell it's just going to be miserable if I continue this way.

For now I'm trying an alternate design that decouples computations from outputs. Computation is "monad-like" and the outputs are "monoid-like", so the problems are probably best solved in orthogonal ways. For now I'm making a trait with combinators for self-caching computations, and I imagine the outputs will be easily managed somehow.


Edit: ...wait a minute, what's the trait for then? Why am I composing objects that compute something and cache it, when I could instead just call regular functions that compute the thing right then and there, and use those values as regular arguments to other functions?! I think I need a time out...

Could you model this (in spirit/technique) like an Iterator or Future chain, with combinators changing inputs/outputs and transitioning the state machine?

This is my uneducated understanding;
Like you say in edit you could first be just starting with regular functions.
trait Analyze::analyze should instead be returning Rc<Self::Data>.
You need to wrap the regular functions to use it.
CachedAnalysis::analyze then simply checks cache or calls and store.
ErasedAnalysis would need to be a new structure taking generic A: Analysis.

p.s. "Data" identifier as trait, associate type, concrete type, trait object is confusing.