I used a ton of #![feature(provide_any)]
in my own project, and while reading the recent RFC PR on supertrait upcast (this), I put two and two together and thought, "wait, can't we use provide_any to do trait to trait casts?"
Obviously provide_any
in its current form is not going to work, so I whipped something together really quick:
use std::any::TypeId;
#[repr(transparent)]
pub struct Demand(dyn Erased);
pub trait LateralCast: 'static {
fn cast(self: Box<Self>, demand: &mut Demand);
}
pub struct Receiver<'a, I: LateralCast + ?Sized>(&'a mut TaggedOption<I>);
impl<'a, I: LateralCast + ?Sized> Receiver<'a, I> {
pub fn provide(&mut self, value: Box<I>) {
self.0 .0 = Some(value)
}
}
/// Represents a type-erased but identifiable object.
///
/// This trait is exclusively implemented by the `TaggedOption` type.
unsafe trait Erased {
/// The `TypeId` of the erased type.
fn tag_id(&self) -> TypeId;
}
unsafe impl<I: LateralCast + ?Sized> Erased for TaggedOption<I> {
fn tag_id(&self) -> TypeId {
std::any::TypeId::of::<I>()
}
}
impl Demand {
fn new(erased: &mut (dyn Erased)) -> &mut Demand {
// SAFETY: transmuting `&mut (dyn Erased)` to `&mut Demand` is safe
// since `Demand` is repr(transparent).
unsafe { &mut *(erased as *mut (dyn Erased) as *mut Demand) }
}
pub fn maybe_provide<T: LateralCast + ?Sized>(&mut self) -> Option<Receiver<T>> {
if let Some(res @ TaggedOption(None)) = self.0.downcast_mut::<T>() {
Some(Receiver(res))
} else {
None
}
}
}
impl dyn Erased {
/// Returns some reference to the dynamic value if it is tagged with `I`,
/// or `None` otherwise.
#[inline]
fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<I>>
where
I: LateralCast + ?Sized,
{
if self.tag_id() == TypeId::of::<I>() {
// SAFETY: Just checked whether we're pointing to an I.
Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<I>>() })
} else {
None
}
}
}
#[repr(transparent)]
struct TaggedOption<I: LateralCast + ?Sized>(Option<Box<I>>);
impl<I: LateralCast + ?Sized> TaggedOption<I> {
fn as_demand(&mut self) -> &mut Demand {
Demand::new(self as &mut (dyn Erased))
}
}
pub fn lateral_cast<A, B>(from: Box<A>) -> Option<Box<B>>
where
A: LateralCast + ?Sized,
B: LateralCast + ?Sized,
{
let mut tagged = TaggedOption::<B>(None);
from.cast(tagged.as_demand());
tagged.0
}
#[cfg(test)]
mod test {
use super::LateralCast;
trait A: LateralCast {}
trait B: LateralCast {}
struct Struct;
impl A for Struct {}
impl B for Struct {}
impl LateralCast for Struct {
fn cast(self: Box<Self>, demand: &mut crate::Demand) {
if let Some(mut receiver) = demand.maybe_provide::<dyn A>() {
receiver.provide(self);
} else if let Some(mut receiver) = demand.maybe_provide::<dyn B>() {
receiver.provide(self);
}
}
}
#[test]
fn test() {
let a = Box::new(Struct) as Box<dyn A>;
let b = crate::lateral_cast::<_, dyn B>(a);
assert!(b.is_some());
}
}
This allows you to cast between any 2 traits that has LateralCast
as their supertrait. The user has to impl LateralCast
to provide the traits they want to be able to cast to, which is the only con here, I think.
Does this make sense? Could this potentially be unsound?