Cross trait casting suggestion: second attempt

I’ve been thinking about cross-trait casting (e.g., casting dyn Any to dyn SomeTrait if the underlying type implements dyn SomeTrait).

I made a previous attempt before, but the implementation of it was severely complex due to how the rust compiler works

So I came up with another method

What it enables

Given:

#[exhaustive]
trait Behavior: Any { ... }

If I have dyn Any, I want to be able to attempt:

let casted: Option<&dyn Behavior> = any.downcast_ref::<dyn Behavior>()

Motivation

It will enable dyn trait pattern matching, which will also enable many other ways of making programs.

Say you are making a game, and your bullet collides with an entity. If you want to damage it, you would want to check if the object has a Damageable trait so you can call its damage method, assuming not everything in the game can be damaged. This method can be seen as another way of composition. A different pattern from having a collection of components (Vec<Box<dyn Component>>), or the ECS pattern.

bevy_reflect, which is used in the game engine “bevy”, has functionality that enables you to cast between 2 unrelated traits.

But it involves macros and you have to manually register the trait to enable cross trait casting, and tell the struct “yeah you can be casted to this trait even if the compiler does not know your concrete type”

GUI/widget capabilities: Clickable, Draggable, Focusable, Scrollable, etc. In GUI frameworks, you may want “if this widget supports X, call X” at runtime without a giant enum or manual registration.

Making casting between unrelated traits a natural part of the language would make this much easier

Idea

Introduce a trait-level marker attribute:

#[exhaustive]
trait SomeTrait { ... }

doesn't need to be a marker. Could be a keyword or something else, but let's keep it as a marker called "exhaustive" for this post.

If a trait is marked #[exhaustive], then the compiler enforces certain rules that would make cross trait casting much easier to implement

Rule 1: A crate may only implement an exhaustive trait for types it owns.

Equivalently:

  • impl SomeTrait for LocalType {} is allowed.
  • impl<T> SomeTrait for Vec<T> {} is rejected (type not owned).
  • Blanket impls for the trait are also not allowed

The core problem with cross-trait downcasting is that two unrelated crates could each see the “same” concrete type but a different set of trait impls, due to downstream impls and blanket impls. With separate compilation, that makes any global “type → trait set” table incoherent unless you delay codegen or centralize it, both of which are hostile to Rust’s model.

#[exhaustive] sidesteps this by making the implementation set for a given type deterministic and crate-local:

  • Every impl of an exhaustive trait for T must live in T’s defining crate.
  • Therefore no other crate can add impls later.
  • Therefore all crates see the same exhaustive-trait set for a type.

This makes a restricted form of cross-trait downcasting feasible.

This rule applies even to the crate that defines the exhaustive trait. Ownership of the type is what matters, not ownership of the trait.

Rule 2 (revised): An impl is only allowed if the trait’s generic arguments are fully determined by the implementing type.

Concretely: in impl<...> ExhaustiveTrait<TraitArgs...> for SelfTy<TypeArgs...>, every generic parameter that appears in TraitArgs... must also be a generic parameter of SelfTy, or be a concrete argument (eg i32).

Examples

#[exhaustive]
trait MyTrait<T> {}

// ERR: creates infinite implementations for the exhaustive trait.
impl<U> MyTrait<U> for MyType {}

// OK: trait args are concrete → finite
impl MyTrait<i32> for MyType {}

// OK: trait arg is tied to Self’s generic parameter → 
// each concrete MyType<T> has exactly one matching impl
impl<T> MyTrait<T> for MyType<T> {}

// also OK: still determined by Self
impl<T> MyTrait<Vec<T>> for MyType<T> {}

This makes it impossible for type "T" to implement an infinite amount of #[exhaustive] traits, which is what we do not want, since the implementation set of #[exhaustive] traits should be deterministic.

Because the exhaustive-trait implementation set for the concrete type is deterministic, the compiler/runtime can safely use per-type metadata to answer “does this type implement Behavior?” in different crates without coherence surprises

Rule 3: Exhaustive traits and all their implementors must be 'static.

This gives us the ability to map traits to vtables. (TraitTypeId -> dyn VTable)

Where are the VTables stored?

Each type will have an array stored globally. Something like [(TypeId, TraitVTable)], where TypeId is the TypeId of the dyn Trait. this is possible because of the 'static only restriction, and this is similar to how C# does it.

Essentially, an iteration would be done, until it finds the relevant vtable. If it cannot be found, None would be returned. Ofc, this makes it O(n), but C# has a fast path which we could be able to emulate, which I have yet to fully understand. Something we could discuss.

Inside the vtable of every trait object, a ptr that represents the array can be found. Types that do not have any exhaustive traits implemented, AND non static types could simply have a ptr that represents an empty array

A quick sketch

struct VTable {
    *drop_in_place
    usize size;
    usize align;
    // method fns for TraitX, in trait order
    *method1
    *method2
    ...

    // NEW: pointer to exhaustive trait map for concrete T. points to the first implementation map if theres any
    ExhaustiveEntry* exhaustive_ptr;
    usize exhaustive_len;
};

What do u guys think? I am not married to this exact design, so we can reevaluate this approach and make appropriate changes.

Now that i really think about it, based on the restrictions as to how generics are implemented on exhaustive traits, i guess its better to just enforce that exhaustive traits should not have generics, and associated types should be used instead

I was stuck up on how c#, java and some other languaes did it and forgot that rust has exactly what I was looking for`

Edit: this was re-evaluated to a more flexible approach, generics are allowed again, though they have restrictions as to how they are implemented (the original post has been edited to explain this in more detail)

Do you have concrete examples of existing traits that could benefit from this? Particularly traits that can satisfy thr given rules. I tried giving a look at a couple of popular traits you might want to downcast to and most break rule n.2 n.1. The only exception seem to be a couple of traits in core like Debug.

After further thought, I believe we can actually make rule 2 a bit more flexible.

it should be "generic arguments cannot be used on a trait's generic parameters if its also not used in the implementors generic parameters, and if its not concrete"

so for example

impl<T> MyTrait<T> for MyType{}

is invalid,

but

impl MyTrait<i32> for MyType{}

is valid

impl<T> MyTrait<T> for MyType<T>{}

is also valid

it should still let the implementation set of exhaustive traits for a type still be deterministic

Sorry, I meant rule n.1 :sweat_smile:

I would still appreciate some concrete usecases though.

Oh I see. But you did make me re-evaluate rule 2 for the better lol

Yeah, Rule 1 is a big restriction, but it’s also the thing that makes cross-trait casting sound under separate compilation.

On “what traits benefit / satisfy the rules”: I’m not aiming this at existing std traits (most of those shouldn’t become exhaustive anyway). This is meant for application/framework traits where implementations are naturally done in crates where the types live

I did put a use case in the original post in the Motivation section, in cased you missed it

It will enable dyn trait pattern matching, which will also enable many other ways of making programs.

Say you are making a game, and your bullet collides with an entity. If you want to damage it, you would want to check if the object has a Damageable trait so you can call its damage method, assuming not everything in the game can be damaged. This method can be seen as another way of composition. A different pattern from having a collection of components (Vec<Box<dyn Component>>), or the ECS pattern.

I'll also add:

GUI/widget capabilities: Clickable, Draggable, Focusable, Scrollable, etc. In GUI frameworks, you may want “if this widget supports X, call X” at runtime without a giant enum or manual registration. The concrete widget types and their capability implementation typically live together, so Rule 1 holds.