4 releases
0.1.3 | Jan 16, 2021 |
---|---|
0.1.2 | Nov 4, 2020 |
0.1.1 | Sep 10, 2020 |
0.1.0 | Sep 10, 2020 |
#1101 in Rust patterns
130 downloads per month
Used in 7 crates
(2 directly)
89KB
982 lines
cast_trait_object
This crate offers functionality for casting between trait objects using only
safe Rust and no platform specific code. If you want to downcast to concrete
types instead of other trait objects then this crate can't help you, instead
use std::any
or a crate like downcast-rs
.
This crate offers two things, a trait DynCast
that abstracts over methods
used to cast between trait objects and some macros to minimize the boilerplate
needed to implement that trait.
Usage
You should use the DynCast
trait in trait bounds or as a supertrait and
then do casts using the methods provided by the DynCastExt
trait. The
DynCast
trait takes a type parameter that should be a "config" type
generated by the create_dyn_cast_config
macro, this type defines from
which trait and to which trait a cast is made. Types that need to allow casting
to meet the DynCast
trait bound can then implement it via the
impl_dyn_cast
macro.
Examples
use cast_trait_object::{create_dyn_cast_config, impl_dyn_cast, DynCast, DynCastExt};
create_dyn_cast_config!(SuperToSubCast = Super => Sub);
create_dyn_cast_config!(SuperUpcast = Super => Super);
trait Super: DynCast<SuperToSubCast> + DynCast<SuperUpcast> {}
trait Sub: Super {}
struct Foo;
impl Super for Foo {}
impl Sub for Foo {}
impl_dyn_cast!(Foo as Super => Sub, Super);
let foo: &dyn Super = &Foo;
// Casting to a sub trait is fallible (the error allows us to keep using the
// `dyn Super` trait object if we want which can be important if we are casting
// movable types like `Box<dyn Trait>`):
let foo: &dyn Sub = foo.dyn_cast().ok().unwrap();
// Upcasting to a supertrait is infallible:
let foo /*: &dyn Super*/ = foo.dyn_upcast::<dyn Super>();
When implementing the DynCast
trait via the impl_dyn_cast
macro you
can also list the created "config" types instead of the source and target
traits:
impl_dyn_cast!(Foo => SuperToSubCast, SuperUpcast);
If the proc-macros
feature is enabled (which it is by default) we can also
use procedural attribute macros to write a little bit less boilerplate:
use cast_trait_object::{dyn_cast, dyn_upcast, DynCastExt};
#[dyn_cast(Sub)]
#[dyn_upcast]
trait Super {}
trait Sub: Super {}
struct Foo;
#[dyn_cast(Sub)]
#[dyn_upcast]
impl Super for Foo {}
impl Sub for Foo {}
Note that #[dyn_upcast]
does the same as #[dyn_cast(Super)]
but it is a bit
clearer about intentions:
use cast_trait_object::{dyn_cast, DynCastExt};
#[dyn_cast(Super, Sub)]
trait Super {}
trait Sub: Super {}
struct Foo;
#[dyn_cast(Super, Sub)]
impl Super for Foo {}
impl Sub for Foo {}
let foo: &dyn Sub = &Foo;
// Upcasting still works:
let foo /*: &dyn Super*/ = foo.dyn_upcast::<dyn Super>();
Generics
Generics traits and types are supported and both the declarative macros
(impl_dyn_cast
, create_dyn_cast_config
, impl_dyn_cast_config
)
and the procedural attribute macros (dyn_cast
and dyn_upcast
) can
be used with generics.
use cast_trait_object::{DynCastExt, dyn_cast, dyn_upcast};
// Define a source and target trait:
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
trait Super<T> {}
trait Sub<T>: Super<T> {}
// Since `T` isn't used for `Color` it doesn't need to be `'static`:
struct Color(u8, u8, u8);
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
impl<T> Super<T> for Color {}
impl<T> Sub<T> for Color {}
struct Example<T>(T);
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
impl<T: 'static> Super<T> for Example<T> {}
impl<T: 'static> Sub<T> for Example<T> {}
let as_sub: &dyn Sub<bool> = &Example(false);
let upcasted: &dyn Super<bool> = as_sub.dyn_upcast();
let _downcasted /*: &dyn Sub<bool> */ = upcasted.dyn_cast::<dyn Sub<bool>>().ok().unwrap();
Note that one limitation of the current support for generic types is that if
the type that implements DynCast
has any generic type parameters then
they might need to be constrained to be 'static
.
There is also another limitation with generic types and this one can be a bit
counter intuitive. The DynCast
implementations that are generated by the
macros must always succeed or always fail. This means that if a target trait
is only implemented for a subset of the types that the DynCast
trait is
implemented for then the cast will always fail.
use cast_trait_object::{create_dyn_cast_config, impl_dyn_cast, DynCast, DynCastExt};
// Define a source and target trait:
create_dyn_cast_config!(UpcastConfig = Super => Super);
create_dyn_cast_config!(SuperConfig = Super => Sub);
trait Super: DynCast<SuperConfig> + DynCast<UpcastConfig> {}
trait Sub: Super {}
/// Only implements `Sub` for types that implement `Display`.
struct OnlyDisplayGeneric<T>(T);
impl<T: 'static> Super for OnlyDisplayGeneric<T> {}
impl<T: core::fmt::Display + 'static> Sub for OnlyDisplayGeneric<T> {}
// The cast to `Sub` will always fail since this impl of DynCast includes
// some `T` that don't implement `Display`:
impl_dyn_cast!(for<T> OnlyDisplayGeneric<T> as Super where {T: 'static} => Sub);
impl_dyn_cast!(for<T> OnlyDisplayGeneric<T> as Super where {T: 'static} => Super);
// &str does implement Display:
let _is_display: &dyn core::fmt::Display = &"";
// But the cast will still fail:
let as_super: &dyn Super = &OnlyDisplayGeneric("");
assert!(as_super.dyn_cast::<dyn Sub>().is_err());
// `OnlyDisplayGeneric<&str>` does implement `Sub`:
let as_sub: &dyn Sub = &OnlyDisplayGeneric("");
// Note that this means that we can perform an upcast and then fail to downcast:
let upcasted: &dyn Super = as_sub.dyn_upcast();
assert!(upcasted.dyn_cast::<dyn Sub>().is_err());
The best way to avoid this problem is to have the same trait bounds on both the source trait implementation and the target trait implementation.
How it works
How the conversion is preformed
Using the DynCast
trait as a supertraits adds a couple of extra methods
to a trait object's vtable. These methods all essentially take a pointer to
the type and returns a new fat pointer which points to the wanted vtable.
There are a couple of methods since we need to generate one for each type of
trait object, so one for each of &dyn Trait
, &mut dyn Trait
,
Box<dyn Trait>
, Rc<dyn Trait>
and Arc<dyn Trait>
. Note that these methods
are entirely safe Rust code, this crate doesn't use or generate any unsafe
code at all.
These methods work something like:
trait Super {}
trait Sub {
fn upcast(self: Box<Self>) -> Box<dyn Super>;
}
impl Super for () {}
impl Sub for () {
fn upcast(self: Box<Self>) -> Box<dyn Super> { self }
}
let a: Box<dyn Sub> = Box::new(());
let a: Box<dyn Super> = a.upcast();
The DynCastExt
trait then abstracts over the different types of trait
objects so that when a call is made using the dyn_cast
method the compiler can inline that static method call to the correct method
on the trait object.
Why "config" types are needed
We have to generate "config" types since we need to uniquely identify each
DynCast
supertrait based on which trait it is casting from and into.
Originally this was just done using two type parameters on the trait, something
like DynCast<dyn Super, dyn Sub>
, but that caused compile errors when they were
used as a supertrait of one of the mentioned traits. So now the traits are
"hidden" as associated types on a generated "config" type. To make this "config"
type more ergonomic we also implement a GetDynCastConfig
trait to easily
go from the source trait and target trait to a "config" type via something
like <dyn Source as GetDynCastConfig<dyn Target>>::Config
. This allows
the macros (impl_dyn_cast
, dyn_cast
and dyn_upcast
) to take traits
as arguments instead of "config" types, it also makes type inference work for
the DynCastExt
trait.
How the macros know if a type implements a "target" trait or not
When a type implementing DynCast
for a specific config and therefore
source to target trait cast the generated code must choose if the cast is
going to succeed or not. We want to return Ok(value as &dyn Target)
if
the type implements the Target
trait and Err(value as &dyn Source)
if
it doesn't.
We can use a clever hack to only preform the coercion if a type actually implements the target trait. See dtolnay's [Autoref-based stable specialization] (https://github.com/dtolnay/case-studies/tree/master/autoref-specialization) case study for more information about how this hack works. In short the hack allows us to call one method if a trait bound is met and another method if it isn't. In this way we can call a helper method that performs the coercion to the target trait only if the type actually implements that trait.
So we could generate something like:
trait Source {}
trait Target {}
struct Foo;
impl Source for Foo {}
struct Fallback;
impl Fallback {
fn cast<'a, T: Source>(&self, value: &'a T) -> &'a dyn Source { value }
}
struct HasTrait<T>(core::marker::PhantomData<T>);
impl<T> HasTrait<T> {
fn new() -> Self {
Self(core::marker::PhantomData)
}
}
impl<T: Target> HasTrait<T> {
fn cast<'a>(&self, value: &'a T) -> &'a dyn Target { value }
}
impl<T> core::ops::Deref for HasTrait<T> {
type Target = Fallback;
fn deref(&self) -> &Self::Target {
static FALLBACK: Fallback = Fallback;
&FALLBACK
}
}
let used_fallback: &dyn Source = HasTrait::<Foo>::new().cast(&Foo);
So the impl_dyn_cast
macro works by generating a struct that implements
core::ops::Deref
into another type. Both types have a cast
method but
they do different things. The first struct's cast
method has a trait bound
so that it is only implemented if the cast can succeed. If the first method
can't be used the compiler will insert a deref operation (&*foo
) and see
if there is a method that can apply after that. In this case that means that
the Fallback
structs method is called. This way the generated code doesn't
call the method that preform the coercion to the Target
trait unless the
type actually implements it.
Alternatives
The intertrait
crate offers similar functionality to this crate but has
a totally different implementation, at least as of intertrait
version
0.2.0
. It uses the linkme
crate to create a registry of std::any::Any
type ids for types that can be cast into a certain trait object. This means
it probably has some runtime overhead when it looks up a cast function in
the global registry using a TypeId
. It also means that it can't work on
all platforms since the linkme
crate needs to offer support for them. This
is a limitation that this crate doesn't have.
The traitcast
crate works similar to intertrait
in that it has a
global registry that is keyed with TypeId
s. But it differs in that it
uses the inventory
crate to build the registry instead of the linkme
crate. The inventory
crate uses the ctor
crate to run some code before
main
, something that is generally discouraged and this is something that
intertrait
actually mentions as an advantage to its approach.
The traitcast_core
library allow for a more low level API that doesn't
depend on a global registry and therefore also doesn't depend on a crate like
linkme
or inventory
that needs platform specific support. Instead it
requires that you explicitly create a registry and register all your types
and their casts with it.
The downcast-rs
crate offers downcasting to concrete types but not
directly casting from one trait object to another trait object. So it has a
different use case and both it and this crate could be useful in the same
project.
You could just define methods on your traits similar to the ones provided by
this crate's DynCast
trait. Doing this yourself can be more flexible and
you could for example minimize bloat by only implementing methods for casts
that you actually require. The disadvantage is that it would be much less
ergonomic than what this crate offers.
References
The following GutHub issue Clean up pseudo-downcasting from VpnProvider supertrait to subtraits with better solution · Issue #21 · jamesmcm/vopono inspired this library.
This library was mentioned in the following blog post in the "Upcasting" section: So you want to write object oriented Rust :: Darrien's Blog — Dev work and musings
License
This project is released under either:
at your choosing.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~0–305KB