Rust polymorphism

Is it possible in safe rust to pass a struct instance through a black box who only takes structs of a certain type and get additional info through not a part of that struct?

in C++ I could have B that inherits from A, call blackbox::some_query(*A) with a pointer to an object of B and when I get the callback(*A) from that typecast it back to B and be able to access my extra fields.

My problem is basically this, I have a system that is a blackbox where I can queue up stuff of a certain format (the data is passed by struct not by trait) and at a later point the system calls my handler (which I control the implementation of by implementing a trait) but I would like more information than the initial struct takes. In c++ I would solve this by polymorphism but struggling to figure out how these things should be solved in idiomatic rust.

If only I had coded the blackbox so it was implemented by trait, then I totally know how I'd fix it...

Your question is hard to answer because you’re a quite vague around what you are trying to do (for example: how does the “blackbox” get the “callback”?), and you explain the technical details mostly only in a C++ setting (for example saying B inherits from A but you also already have implemented your A as a struct in Rust, so what does B look like in your Rust code?)

In case the “blackbox” gets passed the “callback” together with the A, you might just want to include the extra information that sets B apart from A in the callback closure itself (using some Box<dyn Fn...> kind of type).

Add : AsAny as a superbound for your trait, that will enable downcasting (but forbid types with short-lived borrows, since those are incompatible with checked downcasting), where AsAny is defined as follows:

trait AsAny : Any {
    fn as_any_ref (self: &'_ Self) -> &'_ (dyn Any + 'static)
    ;
    fn as_any_mut (self: &'_ mut Self) -> &'_ mut (dyn Any + 'static)
    ;
    fn into_any_boxed (self: Box<Self>) -> Box<dyn Any + 'static>
    ;
    // feel free to add Rc and Arc as well
}
impl<T : Any> AsAny for T {
    fn as_any_ref (self: &'_ Self) -> &'_ (dyn Any + 'static)
    { self }
    fn as_any_mut (self: &'_ mut Self) -> &'_ mut (dyn Any + 'static)
    { self }
    fn into_any_boxed (self: Box<Self>) -> Box<dyn Any + 'static>
    { self }
    // feel free to add Rc and Arc as well
}
  • As to why Any itself doesn't suffice, it's related to Rust not providing upcasting for free, you have to explicitly ask for it, which is what AsAny does.

Then, within your code

let _: &'_ (dyn YourTrait + 'static) = thing;
if let Some(instance) = thing.as_any_ref().downcast_ref::<YourStruct>() {
    // you get to have access to `YourStruct` _inherent_ logic / data
    let _: &YourStruct = instance;
    // ...
} else {
    // classic impl with no extra data
}

Another (less general) option is, if you only have a limited amount of such types, to add direct downcasting methods to your trait:

trait YourTrait {
    // usual stuff

    fn downcast_ref (self: &'_ Self) -> Option<&'_ YourStruct>
    {
        None
    }
}

// Later
impl YourTrait for YourStruct {
    // ...

    fn downcast_ref (self: &'_ YourStruct) -> Option<&'_ YourStruct>
    {
        Some(self)
    }
}

and then you can simply do, with a given thing: &'_ (dyn YourTrait + '_):

if let Some(your_struct) = thing.downcast_ref() {
    // etc.

Finally, if you don't expect the trait to be implemented for an arbitrarily big number of types (i.e., if you don't need extensionability), you should seriously consider replacing your dyn YourTrait with an enum that has all the possible types as variants:

enum DynObject {
    YourStruct(YourStruct),
    // ...
}

// and later
if let DynObject::YourStruct(your_struct) = thing {
    // etc.
5 Likes

To add to this, upcasting to a general type and then trying to downcast is generally an antipattern. If you need the extra information about your types anyway, you should incorporate that into the interface (e.g. by adding the appropriate accessor methods to your common trait).

2 Likes

I tried to generalize/simplify it but I guess it just became harder to understand.

Basically, I have a lib (that I can't alter) that provides a struct A. It has a function I can call that takes an &A as an argument.

At a later point, I can call a separate function in the lib passing a reference to a struct that implements the trait C (C is also defined by the lib). C has a function that the lib calls, passing a reference to an array of all the As I called that frame as well as a reference to an array with the data I want. At this point in the code (inside the function I implemented that was declared by C), I would actually like to have more data passed from my initial callsites than is stored in A. I would've liked to be able to implement a B that is an A and then been able to cast it back to a B.

Had A been a trait rather than a struct, this would've been easy as I can have any struct of my choosing containing any data I like as long as it implements that A needs. Had this been C++ I would've been able create a class (or struct) B that inherits from A and done this with casting. My question is if there is something equivalent or if there's some other way of achieving the same goals (without altering the lib) in idiomatic rust or if simply this lib is a bastard :wink:

So... that only lets me to keep functions right? not data fields?

Started coding rust for the first time this week so some of the more generic stuff is still a bit hard for me to follow :slight_smile:

What does the struct A look like? In that situation I’d probably be looking for an identifier in A that I could use as a HashMap or BTreeMap key, and send the extra information out-of-band.

Nah. Well, maybe, but it still depends on the library exposing an API that allows you to do that. Is A not marked final? Are the behaviors of A that C uses marked virtual? If you need to delete a B through the base class, does A have a virtual destructor? Does the array contain pointers that can be type-erased? Inheritance doesn't make code magically reusable; the code has to be designed for it.

Rust does support polymorphism. It has several forms, in fact. Generics and trait objects are the major ways to achieve polymorphism in Rust (there are potentially others depending on how loosely you define the word). But if the library author did not see fit to expose a generic API or one that accepts a trait object, you have to work with the concrete types.

Yeah, sorry, didn't mean to make it out as a language war. I only mentioned c++ as reference so it would be easier to understand what I mean for those who'd used C++.

Like I wrote above, I completely get how this would've been done had the lib I'm using been using a traits instead of structs for enforcing it's interface so it wasn't in any way meant as saying rust doesn't have polymorphism. I'm just trying to understand how things are done in an idiomatic way as rust seems very much like C or java at first glance, but is way further away in practice and yet still not like the functional languages I've used either even though it incorporates a lot of elements. Just trying to get a grasp of what solutions are used in rust to use rust in the right way, get a feel for the paradigm, if you will.

Unfortunately it's very generic. Basically it's the parameters for a raycast, position, direction, etc, no identifying elements.

I think I just have to come to terms with that I won't be able to and solve what I'm wanting to do from scratch :slight_smile:

You may be interested with the ECS pattern, which is invented for the game industry to replace complex hierarchy graph, and well supported in Rust via crates like specs.

1 Like

Thanks for the tip, luckily I am well versed already :slight_smile:

edit: did not know about specs though, that's really cool, thanks!