In https://github.com/rust-lang/rfcs/issues/349#issuecomment-615934675 I briefly described a mechanism for casts between dyn Trait types that resembles vorner's suggestions in Where's the catch with Box<Read + Write>? as mentioned in https://github.com/rust-lang/rfcs/issues/2035.
We define "casting families" with ZSTs that impl DynCastFamily which structure the compilation unit problem.
pub trait DynCastFamily { const NUM_TRAITS: usize; }
Assume some fixed CF: DynCastFamily which we take as synonymous with the crate defining CF.
We first impl DynCastTo<CF> for dyn Trait { .. } for any dyn Trait types to which trait object casts occur, where
unsafe pub trait DynCastTo<CF: DynCastFamily> : ?Sized {
const dyncast_index: usize;
}
In essence, these provide vormer's trait_id but compacted into lookup indices since any::type_id(dyn Trait)s are way too large. We define these indexes in the compilation unit CF so impl DynCastTo<CF> for dyn Trait becomes hard when Trait lies downstream of CF (see below).
We next impl DynCastFrom<CF> for T for any type T from which we permit dynamic casts within CF, which morally resembles vomer's MultiTraitObject. We never use this when we know T but the object safe trait Trait: DynCastFrom<CF> makes this available from dyn Trait objects.
unsafe pub trait DynCastFrom<CF: DynCastFamily> {
const DYNCAST_VTABLES: &'static [VTablePointer; CF::NUM_TRAITS] where Self: Sized;
const fn dyncast_vtables(&self) -> &'static [VTablePointer; CF::NUM_TRAITS] { DYNCAST_VTABLES }
}
There are no polymorphic methods on trait objects, so we implement the dynamic cast on the trait object itself, like
impl<CF: DynCastFamily> dyn DynCastFrom<CF>> + ?Sync + ?Send + 'static {
fn dyncast<T,P>(mut self: P::Pointer<Self>) -> Option<P::Pointer<T>>
where T: DynCastTo<CF>, P: PointerFamily+PointerVTable,
{
let i = <T as CastTo<CF>>::dyncast_index;
let new_vtable = self.dyncast_vtables()[i];
if new_vtable.is_null() { return None; }
let old_vtable = PointerVTable::vtable_offset(self);
Some(unsafe { *old_vtable.write(new_vtable); mem::transmute(self) })
}
}
where PointerVTable::vtable_offset permit altering vtable pointers manually via the PointerFamily ATCs (see https://github.com/rust-lang/rfcs/issues/349#issuecomment-615934675).
We'd expect high performance from this solution because it requires only two pointer dereferences from potentially well traversed tables. We need one if check too in the full dynamic cast setting, but not for supertrait casts.
We can freely impl DynCastFrom<CF> for T for types T downstream of CF or use Trait: DynCastFrom<CF> for traits downstream of CF, so this suffices for upcasting among trait objects.
We represent the trait object dyn TraitA+TraitB by dyn DynCastFrom<CF>, provided both dyn TraitA: DynCastTo<CF> and dyn TraitB: DynCastTo<CF>. If TraitAB: TraitA+TraitB then we need TraitAB: DynCastFrom<CF> too, probably including the trait alias case pub trait TraitAB = TraitA+TraitB too.
We support multiple CF being declared in one crate if one prefers shorter &'static [VTablePointer], which enables some executable size optimizations, and makes casting more useful in memory constrained environments that avoid monomorphisation.
We can only impl DynCastTo<CF> for dyn Trait if (a) CF reserves space for Trait and (b) all impl DynCastFrom<CF> tell the linker what goes where, so doing this downstream from CF requires build tools that couple CF extremely tightly with the downstream crate. It's likely this creates headaches for extremely large object oriented programs, but the performance should outweigh such costs.
We cannot currently make associated constants object safe with where Self: Sized bounds, so one either removes DYNCAST_VTABLES or else fixes this limitation. I included DynCastFrom::DYNCAST_VTABLES here because doing so helps indicate that rustc should inline the slice directly into the DynCastFrom vtable, avoiding one indirection for the dyn TraitA+TraitB case.
As written, we could implement the underlying casting machinery using only proc macros, lazy_static, and planned extensions like arbitrary self types and PointerFamily ATCs, but rustc could build the &'static [VTablePointer] more cleanly than lazy_static. In other words, anyone interested could work towards this completely outside the rust tree without consuming any lang team resources!
We'd need rustc support for truly implicit DynCastFrom<CF> supertraits of course, and the notation TraitA+TraitB equaling some DynCastFrom<CF>, but really crates could enable this selectively, perhaps via a #[derive(DynCastFrom)] proc macro that deduces CF somehow.
In principle, rust could expose dyn TraitA+TraitB and upcasting, while another unstable crate exposed the casting families machinery for projects that'd benefit, much like std::future exposes stable futures, while the futures crate exposes unstable functionality. We could thus reject pressure to ever bring the casting families machinery into std, like from OO proponents, while still permiting its usage in more complex casting scenarios.
