Idea: include `TypeId` in all trait object vtables

Sometimes, one finds oneself wanting to combine the functionality of the dyn Any with that of dyn SomeOtherTrait. However, this currently requires custom unsafe code, or third-party libraries like mopa.

It would be convenient if this functionality was built in to all trait objects. This could be accomplished by including the TypeId of the concrete type inside every trait object vtable. The Any trait could then become a magic lang item, with the implementation of dyn Any making use of this vtable entry. This would make it possible for any 'static trait object to upcast to dyn Any.

Example

use core::any::Any;

fn foo(obj: &dyn Debug) {
    dbg!(obj);
    if let Some(num) = obj.downcast_ref::<i32>() {
        dbg!(num + 17);
    }
}

Miscellanea

  • This would change the behavior of <dyn SomeOtherTrait as dyn Any>::type_id() (where SomeOtherTrait does not have Any as a supertrait). Previously, this would give the TypeId of the type dyn SomeOtherTrait; after this change, it would give the TypeId of the underlying concrete type.
  • This change might help resolve longstanding soundness bug #57893, because an explicitly magical dyn Any would no longer need to rely on an incoherent impl for its functionality. However, we would still have to find a solution for reimplementations of Any in the ecosystem (like the aforementioned mopa), before the issue could be fixed for good.
  • One potential drawback is the impact on binary sizes. Adding a single entry for the TypeId shouldn’t bloat vtable sizes by too much. However, it would prevent de-duplication of vtables that were previously identical.

It doesn't require unsafe code:

use std::fmt::Debug;
use std::any::Any;

trait Foo: Any + Debug {
    fn as_any(&self) -> &(dyn Any + 'static);
}

impl<T: Any + Debug> Foo for T {
    fn as_any(&self) -> &(dyn Any + 'static) {
        self
    }
}

And with trait upcasting it will become even more convenient (and performant).

Doing it for all traits will violate the zero-cost abstraction principle.

Yes, you are correct that trait upcasting makes this easier.

However, that approach still has limitations. Notably, it only works if you control how the trait object is produced. If you get a dyn SomeTrait from an API you don’t control, you cannot downcast it.

1 Like

This vtable bloating would in general be bad for embedded (even if deduplication could work). It is not a zero cost abstraction, in fact everyone would have to pay for it, even if they don't use it. I don't think that is acceptable, especially for such a niche feature.

1 Like

I think this depends heavily on what the impact would be in practice. For example, people were concerned about whether the recently-stabilized dyn trait upcasting feature would bloat vtables too much, but data showed the impact was minimal. However, it’s possible that the size increase from this would be worse? It would be a smaller impact per trait, but bloating all traits instead of just a few. Hard to tell without testing…

Is there a simple way to make this opt-in? Make a (probably magic) Typed<dyn Trait>/Tagged<dyn Trait>/dyn🚲 Trait that just stores a type id?