Why doesn't Rust support trait object upcasting?
Asked Answered
C

6

85

Given this code:

trait Base {
    fn a(&self);
    fn b(&self);
    fn c(&self);
    fn d(&self);
}

trait Derived : Base {
    fn e(&self);
    fn f(&self);
    fn g(&self);
}

struct S;

impl Derived for S {
    fn e(&self) {}
    fn f(&self) {}
    fn g(&self) {}
}

impl Base for S {
    fn a(&self) {}
    fn b(&self) {}
    fn c(&self) {}
    fn d(&self) {}
}

Unfortunately, I cannot cast &Derived to &Base:

fn example(v: &Derived) {
    v as &Base;
}
error[E0605]: non-primitive cast: `&Derived` as `&Base`
  --> src/main.rs:30:5
   |
30 |     v as &Base;
   |     ^^^^^^^^^^
   |
   = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait

Why is that? The Derived vtable has to reference the Base methods in one way or another.


Inspecting the LLVM IR reveals the following:

@vtable4 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

@vtable26 = internal unnamed_addr constant {
    void (i8*)*,
    i64,
    i64,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*,
    void (%struct.S*)*
} {
    void (i8*)* @_ZN2i813glue_drop.98717h857b3af62872ffacE,
    i64 0,
    i64 1,
    void (%struct.S*)* @_ZN9S.Derived1e20h9992ddd0854253d1WaaE,
    void (%struct.S*)* @_ZN9S.Derived1f20h849d0c78b0615f092aaE,
    void (%struct.S*)* @_ZN9S.Derived1g20hae95d0f1a38ed23b8aaE,
    void (%struct.S*)* @_ZN6S.Base1a20h57ba36716de00921jbaE,
    void (%struct.S*)* @_ZN6S.Base1b20h3d50ba92e362d050pbaE,
    void (%struct.S*)* @_ZN6S.Base1c20h794e6e72e0a45cc2vbaE,
    void (%struct.S*)* @_ZN6S.Base1d20hda31e564669a8cdaBbaE
}

All Rust vtables contain a pointer to the destructor, size and alignment in the first fields, and the subtrait vtables don't duplicate them when referencing supertrait methods, nor use indirect reference to supertrait vtables. They just have copies of the method pointers verbatim and nothing else.

Given that design, it's easy to understand why this does not work. A new vtable would need to be constructed at runtime, which would likely reside on the stack, and that isn't exactly an elegant (or optimal) solution.

There are some workarounds, of course, like adding explicit upcast methods to the interface, but that requires quite a bit of boilerplate (or macro frenzy) to work properly.

Now, the question is - why isn't it implemented in some way that would enable trait object upcasting? Like, adding a pointer to the supertrait's vtable in the subtrait's vtable. For now, Rust's dynamic dispatch doesn't seem to satisfy the Liskov substitution principle, which is a very basic principle for object-oriented design.

Of course you can use static dispatch, which is indeed very elegant to use in Rust, but it easily leads to code bloat which is sometimes more important than computational performance - like on embedded systems, and Rust developers claim to support such use cases of the language. Also, in many cases you can successfully use a model which is not purely Object-Oriented, which seems to be encouraged by Rust's functional design. Still, Rust supports many of the useful OO patterns... so why not the LSP?

Does anyone know the rationale for such design?

Countertype answered 20/2, 2015 at 15:49 Comment(5)
As a side note: Rust is not object-oriented language. Traits are not interfaces, they are more like type classes from Haskell. Rust also does not have subtyping, so LSP is somewhat inapplicable to it because its definition is tied to the subtyping relationship.Curdle
Still, as I said, Rust supports a lot of OO-style abstractions, and traits are allowed to inherit, forming something akin to a type hierarchy. For me, it would seem natural to support LSP for trait objects, even if OO isn't the main paradigm of the language.Countertype
Please make sure to upvote useful answers and mark an answer as accepted if it solved your problem! If no answer is acceptable, consider leaving comments explaining why, or edit your question to phrase the problem differently.Pushball
There's a Rust issue for this question: github.com/rust-lang/rust/issues/5665 (which I see you've already found; just placing a link to it here.)Florella
how did you get this LLVM IR?Thinkable
C
76

Actually, I think I got the reason. I found an elegant way to add upcasting support to any trait that desires it, and that way the programmer is able to choose whether to add that additional vtable entry to the trait, or prefer not to, which is a similar trade-off as in C++'s virtual vs. non-virtual methods: elegance and model correctness vs. performance.

The code can be implemented as follows:

trait Base: AsBase {
    // ...
}

trait AsBase {
    fn as_base(&self) -> &Base;
}

impl<T: Base> AsBase for T {
    fn as_base(&self) -> &Base {
        self
    }
}

