rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Exhaustive traits. Traits that enable cross trait casting between trait objects.

Open izagawd opened this issue 1 month ago • 10 comments

This RFC proposes #[exhaustive] traits to enable sound cross-trait casting for trait objects.

For any concrete type T, the set of #[exhaustive] traits it implements is finite and deterministic, allowing runtime checks like “if this dyn A also implements dyn B, cast and use it.”

The design adds a per-type exhaustive trait→vtable map and enforces four rules (type-crate ownership of implementation, trait arguments determined by Self, object safe, and 'static only) to keep the mapping coherent under separate compilation.

Use cases include capability-based game entities (e.g., Damageable, Walkable traits) and GUI widgets (e.g., Clickable, Scrollable), avoiding manual registry/macro approaches such as bevy_reflect.

This enables patterns such as: "if dyn Character is dyn Flyable, then character.fly()"

Rendered

izagawd avatar Nov 24 '25 22:11 izagawd

We could avoid Rule 1 by building the vtable lookup table externally in ./target, no?

As &'static [(TypeId, TraitVTable)] is a nasty lookup, https://internals.rust-lang.org/t/casting-families-for-fast-dynamic-trait-object-casts-and-multi-trait-objects/12195 proposed defining fresh indices for each "casting family", so that lookup takes place in &'static [TraitVTable]. In that, the families kept the vtable lookup tables small, but this could be avoided perhaps. And it could all be done via procmacros, without changing rustc.

burdges avatar Nov 26 '25 01:11 burdges

I made another RFC: https://github.com/rust-lang/rfcs/pull/3888.

If that lands, rather than having every trait object being forced to store metadata for casting, there could be a

trait Castable: 'static {
    const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)];
}

which could have a blanket implementation of:

impl<T: 'static> Castable for T {
   const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)] = std::intrinsics::exhausive_implementations::<T>();
}

Which would make the metadata for casting opt in

Though this will require a good chunk of changes to the exhaustive_traits RFC

It would be ideal to make this Castable trait use final for the EXHAUSTIBLE_IMPLEMENTATIONS const self field. final coming from this RFC: https://github.com/rust-lang/rfcs/pull/3678, as making the EXHAUSTIBLE_IMPLEMENTATIONS final would be better than having a blanket implementation. The exhaustible_trait RFC doesnt allow exhaustible traits to have blanket implementations after all, so the final keyword would do wonders. It would be weird that the Castable trait itself was not considered as exhaustible.

Of course we can do compiler magic, and make the Castable trait the only exhaustible_trait that is blanket implemented, but that does not seem ideal to me if we can just do final const self EXHAUSTABLE_IMPLEMENTATIONS: &'static [(TypeId, TraitVTable)] = std::intrinsics::exhausive_implementations::<Self>();

izagawd avatar Nov 26 '25 21:11 izagawd

To me "exhaustive" suggests that the trait can only be implemented within the defining crate (like sealed). I'm not really sure what a better name is though.

tmccombs avatar Nov 28 '25 16:11 tmccombs

@tmccombs I would use a better name than exhaustive if I could, but I can't think of a better name either ¯_(ツ)_/¯

izagawd avatar Nov 28 '25 21:11 izagawd

Appears intertrait already provides a much cleaner solution, by using the linkme.

At present linkme needs linker support, but one could bypass the linker, using only #[no_mangle] and procmacros. Also, linkme cannot control ordering, which demands a hashmap like in this RFCs, so maybe best if all the procmacros compute trait indices too, thereby avoiding the hashmap entirely.

burdges avatar Nov 29 '25 04:11 burdges

@burdges i tried intertrait and it doesnt... work, not even the example code in their crates.io page

I also tried out trait-cast. It does not support generic parameters at all, and requires that i must have the ptr_metadata feature gate available. And again, I have to do the extra step of registering the relationship between a type and interface, which i think should be implicit rather than explicit

cast_trait_object seems to support generic parameters, but it involves some boilerplate setup that registers relationships, which again, I feel should not be explicit.

izagawd avatar Nov 29 '25 21:11 izagawd

Interesting thanks. Appears 4 years since their last version, and the owning blockchain has no updates since May 2023, so bitrot. It nevertheless shows that rule 1 maybe excessively restrictive.

Anyways I definitely agree that generic statics or similar ala https://github.com/rust-lang/rfcs/pull/3888 sound useful.

burdges avatar Nov 29 '25 22:11 burdges

It nevertheless shows that rule 1 maybe excessively restrictive.

I agree, but I cannot find a way around this due to how separate compilation works. I would gladly remove that rule if it were soundly possible

izagawd avatar Nov 30 '25 03:11 izagawd

Is it possible for sidecasting from trait A to trait B to make a subtrait of both, downcast to that subtrait, then upcast to B?

I don't think it's that farfetched but I only have vague surface level of the compiler so I don't really know. I just figured I should make sure it's considered.

Scripter17 avatar Nov 30 '25 06:11 Scripter17

Yes, but only for types defined downstream of both traits, by Rule 1. If you have a &dyn A coming from a &u64 under the hood, then you cannot cast it to a &dyn B. And Box was not directly addressed.

how separate compilation works

There are degrees of separate compilation here: We've large projects using codegen-units=1 for various reasons, by far the worst failure of separate compilation, but really quite standard. By comparison, intertrait only needs some append only structure that's compiled late. Imho intertrait does not go far enough, and should reprocess those append only structures into efficently indexable families.

Around this, I wonder if intertrait failed because it depends upon lto or another linker flag?

Anyways, all these crates for casting dyn Trait track the vtables for the different pointer types seperately. As they check the underlying type, their reason for doing would be unstability of the Pointer<dyn Trait> layout, aka soundness bitrot. I'd suggest two preliminary changes:

First, Rust should commit to all smart pointer types using the same vtables. This is probably already the case. Second, Rust should exposes unsafe methods that manipulate the vtable pointer, optionally safe ones too. This would prevent soundness bitrot in external crates that cast dyn Traits, and allow wider adoption.

burdges avatar Nov 30 '25 14:11 burdges