Skip to content

Cast between trait objects using only safe Rust.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

Lej77/cast_trait_object

Repository files navigation

docs.rs crates.io

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 TypeIds. 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.

About

Cast between trait objects using only safe Rust.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages