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.