MultiAny: Downcasting to Multiple Trait Objects in Rust
The Question
In the Reddit comments to my post, the user mio991 asked:
Good, you got it working. But how do I store a value in a way to get any trait object implemented?
As I see it, the task is to store a value in a type-erased way and be able to restore not just one trait as a reference to a trait object, but any implemented trait.
Naive Approach
I doubt it is possible in exactly this way, but if we, for example, just list all implemented traits for an object, then it is doable:
// pseudocode
// SomeStruct should implement Trait1, Trait2, and Trait3 for this to work.#[derive(MultiAny)]#[multi_any(Trait1, Trait2, Trait3)]struct SomeStruct {}
// and laterlet dyn_value = MultiAny::new(SomeStruct{});dyn_value.downcast_ref::<dyn Trait1>();dyn_value.downcast_ref::<dyn Trait2>();
This is entirely possible to implement using ideas from the previous post. The only difference is that you not only store one piece of metadata, but a list of (TypeId, metadata)
pairs. I’ve actually implemented this: Rust Playground. Do not expect anything production-ready; it is just a proof of concept, and a dismissed one.
This would not be worth a new blog post though, as there were no new ideas in it compared to the previous post.
Learning from the Any Trait
Looking at the code, I was thinking: should it be that complicated? Do we need to manually manage memory allocation, implement Drop
, and hope it all works as intended? There is an example in the standard library that is worth studying: the Any
trait.
It turns out Any
is just a normal trait, like any other. It has only one function, type_id
, and also has a blanket implementation for all supported types:
pub trait Any: 'static { fn type_id(&self) -> TypeId;}
impl<T: 'static + ?Sized> Any for T { fn type_id(&self) -> TypeId { TypeId::of::<T>() }}
And actually, that is all that is needed to implement downcasting:
// this is not the actual code from the standard library, but it works the sameimpl dyn Any { pub fn downcast_ref<T: Any>(&self) -> Option<&T> { if TypeId::of::<T>() == self.type_id() { unsafe { &*(self as *const dyn Any as *const T) } } else { None } }}
This works because a pointer to a dyn Trait
, including dyn Any
, is actually a fat pointer consisting of the data pointer and metadata. All the code in downcast_ref
does is discard the metadata if the type IDs match.
MultiAny Concept
Can we utilize a similar approach for downcasting to multiple trait objects? To downcast to a trait object, we need to not just discard the metadata, but replace it with the correct metadata. So our trait can look like this:
pub trait MultiAny { // Returns metadata if the trait is implemented // `type_id` is for the type of the trait object `dyn Trait` fn get_metadata(&self, type_id: TypeId) -> Option<usize>;}
Here is how we can use it:
impl dyn MultiAny { pub fn downcast_ref<T>(&self) -> Option<&T> where T: Pointee<Metadata = DynMetadata<T>> + ?Sized + 'static, { // Get metadata for the requested type ID and convert it to DynMetadata let meta_raw = self.get_metadata(TypeId::of::<T>())?; let meta: DynMetadata<T> = unsafe { transmute(meta_raw) };
// Construct a fat pointer by combining the raw data pointer with metadata let ptr: *const T = from_raw_parts(self as *const dyn MultiAny as *const (), meta);
unsafe { &*ptr }.into() }}
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<usize> { // Return metadata for the requested trait object if it is implemented, or None if type_id == TypeId::of::<dyn Trait1>() { let trait_ptr = self as &dyn Trait1 as *const dyn Trait1; let meta = metadata(trait_ptr); let meta_raw: usize = unsafe { transmute(meta) }; Some(meta_raw) } // Check other implemented traits here else { None } }}
let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() });assert_eq!(foo.downcast_ref::<dyn Trait1>().unwrap().hello(), "Hello, Bob");
Full source code for reference
#![feature(ptr_metadata)]
use std::{ any::TypeId, mem::transmute, ptr::{DynMetadata, Pointee, from_raw_parts},};
pub trait MultiAny { fn get_metadata(&self, type_id: TypeId) -> Option<usize>;}
impl dyn MultiAny { // T is a dyn Trait here pub fn downcast_ref<T>(&self) -> Option<&T> where T: Pointee<Metadata = DynMetadata<T>> + ?Sized + 'static, { // get metadata for type_id and convert it to DynMetadata let meta_raw = self.get_metadata(TypeId::of::<T>())?; let meta: DynMetadata<T> = unsafe { transmute(meta_raw) };
// make fat pointer by combining raw data pointer with metadata let ptr: *const T = from_raw_parts(self as *const dyn MultiAny as *const (), meta);
unsafe { &*ptr }.into() }}
#[cfg(test)]mod tests { use super::*; use std::ptr::metadata;
trait Trait1 { fn hello(&self) -> String; } trait Trait2 { fn bye(&self) -> String; } trait Trait3 {}
struct Foo { name: String, }
impl Trait1 for Foo { fn hello(&self) -> String { format!("Hello, {}", self.name) } } impl Trait2 for Foo { fn bye(&self) -> String { format!("Bye, {}", self.name) } }
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<usize> { if type_id == TypeId::of::<dyn Trait1>() { let trait_ptr = self as &dyn Trait1 as *const dyn Trait1; let meta = metadata(trait_ptr); let meta_raw: usize = unsafe { transmute(meta) }; Some(meta_raw) } else if type_id == TypeId::of::<dyn Trait2>() { let trait_ptr = self as &dyn Trait2 as *const dyn Trait2; let meta = metadata(trait_ptr); let meta_raw: usize = unsafe { transmute(meta) }; Some(meta_raw) } else { None } } }
#[test] fn test() { let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() }); assert_eq!( foo.downcast_ref::<dyn Trait1>().unwrap().hello(), "Hello, Bob" ); assert_eq!(foo.downcast_ref::<dyn Trait2>().unwrap().bye(), "Bye, Bob"); assert!(foo.downcast_ref::<dyn Trait3>().is_none()); }}
Safety issues
As a proof of concept this code works. From an ergonomics perspective, it only needs a derive macro. But from a safety perspective, it is unsound: it is very easy to implement MultiAny
incorrectly:
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<usize> { // when asked for metadata for the Trait1 return metadata for Trait2 if type_id == TypeId::of::<dyn Trait1>() { let trait_ptr = self as &dyn Trait2 as *const dyn Trait2; let meta = metadata(trait_ptr); let meta_raw: usize = unsafe { transmute(meta) }; Some(meta_raw) } else { None } }}
The code above will return metadata for Trait2
when asked for metadata for Trait1
. There are no further checks, so the code in downcast_ref
will just interpret DynMetadata<dyn Trait2>
as DynMetadata<dyn Trait1>
and return the reference. This will lead to undefined behavior.
Type-Safe Metadata
We need to make sure that returned metadata actually matches the requested type_id
(which refers to dyn Trait
). We can do this by making get_metadata
return a type, that guarantees that type_id matches the metadata:
// type representing type-erased metadatapub struct Meta { meta_raw: usize, type_id: TypeId,}
impl Meta { // The only way to construct `Meta` is through this function. // This guarantees that the stored metadata always matches the stored type ID.
pub fn try_from_trait<DynTrait>(t: &DynTrait, type_id: TypeId) -> Option<Meta> { if type_id != TypeId::of::<DynTrait>() { return None; } let meta = metadata(t); let meta_raw: usize = unsafe { transmute(meta) }; Some(Meta { meta_raw, type_id }) }
// This function checks that the stored type ID matches the type we are converting to. // It is impossible to convert `Meta` into the wrong `DynMetadata`. pub fn into_metadata<DynTrait>(self) -> DynTrait::Metadata { assert_eq!(self.type_id, TypeId::of::<DynTrait>(), "Wrong dyn Trait type"); let meta: DynMetadata<DynTrait> = unsafe { transmute(self.meta_raw) }; meta }}
// `get_metadata` now returns `Meta`pub trait MultiAny { fn get_metadata(&self, type_id: TypeId) -> Option<Meta>;}
// `downcast_ref` uses `Meta` to safely convert metadataimpl dyn MultiAny { pub fn downcast_ref<T>(&self) -> Option<&T> { let meta = self.get_metadata(TypeId::of::<T>())?.into_metadata::<T>(); let ptr: *const T = from_raw_parts(self as *const dyn MultiAny as *const (), meta); unsafe { &*ptr }.into() }}
Full source code for reference
#![feature(ptr_metadata)]
use std::{ any::TypeId, mem::transmute, ptr::{DynMetadata, Pointee, from_raw_parts, metadata},};
pub struct Meta { meta_raw: usize, type_id: TypeId,}
impl Meta { pub fn try_from_trait<DynTrait>(t: &DynTrait, type_id: TypeId) -> Option<Meta> where DynTrait: Pointee<Metadata = DynMetadata<DynTrait>> + ?Sized + 'static, { if type_id != TypeId::of::<DynTrait>() { return None; }
let meta = metadata(t);
// SAFETY: We transmute DynMetadata<T> to usize. // This safe because we will only use resulting usize to transmute back to exact same type. // Note: transmute guarantees that types has the same size. let meta_raw: usize = unsafe { transmute(meta) };
Some(Meta { meta_raw, type_id }) }
pub fn into_metadata<DynTrait>(self) -> DynTrait::Metadata where DynTrait: Pointee<Metadata = DynMetadata<DynTrait>> + ?Sized + 'static, { assert_eq!( self.type_id, TypeId::of::<DynTrait>(), "Wrong dyn Trait type" );
// SAFETY: We transmute usize to DynMetadata<T>. // This safe because we are sure that usize was aquired by transmuting exact same type. // Note: transmute guarantees that types has the same size. let meta: DynMetadata<DynTrait> = unsafe { transmute(self.meta_raw) };
meta }}
pub trait MultiAny { fn get_metadata(&self, type_id: TypeId) -> Option<Meta>;}
impl dyn MultiAny { pub fn downcast_ref<T>(&self) -> Option<&T> where T: Pointee<Metadata = DynMetadata<T>> + ?Sized + 'static, { let meta = self.get_metadata(TypeId::of::<T>())?.into_metadata::<T>();
let ptr: *const T = from_raw_parts(self as *const dyn MultiAny as *const (), meta);
// SAFETY: We are converting raw pointer to reference. // This is safe because pointer points to the same data as self. unsafe { &*ptr }.into() }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_basic() { trait Hello { fn hello(&self) -> String; }
trait Bye { fn bye(&self) -> String; }
trait Other {}
struct Foo { name: String, }
impl Hello for Foo { fn hello(&self) -> String { format!("Hello, {}", self.name) } }
impl Bye for Foo { fn bye(&self) -> String { format!("Bye, {}", self.name) } }
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { Meta::try_from_trait(self as &dyn Hello, type_id) .or_else(|| Meta::try_from_trait(self as &dyn Bye, type_id)) } }
let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() }); assert_eq!( foo.downcast_ref::<dyn Hello>().unwrap().hello(), "Hello, Bob" ); assert_eq!(foo.downcast_ref::<dyn Bye>().unwrap().bye(), "Bye, Bob"); assert!(foo.downcast_ref::<dyn Other>().is_none()); }
#[test] fn test_wrong_dyn_trait_type() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, _type_id: TypeId) -> Option<Meta> { // try to use type_id not matching the supplied reference Meta::try_from_trait(self as &dyn Trait, TypeId::of::<dyn WrongTrait>()) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); assert!(bar.downcast_ref::<dyn Trait>().is_none()); assert!(bar.downcast_ref::<dyn WrongTrait>().is_none()); }
#[test] #[should_panic = "Wrong dyn Trait type"] fn test_wrong_dyn_trait_type_panic() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, _type_id: TypeId) -> Option<Meta> { // return metadata for WrongTrait regardless of what was requested Meta::try_from_trait(self as &dyn WrongTrait, TypeId::of::<dyn WrongTrait>()) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); bar.downcast_ref::<dyn Trait>(); }}
The only way to construct Meta is through try_from_trait
, which guarantees that the stored metadata corresponds to the provided type. When converting Meta
back into DynMetadata<T>
, the type ID is validated, ensuring that we cannot mistakenly convert DynMetadata<T>
into DynMetadata<K>
for a different type K
.
Data Type Validation
There is still a way to implement MultiAny
incorrectly so that it will lead to undefined behavior. We can return metadata that matches the trait object, but does not match the data type:
trait Trait {}struct Bar;struct Baz;impl Trait for Bar {}impl Trait for Baz {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { // returning metadata for right dyn Trait but wrong data type Meta::try_from_trait(&Baz as &dyn Trait, type_id) }}
Here get_metadata
returns the right type DynMetadata<dyn Trait>
, but the vtable is for Trait
implemented for Baz
, not Bar
.
To fix this, we can store type_id
for the data type with meta. Then downcast_ref
can assert that data_id
matches self.type_id()
, preventing undefined behavior:
// `Meta` stores two type IDs:// - `data_id`: the concrete type of the underlying value// - `dyn_trait_id`: the trait object type (`dyn Trait`) this metadata belongs topub struct Meta { meta_raw: usize, data_id: TypeId, dyn_trait_id: TypeId,}
impl Meta { // `try_from_trait` takes a reference to the data and a cast function that produces the trait object. // This avoids invalid combinations of type IDs by tying the metadata to an actual cast. pub fn try_from_trait<Data, DynTrait>( data: &Data, requested_trait_id: TypeId, cast_fn: fn(&Data) -> &DynTrait, ) -> Option<Meta> { ... }
// `into_metadata` validates only the trait object type (`dyn_trait_id`). // It cannot verify the underlying data pointer, but the stored `data_id` is consistent with the original data. pub fn into_metadata<DynTrait>(self) -> DynTrait::Metadata { ... }}
// `MultiAny` extends `Any` to provide access to the concrete type ID of the stored value.pub trait MultiAny: Any { fn get_metadata(&self, type_id: TypeId) -> Option<Meta>;}
impl dyn MultiAny { // `downcast_ref` checks that the underlying data type matches before reconstructing the pointer. pub fn downcast_ref<DynTrait>(&self) -> Option<&DynTrait> { let meta = self.get_metadata(TypeId::of::<DynTrait>())?;
// Verify the stored `data_id` via `Any::type_id()`. assert_eq!(meta.data_id, self.type_id(), "Wrong Data type");
... }}
Full source code for reference
#![feature(ptr_metadata)]
use std::{ any::{Any, TypeId}, mem::transmute, ptr::{DynMetadata, Pointee, from_raw_parts, metadata},};
pub struct Meta { meta_raw: usize, data_id: TypeId, dyn_trait_id: TypeId,}
impl Meta { /// If `requested_trait_id` matches `DynTrait` extract pointer metadata for `DynTrait` pub fn try_from_trait<Data, DynTrait>( data: &Data, requested_trait_id: TypeId, cast_fn: fn(&Data) -> &DynTrait, ) -> Option<Meta> where Data: 'static, DynTrait: Pointee<Metadata = DynMetadata<DynTrait>> + ?Sized + 'static, { if requested_trait_id != TypeId::of::<DynTrait>() { return None; }
let dyn_trait = cast_fn(data); let meta = metadata(dyn_trait);
// SAFETY: We transmute DynMetadata<T> to usize. // This safe because we will only use resulting usize to transmute back to exact same type. // Note: transmute guarantees that types has the same size. let meta_raw: usize = unsafe { transmute(meta) };
Some(Meta { meta_raw, data_id: TypeId::of::<Data>(), dyn_trait_id: requested_trait_id, }) }
/// Convert into concrete pointer metadata pub fn into_metadata<DynTrait>(self) -> DynTrait::Metadata where DynTrait: Pointee<Metadata = DynMetadata<DynTrait>> + ?Sized + 'static, { assert_eq!( self.dyn_trait_id, TypeId::of::<DynTrait>(), "Wrong dyn Trait type" );
// SAFETY: We transmute usize to DynMetadata<T>. // This safe because we are sure that usize was aquired by transmuting exact same type. // Note: transmute guarantees that types has the same size. let meta: DynMetadata<DynTrait> = unsafe { transmute(self.meta_raw) };
meta }}
pub trait MultiAny: Any { fn get_metadata(&self, type_id: TypeId) -> Option<Meta>;}
impl dyn MultiAny { pub fn downcast_ref<DynTrait>(&self) -> Option<&DynTrait> where DynTrait: Pointee<Metadata = DynMetadata<DynTrait>> + ?Sized + 'static, { let meta = self.get_metadata(TypeId::of::<DynTrait>())?; assert_eq!(meta.data_id, self.type_id(), "Wrong Data type");
// SAFETY: We are merging raw data pointer with metadata. // This is safe because we know that data type matches the type this metadata was created for. let ptr: *const DynTrait = from_raw_parts( self as *const dyn MultiAny as *const (), meta.into_metadata::<DynTrait>(), );
// SAFETY: We are converting raw pointer to reference. // This is safe because pointer points to the same data as self. unsafe { &*ptr }.into() }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_basic() { trait Hello { fn hello(&self) -> String; }
trait Bye { fn bye(&self) -> String; }
trait Other {}
struct Foo { name: String, }
impl Hello for Foo { fn hello(&self) -> String { format!("Hello, {}", self.name) } }
impl Bye for Foo { fn bye(&self) -> String { format!("Bye, {}", self.name) } }
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { Meta::try_from_trait(self, type_id, |v| v as &dyn Hello) .or_else(|| Meta::try_from_trait(self, type_id, |v| v as &dyn Bye)) } }
let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() }); assert_eq!( foo.downcast_ref::<dyn Hello>().unwrap().hello(), "Hello, Bob" ); assert_eq!(foo.downcast_ref::<dyn Bye>().unwrap().bye(), "Bye, Bob"); assert!(foo.downcast_ref::<dyn Other>().is_none()); }
#[test] fn test_wrong_dyn_trait_type() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { _ = type_id; // try to use type_id not matching the supplied reference Meta::try_from_trait(self, TypeId::of::<dyn WrongTrait>(), |v| v as &dyn Trait) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); assert!(bar.downcast_ref::<dyn Trait>().is_none()); assert!(bar.downcast_ref::<dyn WrongTrait>().is_none()); }
#[test] #[should_panic = "Wrong dyn Trait type"] fn test_wrong_dyn_trait_type_panic() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { _ = type_id; // return metadata for WrongTrait regardless of what was requested Meta::try_from_trait(self, TypeId::of::<dyn WrongTrait>(), |v| { v as &dyn WrongTrait }) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); bar.downcast_ref::<dyn Trait>(); }
#[test] #[should_panic = "Wrong Data type"] fn test_wrong_data_type_panic() { trait Trait {} struct Bar; struct Baz; impl Trait for Bar {} impl Trait for Baz {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { // returning metadata for right dyn Trait but wrong data type Meta::try_from_trait(&Baz, type_id, |v| v as &dyn Trait) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); bar.downcast_ref::<dyn Trait>(); }}
With these checks in place, an incorrect implementation of MultiAny::get_metadata
will cause a panic rather than triggering undefined behavior.
Downcasting to Concrete Types
It would be nice to be able to downcast to the original type. At first glance, it looks like it would be quite easy to implement with just adding one extra call to Meta::try_from_trait
in the get_metadata
:
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { Meta::try_from_trait(self, type_id, |v| v) .or_else(|| Meta::try_from_trait(self, type_id, |v| v as &dyn Hello)) .or_else(|| Meta::try_from_trait(self, type_id, |v| v as &dyn Bye)) }}
But this will not compile directly, because concrete types and trait objects have different metadata types. To solve this, we introduce a TypedMetadata
trait and implement it for both ()
(concrete types) and DynMetadata<T>
(trait objects), allowing uniform conversion between type-erased Meta
and typed metadata.
With this, we can finally downcast a Box<dyn MultiAny>
to both trait objects and concrete types safely:
// A trait for converting between `Meta` (type-erased) and typed metadata.pub trait TypedMetadata { fn from_meta(meta: Meta) -> Self; fn into_meta<Data: 'static>(self) -> Meta;}
impl TypedMetadata for () { ...}
impl<T> TypedMetadata for DynMetadata<T> { ...}
// `Meta` now relies on `TypedMetadata` for conversions between typed and type-erased forms.pub struct Meta { ... }
impl Meta { pub fn try_from<Data, RequestedType>( data: &Data, requested_type_id: TypeId, cast_fn: fn(&Data) -> &RequestedType, ) -> Option<Meta> where RequestedType: Pointee + ?Sized + 'static, RequestedType::Metadata: TypedMetadata, // Note this constraint { ... RequestedType::Metadata::into_meta::<Data>(typed_meta).into() }
pub fn into_metadata<Data, RequestedType>(self) -> RequestedType::Metadata where Data: 'static, RequestedType: Pointee + ?Sized + 'static, RequestedType::Metadata: TypedMetadata, { assert_eq!(self.data_id, TypeId::of::<Data>(), "Wrong Data type"); RequestedType::Metadata::from_meta(self) }}
// Example: successfully downcasting back to `Foo`.let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() });assert_eq!(foo.downcast_ref::<Foo>().unwrap().name, "Bob");
Full source code for reference
#![feature(ptr_metadata)]
use std::{ any::{Any, TypeId}, mem::transmute, ptr::{DynMetadata, Pointee, from_raw_parts, metadata},};
pub trait TypedMetadata { fn from_meta(meta: Meta) -> Self; fn into_meta<Data: 'static>(self) -> Meta;}
impl TypedMetadata for () { fn from_meta(meta: Meta) -> Self { assert_eq!(meta.meta_id, TypeId::of::<()>(), "Wrong Metadata type"); }
fn into_meta<Data: 'static>(self) -> Meta { Meta { meta_raw: 0, data_id: TypeId::of::<Data>(), meta_id: TypeId::of::<Self>(), } }}
impl<T> TypedMetadata for DynMetadata<T>where T: Pointee<Metadata = Self> + ?Sized + 'static,{ fn from_meta(meta: Meta) -> Self { assert_eq!(meta.meta_id, TypeId::of::<Self>(), "Wrong Metadata type");
// SAFETY: We transmute usize to DynMetadata<T>. // This safe because we are sure that usize was aquired by transmuting exact same type. // Note: transmute guarantees that types has the same size. let typed_meta: Self = unsafe { transmute(meta.meta_raw) };
typed_meta }
fn into_meta<Data: 'static>(self) -> Meta { // SAFETY: We transmute DynMetadata<T> to usize. // This safe because we will only use resulting usize to transmute back to exact same type. // Note: transmute guarantees that types has the same size. let meta_raw: usize = unsafe { transmute(self) };
Meta { meta_raw, data_id: TypeId::of::<Data>(), meta_id: TypeId::of::<Self>(), } }}
pub struct Meta { meta_raw: usize, data_id: TypeId, meta_id: TypeId,}
impl Meta { /// If `requested_type_id` matches `RequestedType` extract pointer metadata for RequestedType` pub fn try_from<Data, RequestedType>( data: &Data, requested_type_id: TypeId, cast_fn: fn(&Data) -> &RequestedType, ) -> Option<Meta> where Data: 'static, RequestedType: Pointee + ?Sized + 'static, RequestedType::Metadata: TypedMetadata, { if requested_type_id != TypeId::of::<RequestedType>() { return None; }
let dyn_trait = cast_fn(data); let typed_meta = metadata(dyn_trait);
RequestedType::Metadata::into_meta::<Data>(typed_meta).into() }
/// Convert into concrete pointer metadata pub fn into_metadata<Data, RequestedType>(self) -> RequestedType::Metadata where Data: 'static, RequestedType: Pointee + ?Sized + 'static, RequestedType::Metadata: TypedMetadata, { assert_eq!(self.data_id, TypeId::of::<Data>(), "Wrong Data type"); RequestedType::Metadata::from_meta(self) }}
pub trait MultiAny: Any { fn get_metadata(&self, type_id: TypeId) -> Option<Meta>;}
impl dyn MultiAny { pub fn downcast_ref<RequestedType>(&self) -> Option<&RequestedType> where RequestedType: Pointee + ?Sized + 'static, RequestedType::Metadata: TypedMetadata, { let meta = self.get_metadata(TypeId::of::<RequestedType>())?; assert_eq!(meta.data_id, self.type_id(), "Wrong Data type");
let typed_meta = RequestedType::Metadata::from_meta(meta); let data_pointer = self as *const dyn MultiAny as *const ();
// SAFETY: We are merging raw data pointer with metadata. // This is safe because we know that data type matches the type this metadata was created for. let ptr: *const RequestedType = from_raw_parts(data_pointer, typed_meta);
// SAFETY: We are converting raw pointer to reference. // This is safe because pointer points to the same data as self. unsafe { &*ptr }.into() }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_basic() { trait Hello { fn hello(&self) -> String; }
trait Bye { fn bye(&self) -> String; }
trait Other {}
struct Foo { name: String, }
impl Hello for Foo { fn hello(&self) -> String { format!("Hello, {}", self.name) } }
impl Bye for Foo { fn bye(&self) -> String { format!("Bye, {}", self.name) } }
impl MultiAny for Foo { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { Meta::try_from(self, type_id, |v| v) .or_else(|| Meta::try_from(self, type_id, |v| v as &dyn Hello)) .or_else(|| Meta::try_from(self, type_id, |v| v as &dyn Bye)) } }
let foo: Box<dyn MultiAny> = Box::new(Foo { name: "Bob".into() });
// downcast to concrete type assert_eq!(foo.downcast_ref::<Foo>().unwrap().name, "Bob");
// downcast to dyn Traits that are implemented assert_eq!( foo.downcast_ref::<dyn Hello>().unwrap().hello(), "Hello, Bob" ); assert_eq!(foo.downcast_ref::<dyn Bye>().unwrap().bye(), "Bye, Bob");
// downcast to dyn Trait that is not implemented assert!(foo.downcast_ref::<dyn Other>().is_none());
// downcast to wrong type assert!(foo.downcast_ref::<()>().is_none()); }
#[test] fn test_wrong_dyn_trait_type() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { _ = type_id; // try to use type_id not matching the supplied reference Meta::try_from(self, TypeId::of::<dyn WrongTrait>(), |v| v as &dyn Trait) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); assert!(bar.downcast_ref::<dyn Trait>().is_none()); assert!(bar.downcast_ref::<dyn WrongTrait>().is_none()); }
#[test] #[should_panic = "Wrong Metadata type"] fn test_wrong_dyn_trait_type_panic() { trait Trait {} trait WrongTrait {} struct Bar; impl Trait for Bar {} impl WrongTrait for Bar {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { _ = type_id; // return metadata for WrongTrait regardless of what was requested Meta::try_from(self, TypeId::of::<dyn WrongTrait>(), |v| { v as &dyn WrongTrait }) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); bar.downcast_ref::<dyn Trait>(); }
#[test] #[should_panic = "Wrong Data type"] fn test_wrong_data_type_panic() { trait Trait {} struct Bar; struct Baz; impl Trait for Bar {} impl Trait for Baz {}
impl MultiAny for Bar { fn get_metadata(&self, type_id: TypeId) -> Option<Meta> { // returning metadata for right dyn Trait but wrong data type Meta::try_from(&Baz, type_id, |v| v as &dyn Trait) } }
let bar: Box<dyn MultiAny> = Box::new(Bar); bar.downcast_ref::<dyn Trait>(); }}
With this change, a Box<dyn MultiAny>
can downcast either to any implemented trait object or directly to the concrete type of the stored value.
What’s Next?
To make this approach fully practical, a few additional features would be helpful:
- A derive macro for MultiAny
- A downcast_mut function
- A downcast function for Box
I haven’t covered these here, but they are implemented in the MultiAny crate. Thanks to the ptr_meta library, the crate can work on stable Rust, not just nightly.
Thanks for joining me on this little coding adventure.
Discussion
Join the discussion on Reddit.