Cast from Concrete to Any and subtraits

AnyTrait

I needed to write some almost-OOP code in rust and I needed something like ::core::any::Any, but that let me downcast to traits, without knowing the concrete type.

After reading around how it is not possible to just directly downcast for a lot of reasons, I made a small crate to do something like that (with extra steps).

docs on: any_trait - Rust
code on: GitHub - LucaFulchir/any-trait: any-trait: rust crate to downcast to any sub-trait

What it gives you

I have not actually even started using it, but I though I might get some comments here on what parts are not going to work for reasons I don't know, or some comments on how this will enable horrifying half-OOP code in your codebase from now on.

Currently it lets you:

  • Upcast from the concrete type to AnyTrait (like core's Any)
  • Downcast from the AnyTrait to the concrete type (like core's Any)
  • Downcast from AnyTrait to any other &dyn SubTrait implemented by your concrete type (missing from core's Any)
  • Upcast from subtraits that require AnyTrait to &dyn AnyTrait

I have only implemented the downcast_ref::<T> and downcast_mut::<T> for the basic case, I will have to duplicate things a bit to add Sync and Send support.

check the test directory for a working example

How it works

First we need a ::core::any::TypeId that is const-comparable, sortable.
we use ::core::any::type_name and do the rest manually since I could not find a way to use TypeId for that.

Then for every time you add #[derive(AnySubTrait)] for your structs, we add an impl AnyTrait that has a const list of the types it can be up/down cast to.

This AnyTrait has a method that will cast to the concrete to the correct subtrait....and then union to usize. horrifying, I know.
But that lets us have a common impl dyn AnyTrait implementation that can actually take that and use generics to cast back to the &dyn Trait that you requested.

Everything not marked as unsafe should be safe, and you won't need to use unsafe.

Performance

This is not a zero-cost abstraction since we need to search the correct type at runtime in a list.

Comments?

Well, that's definitely not sound. There's no guarantee that those are unique, and in fact, there's an explicit non-guarantee.

The returned string must not be considered to be a unique identifier of a type as multiple types may map to the same type name. Similarly, there is no guarantee that all parts of a type will appear in the returned string. In addition, the output may change between versions of the compiler. For example, lifetime specifiers were omitted in some earlier versions.

1 Like

Ah, I was afraid of that but I missed that when I quickly dropped TypeId since I could not sort that.

Thank you.

TypeId is at least const-comparable on unstable, so I will switch back to that, hoping it won't take 10years to get to a const .cmp(). I'll have to drop the sorting, but that was just for performance anyway.

I think I am also casting the pointers to usize the wrong way, just a union with *const *const and usize. I will switch that to *mut() stile maybe tomorrow.

It's just a a round-trip, so it should be fine?

I didn't really review the code after I read it was based on type_name, but let's see if I can make sense of it...

unsafe fn cast_to_mut(&mut self, trait_num: usize) -> usize {
// ...
                            union U {
                                ptr: *mut *mut dyn AnyTrait,
                                raw_ptr: usize,
                            }
/* X */                     let t = &mut *(self as *mut dyn AnyTrait);
                            let tmp = U {
/* Y */                         ptr: &mut (t as *mut dyn AnyTrait),
                            };
/* Z */                     return tmp.raw_ptr;

Your roundtripping through unions, like at Z, is basically just an indirect way to transmute or transmute_copy. I'd probably avoid usize and instead go with [*const (); 2] or such to avoid some ambiguity around provenance.

At X, that's just

let t: &mut dyn AnyTrait = self;

But most importantly, at Y you're storing the address of a temporary. The pointed-to place will be invalidated at the end of the block. So your downcast methods are performing UB by reading through the returned pointer.

You can presumably run your tests with Miri to see this. You can run Miri in the playground until Tools, top right.


So the idea seems to be that the macro generates a way to create a type-erased pointer to the implementing type as &Self or as &dyn Trait for some set of specified traits. Getting a valid pointer isn't a problem, though type erasing it is a challenge. Then dyn AnyTrait can generically downcast based on that type erased pointer.

It turns out that you can destructure and restructure potentially wide pointers on stable. Layout is more the concern than size, as the FCP'd safety invariant is that the metadata is a valid word-aligned pointer. But there are enough pointer-manipulating methods available that the layout can be determined too.

I didn't make any compile-time considerations, but here's a POC that follows the basic shape of your trait as I understand it.

It's quite possible that there's a crate which does this or something analogous; I didn't go looking.

That's all I was looking for, I think.

Thank you for the help!

I spent the day studying and trying to understand rust internals, you post and code was immensely helpful, thank you.

I did check a bit and I did not find another no_std crate that implemented only the type erasure, though I did find others re implementing close to the same thing.

I ended up copying your playground AnyPtr and modifying very little (I also cleared up how license/copyright does not apply in that file) since I did not want to take a lot of other dependencies, too.

miri does not complain anymore, too.

I'll play around it a bit more, maybe I'll go bug someone for that const Ord, but I'm happy with the results, so thanks again.

1 Like

There's this old tracking issue, but the implementation was yanked since then due to concerns about committing to the same TypeId ordering at compile time and run time (and otherwise exposing the hash/exact implementation). Here's a newer one.

Const equality comparison for TypeId isn't contentious and is implemented on unstable.

ptr_meta — Rust library // Lib.rs I believe. Rkyv is one of the dependants.