&mut dyn SuperTrait calling SubTrait [in closure]

Another dyn trait question, this time with references.
I have trait A and trait B : A
in I have a closure which gets an object of &mut dyn B and I want to use that to call a method which takes &mut dyn A, when I do that I get an error: "expected trait A, found trait B"

here is a playground.

It works fine outside of the closure.. I'm wondering if/how I can get the call_a to work?
Thanks!

it looks like I can create an as_a method in the B trait that returns an A, but it seems like i shouldn't need to do that:

You might be thinking a bit much in terms of OOP inheritance. A trait B: A does not mean that B inherits A - no such inheritance exists in Rust. It says that all impls of B must also impl A - the subtle distinction is that it doesn’t mean that a B trait object can be substituted in places an A trait object is needed.

The reason is a B trait object is a pair of two ptrs: one pointing at the data/value behind the trait object, and the second is to the vtbl that contains the impl’s B methods. This vtbl, crucially, doesn’t have the A methods, even though we know the impl must have them (due to trait B: A). This is the reason you can’t just upcast from B to A trait objects.

The trampoline method I mentioned in the other thread achieves this manually: when you call into as_B, you dispatch to a method on that type which then returns you a new trait object, with that impl’s A vtbl now; since the dispatch lands in a concrete type, the compiler knows what A vtbl to use.

It’s possible that in the future Rust gains some automatic support for casts like this, but there’s nothing on the horizon AFAIK.

@vitalyd beat me to the punch.

This is not quite true. The vtable for B contains all of the methods for A, or else we wouldn't be able to call them!

The problem is that there is no contiguous region of the vtable for B which can be singled out as a "complete" vtable for A. (i.e. a value of type dyn A). It's really a problem of memory layout. And it is difficult to fix due to the "diamond problem:"

trait A { fn a(&self); }
trait B: A { fn b(&self); }
trait C: A { fn c(&self); }
trait D: B + C { fn d(&self); }

Suppose the vtable layout for B should contain a followed by b, and that the vtable layout for C contains a followed by c. How would the methods be laid out in the vtable for D so that &dyn D can be coerced either to &dyn B or &dyn C?

2 Likes

Yes I handwaved a bit there (I’m on mobile - don’t shoot me :slight_smile:). The methods are physically in there, yes, but you can’t materialize a new vtbl just for them - so it’s as-if they’re not in there for purposes of casting across trait objects. As mentioned, this is what the trampoline (or landing pad) accomplishes by hand.

1 Like

@vitalyd @ExpHP thanks!

@vitalyd I implemented the trampoline in this approach and that worked well.. it is just kind of odd to me that having this in a closure changes things.. though I guess the compiler knows that X impl's both A & B but the information that &mut dyn B impl's A is lost in the closure? Okay! :slight_smile: that works.

I might’ve missed this but which closure are you referring to?

Well, teeeechnically, &mut dyn B doesn't impl A. It doesn't even impl B! You would have to write that impl yourself:

impl<'a, T: A + ?Sized> A for &'a mut T {
    fn a(&self) { (**self).a() }
}

impl<'a, T: B + ?Sized> B for &'a mut T {
    fn b(&self) { (**self).b() }
}

If you do this, then you will find that there is another, perhaps unusual way out of your predicament: Allowing &mut &mut dyn B to coerce into &mut dyn A:

fn main() {
    let mut x = X {};
    let f = move |mut context: &mut dyn B| {
        call_b(&mut context);
        call_a(&mut context);
    };

    f(&mut x);
}

The coercion from &mut &mut dyn B to &mut dyn A works because it is an "unsizing" coercion (the same kind of coercion that produced the original &mut dyn B). Basically &mut dyn B is now a statically known type (with known size) that implements A, and it is being "unsized" into dyn A. The same coercion from &mut dyn B did not work because dyn B is already unsized.

In this manner I guess it does seem like a kind of arbitrary limitation that it refuses to do the &mut dyn B -> &mut dyn A cast. But the limitation might make sense in generic contexts (i.e. dealing with some T that we don't necessarily know is dyn B). Not sure.

2 Likes

oops, sorry:

I have a struct X that implements both A and B and closure with an argument |context: &mut dyn B|
I can create an object x: X and can pass it to method that takes a &mut dyn A or a method that takes &mut dyn B but the compiler complains if I call a method that takes &mut dyn A using the context: &mut dyn B from the closure.

@ExpHP you're blowing my mind here, I think I'll stick with the trampoline :slight_smile:

It's perhaps a relatively small point in all this, but shouldn't this be linked to the conceptual model of what trait A : B { ... } means? Without advocating one over the other:

One option is to say "that means A extends B", in which case upcasting should just work.

But on the other hand, the point has been made in this thread that it may be wiser to not take the meaning "A extends B", in which case "upcasting"** at the language level, while occasionally useful (and still supported through an additional boilerplatey-but-inlinable-and-thus-free-at-runtime method that effectively casts B to A, like as_a() mentioned above), would be muddling the conceptual waters: why allow casts between arbitrary types*? Similarly to taking a String and trying to cast that directly to an f64, it doesn't seem to make much sense.

*I know, traits aren't types. So it's more by analogy to types, but the point still stands.
**At this point it might be more appropriate to call it sidecasting, as in casting not up or down but sideways. If it sounds dangerous or unsound to you, that's because in general it is.

I think this really boils down to how the compiler, today, synthesizes vtbls. If it was able, through whatever means, to separate out the A vtbl from a B vtbl, given trait B: A, then you could allow the coercion from dyn B to dyn A, or so it seems. So then to tease out the conceptual aspect, a question may arise: if this were possible at the technical level, would it be something that should be possible in Rust? This might be good fodder for whenever trait objects are rethought (if ever).

Note the method isn't really inlinable since it's a dynamic call through the trait object. Not very important here though.

1 Like