One may add additional methods for casting a &mut pointer or a Box (that adds a requirement that T must be a 'static type), but this is a general idea. This allows for safe and simple (although not implicit) upcasting of every derived type without boilerplate for every derived type.

Countertype answered 22/2, 2015 at 23:49 Comment(7)
Unfortunately the usage area of this impl Trait approach is somewhat narrow, if a complex trait architecture is used. play.integer32.com/… Without the possibility of elimination of the intersecting implementations (i.e. impl <T: AsBranchOne + !AsBranchTwo>) it is required to install the as_*() methods directly into the nesting trait to prevent conflicts.Tadzhik
@Pushball Should this be AsRef<Base> in today's Rust?Colossus
@Colossus AsRef has been available since Rust 1.0, but I'm not sure if you can use it here or not. Some quick attempts show various errors.Pushball
"One may add additional methods for casting a &mut pointer or a Box" — could you provide an example for the Box case? It's not clear to me how you make it work in that case without using unsafe.Dowie
@Countertype I have a similar approach in my codebase, but I struggle to get it working with Box. Have you been successfully doing it already?Comprador
The crate downcast-rs implements the above pattern for upcasting to Any, which then allows for downcasting to a concrete type. It also supports Box. crates.io/crates/downcast-rsTransatlantic
This will only work if you have a pair of Traits, one inheriting from another. It doesn't generalize to more complex hierarchies, unless you add a "as_X" for each "X" you want to upcast to.Tolerate
A
29

As of Jun 2017, the status of this "sub-trait coercion" (or "super-trait coercion") is as follows:

  • An accepted RFC #0401 mentions this as a part of coercion. So this conversion should be done implicitly.

    coerce_inner(T) = U where T is a sub-trait of U;

  • However, this is not yet implemented. There is a corresponding issue #18600.

There is also a duplicate issue #5665. Comments there explain what prevent this from being implemented.

  • Basically, the problem is how to derive vtables for super-traits. Current layout of vtables is as follows (in x86-64 case):
    +-----+-------------------------------+
    | 0- 7|pointer to "drop glue" function|
    +-----+-------------------------------+
    | 8-15|size of the data               |
    +-----+-------------------------------+
    |16-23|alignment of the data          |
    +-----+-------------------------------+
    |24-  |methods of Self and supertraits|
    +-----+-------------------------------+
    
    It doesn't contain a vtable for a super-trait as a subsequence. We have at least to have some tweaks with vtables.
  • Of course there are ways to mitigate this problem, but many with differing advantages/disadvantages! One has a benefit for the vtable size when there is a diamond inheritance. Another is supposed to be faster.

There @typelist says they prepared a draft RFC which looks well-organized, but they look like disappeared after that (Nov 2016).

Anapest answered 14/6, 2017 at 8:53 Comment(1)
This now works on nightly (under #![feature(trait_upcasting)]) and even a candidate for stabilization! But there were some concerns about executable sizes.Market
T
23

I ran into the same wall when I started with Rust. Now, when I think about traits, I have a different image in mind than when I think about classes.

trait X: Y {} means when you implement trait X for struct S you also need to implement trait Y for S.

Of course this means that a &X knows it also is a &Y, and therefore offers the appropriate functions. It would require some runtime-effort (more pointer dereferences) if you needed to traverse pointers to Y's vtable first.

Then again, the current design + additional pointers to other vtables probably wouldn't hurt much, and would allow easy casting to be implemented. So maybe we need both? This is something to be discussed on internals.rust-lang.org

Thaxton answered 20/2, 2015 at 16:39 Comment(1)
can you develop ? Particularly how to handle or workaround with a different desing ? I exactly run into the same wall after my first two hours of rust coding :/Scansion
M
5

Trait object upcasting was stabilized on nightly (#118133), scheduled for Rust 1.76.0 (8 February, 2024). The code in the question works almost as-is, except the need to add dyn to trait objects.

Edit: Stabilization was reverted due to soundness issues.

Market answered 4/12, 2023 at 17:20 Comment(0)
C
2

This feature is so desired that there is both a tracking issue for adding it to the language, and an dedicated initiative repository for the people contributing to implementing it.

Tracking Issue: https://github.com/rust-lang/rust/issues/65991

Initiative Repository: https://github.com/rust-lang/dyn-upcasting-coercion-initiative

Contrapositive answered 15/9, 2021 at 14:50 Comment(0)
C
1

This is now working on stable rust, you can upcast to the base trait also you can call base trait functions directly from the derived trait object

trait Base {
   fn a(&self) {
     println!("a from base");
   }
}

trait Derived: Base {
   fn e(&self) {
     println!("e from derived");
   }
}

fn call_derived(d: &impl Derived) {
   d.e();
   d.a();
   call_base(d);
}

fn call_base(b: &impl Base) {
   b.a();
}

struct S;
impl Base for S {}
impl Derived for S {}

fn main() {
   let s = S;
   call_derived(&s);
}

playground link

Courtund answered 29/10, 2022 at 13:0 Comment(3)
What version of stable Rust?Heigho
@CraigMcQueen I'm not sure about the lowest version. but the playground link above shows that it works on 1.64.0.Courtund
The expected feature is not on stable (yet). What wanted is casting from &dyn Derived to &dyn Base. Casting from &impl Derived to &impl Base was always allowed.Market

© 2022 - 2024 — McMap. All rights reserved.