Cross casting of object safe trait objects

From the rust std::any module doc:

Note that &dyn Any is limited to testing whether a value is of a specified concrete type, and cannot be used to test whether a type implements a trait.

Why is this limitation imposed ? What prevents this from getting implemented ?

The situation is that I would like to cross cast between "object safe" traits kind of like C++ dynamic_cast except that I do not want to go into the implementation concrete type to do so.

Well, how would you implement it? Consider for example that types could implement infinite traits, how do you solve this problem? (Just to be clear this is not the actual problem, but it can give you an example of the challenges you need to solve)

Embed the implemented traits in static storage .rodata for the given concrete type (you can opt in for this maybe using an attribute if size of the binary is critical). This way the default implementation of any::Any can refer to that and provide cross cast capabilities given just trait objects. A new method could introduced in any::Any called cross_cast maybe that provides this lookup ?

any::Any {
   .
   .
   .
   fn cross_cast<T>(&self) -> Option<&T> { ... }
   
   fn cross_cast_mut<T>(&mut self) -> Option<&mut T> { ... }
}

When would you generate this list? A crate depending on the crate defining the type in question could have implemented the trait, so it can't be generated when doing the cast to &dyn Any. This may even come from a dylib at runtime. And you could have a generic trait implemented for it, in which case there are an infinite amount of traits that should be added to the list, which is not possible.

So what if blanket and generic traits are implemented on T across crate boundaries: The final artifact is either an a.out or liba.so or liba.a in which case at compile time you know ahead of time all the traits implemented by a type. If this were not the case: trait bounds would have failed as a lang feature on rust. Trait bounds work because at compile time you know what traits are implemented by the concrete type T.

Why not use the above fact and embed the type information as a list in the .rodata for every concrete type implementing a trait ? You can opt into it using an attribute like:

#[embed_type_info]
struct MyType { ... }

The final list of traits is determined by the combination of the executable and all dylibs that will be loaded at runtime, which we may not know yet at compile time. Also even ignoring dylibs you did still have to list all trait impls at link time, but rustc may not be in control of linking and thus may not be able to generate this list.

@bjorn3 why would rustc not be able to control the link stage ? are you saying rustc is incapable of communicating to the linker being used ? This does NOT sound right. You can specify linker options in the toml file.

Some build systems directly invoke the linker bypassing the regular rustc handling. In that case any -Clink-arg and #[link(name="...")] will indeed by ignored.

this is a problem on the consuming end which rust should not be interested in solving. besides the return type is an Option<&T>/Option<&mut T> , if type info in not embeded it simply return None ? You get what you enable.

When mixing C/C++ and Rust letting rustc do the linking is often not feasible.

No, you would get a linker error due to the missing symbol. And even ignoring the "build systems bypassing rustc linking" problem, you still have the "dylib" problem and the "infinite implementations due to generic" problem.

2 Likes

This could be solved using monomorphization you embed what the compiler can synthesize , is it not ?

Just because I had MyConcreteType<T> does not mean it gets used. The type is dead unless it gets instantiated somewhere.

Stepping back to a higher level, the primary things preventing this are:

  • A lack of a detailed implementation plan that addresses the difficulties that have been raised in this thread
  • Rust's developers prioritizing other areas of the language over this one

If you want this to move forward, you'll probably have to drive it yourself. To do that:

  1. Discuss potential designs with the Rust maintainers someplace like IRLO
  2. Write an RFC for the final design, and get it accepted (NB: This also requires convincing people that the added capabilities are worth the increased complexity due to the design)
  3. Convince some developers to actually implement the RFC
  4. Demonstrate that the implementation in nightly is both useful and doesn't introduce any soundness holes
  5. Put the feature up for stabilization
5 Likes

The snippet you posted was about the ability to dynamically test if a type implements a trait. But the question in the subject line is about being able to coerce between different trait objects.

To read more about the latter, you could pursue the upcasting RFC (which is a subset of that functionality), and the related PR, tracking issue, et cetera to learn more about the challenges, concerns, and costs. It's not yet stable.

There are workaround for many scenarios.

1 Like

Ok. So i did did do some digging here to arrive at a cheap solution: My use case here is to at-least cross cast between "object safe" traits and the only way that is possible is if the traits to cross cast to and from are object safe themselves and derive from a common object safe trait.

The common trait is called DynCast and the marker trait called TraitObject.

  • Now I need to automate this with the help of a procedural macro where by "object safe" traits counterpart struct "trait objects" are auto generated as that is boiler plate code along with the implementation of DynCast for the implementation type. I do not know how to write procedural macros so that is where am stuck at for now.

something like this:

struct MyType {...}

trait OOSafe0 : DynCast { ... }

trait OOSafe1: DynCast { ... }

impl OOSafe0 for MyType { ... }
impl OOSafe1 for MyType { ... }

// need help here
generate_boiler_plate!{ OOSafe0, OOSafe1 } 

// need help here
associate_impl! { MyType, OOSafe0, OOSafe1 }

There should ideally be cross cast capabilities for "object safe" traits.. provided by any module.

This is a way to test at runtime if some anonymized blob implements a trait, but it's pretty unidiomatic. It Rust you're generally ensuring such things at compile time. Is there a concrete, practical goal outside of emulating something you're used to from another language?

1 Like

Oh I see. the trait system is more oriented towards compile time checks.
no use case , just tinkering around the rust lang and seeing how it relates to object oriented stuff. object safe interfaces should be provided the minimum feature of cross casting/dynamic_casting across interfaces if you do allow dynamic dispatch without having to descend into the implementation type as done using Any.

Here's how you could remove a bit of boilerplace (could still be improved especially to avoid an allocation everytime you downcast) Rust Playground

Note though that this is neither efficient nor fully general purpose:

  • for every type you need to register all the traits you want it to be downcastable to, which still has the problem that there traits might be a lot or even infinite;

  • the downcast logic is slow, it needs to check every single trait singularly to check if its the one wanted.

1 Like

@SkiFire13 this is really good thank you. Am ok with registering aspects of the system as long as the traits are object safe. As far as the inefficiency of lookup goes: a static hash map with the key as typeid and value as an any would help, lookup could be sped up to some level, just not sure how fast. The problem with this approach is the trait object is cached in the static hash map it would never be destroyed, something to think about.

Nit: I just added a trait bound to your solution: Rust Playground

@Self following Meta Marker Traits: Traits to classify rust type system - language design - Rust Internals I revisited the playground to avoid that weird TraitObjectWrapper and the additional allocation it involved. Rust Playground

2 Likes