Why can't I impl dyn a trait with static methods, and what's the correct way to achieve this?

In the following code (playground):

pub trait Item {
    fn get_category()->String; // error
    fn do_thing_inner(&self);
    fn get_inner_val(&self) -> u32;
}

impl dyn Item {
    pub fn do_thing(&self) {
        self.do_thing_inner();
        //etc.
    }
    pub fn get_val(&self) -> u32 {
        self.get_inner_val() + 1
    }
}

having a static method on the trait is an error, however in some generic code I'm writing, I need to be able to access static data about types which implement this trait. I tried putting get_category in a different trait e.g. Categorizable but then having trait Item: Categorizable { /* ... */ } gives the same error.

Why is this the case? And more importantly, what is the recommended way to achieve something like this?

You can't call static methods on a dyn Trait object because each implementation has a different method, and unless you provide an actual object (with vtable) to inspect, there's no way to discern what method you're intending to call. Provide an object argument, and the code will have access to a vtable and the call will be resolvable dynamically. (Or manually implement the dispatch yourself using enums or something similar.)

1 Like

What do you mean by “something like this”?


I have a strong feeling for a potential XY problem in this thread, since you don’t provide any reason as to what you are trying to archive, let alone how you’d imagine an associated function (without taking self) on a dyn Trait type could even work at all.

In other words: Please give us some pseudo-code or whatever of what your imagined use-site code looks like, and an example what you want to archive with it.

3 Likes

By the way, for a more detailed answer than the practical considerations that @skysch already explained, you can look up “object safety” for figuring out when exactly traits can or cannot be turned into trait objects.

1 Like

That object safe link is really useful, thanks! And regarding the XY problem... That's fair. I tried to give a minimal example, since my actual problem is a rather convoluted mess involving a few different issues that seem almost mutually exclusive in terms of solving them in a remotely elegant way. I'll try to figure out a small-ish example that illustrates my overall problem.

1 Like

So my initial/overall goal is pretty simple. I need an Entity struct that can hold Components. Moreover, this is in a lib, and I'd prefer to avoid something like Entity<T> (where T is an enum provided by the consumer of the lib), and also because the number of components can be rather large, I think Component should be a trait.

I'd like to be able to access a component by type since in a previous c++ codebase I was working on, I found it very elegant and easy to use, and it's faster than e.g. string keys (probably, I haven't benchmarked this in rust, but I did do benchmarks back in c++ land)

let e: Entity;
//stuff
let name: &str = e.get<Name>();

So far so good, my code looks more or less like:

//entity.rs
use std::any::TypeId;
use std::collections::HashMap;
use crate::component::Component;

pub struct Entity {
    components: HashMap<TypeId, Box<dyn Component>>,
}

impl Entity {
    pub fn get<T: 'static + Component>(&self) -> Option<&T> {
        self.components
            .get(&TypeId::of::<T>())
            .and_then(|c| c.cast::<T>())
    }
    pub fn get_mut<T: 'static + Component>(&mut self) -> Option<&mut T> {
        self.components
            .get_mut(&TypeId::of::<T>())
            .and_then(|c| c.cast_mut::<T>())
    }
}
//component.rs
use std::any::Any;

pub trait Component {
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
}

impl dyn Component {
    pub fn cast<T: 'static + Component>(&self) -> Option<&T> {
        self.as_any().downcast_ref::<T>()
    }

    pub fn cast_mut<T: 'static + Component>(&mut self) -> Option<&mut T> {
        self.as_any_mut().downcast_mut::<T>()
    }
}

I haven't done much testing but this compiles and seems like it should work fine. The issue is that Entitys need to be De/Serializeable. I'm aware of typetag which is wonderful, but I'm writing a server that communicates to a client in wasm. and wasm doesn't yet support rust-ctor.

The best solution I can come up with that still lets me do all this (barring a messy proc-macro saving to an external file, which may not be possible to do soundly) is a runtime record mapping some kind of identifier to a set of serialize/deserialize functions.

But in order to do that I need to have a generic function serialize<T: Component> or somesuch and it'd be super cool to be able to do T::custom_type_id() or something, but the real kicker is deserialize which doesn't get passed an actual T. all it has is the static type and a string to differentiate them.

It's absolutely possible I got lost along the way and there's a way better way to do all of this, but I'm really stuck and I felt so close, I even thought I could just split up the trait but even with erased-serde, it doesn't seem possible to do this the way I am? It feels very much like rust is just barely not expressive enough to do what I want (there's like 10 different issues that would have let me solve this in different ways, but all of my ideas got blocked by e.g. specialization, or enum variants as types, or wasm support for ctor, or a bunch of other things I don't remember, I've been working on this for a while now and I've hit blocks on a lot of open issues over the last few days)

Oh well, this seems non-trivial and also involving things I have never tried myself (like working with WASM for example).

Since it seems like you have a list of all the possible component types, you could store the necessary information in a global map indeed. I don’t know if this helps in any way but if you prefer having the “identifier” for your type associated to the type through your Component trait, you can use the Self: Sized bound to exclude the method from trait objects without violating object safety:

use std::any::Any;
pub trait Component {
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
    fn my_custom_identifier() -> &'static str where Self: Sized;
}

impl dyn Component {
    // still works...
}

Then you would still need to instantiate the global map with the necessary information to map from the identifier back to something useful, of course. I’m not too knowledgeable about how deserialization with serde or whatever works in detail, they seem to be using a lot of generic methods in traits, any you’d need to map your identifier to some non-generic function. But provided you find a useful signature that can be implemented generically like

fn my_deserialization_helper<T: Component>(
    input: /* whatever goes here */, ...
) -> ... /* some output involving */ Option<Box<dyn Component>> /* or so */ {
    // ...
}

that can be used to deserialize your components somehow, then you should be able to populate your global map of type HashMap<&'static str, fn(/* same signature as above */) -> /* ... */> with entries of the form .insert(T::my_custom_identifier(), my_deserialization_helper::<T>).


As I said, I have no idea about this, especially whether this is practical at all. I have also never seen typetag before and didn’t try to find out how it works in comparison yet (or why it can’t be used in your usecase, for that matter). Everyone else, please feel free to provide better ideas.

1 Like

Thank you so much! I didn't even consider using a where bound like that! That's exactly what I needed for the initial post! There might still be a less messy way, but at least now I can get something that compiles. I think I'll leave the question open for a bit longer in case anyone drops by with a more elegant solution since this is in theory determinable at compile time, but otherwise I'll mark that as a solution.

Well, it’s the kind of thing you start thinking about when you read that

All associated functions must either have a where Self: Sized bound, or

line in the reference page on object safety that I linked.

1 Like

I'm slightly surprised about this because this is the output in stable:

error[E0038]: the trait `Item` cannot be made into an object
 --> src/lib.rs:7:6
  |
1 | pub trait Item {
  |           ---- this trait cannot be made into an object...
2 |     fn get_category()->String; // error
  |        ------------ ...because associated function `get_category` has no `self` parameter
...
7 | impl dyn Item {
  |      ^^^^^^^^ the trait `Item` cannot be made into an object
  |
help: consider turning `get_category` into a method by giving it a `&self` argument or constraining it so it does not apply to trait objects
  |
2 |     fn get_category()->String where Self: Sized; // error
  |                               ^^^^^^^^^^^^^^^^^

Note the end of the error where we provide a structured suggestion with this exact code. Could you help me understand why you might have not noticed it?

2 Likes

Yeah now I feel dumb, I should have looked harder at the playground as I was posting this.

I think I thought the second part was a duplicate of the same error rather than a suggestion on how to fix it, also I didn't realize that the playground output scrolled horizontally until right now, which is where it actually says "or constraining it so it does not apply to trait object", I just saw the " help: consider turning get_category into a method by giving it a &self argument

In my defense adding a static method was about the 10th thing I tried to get around my issue, and there were 3 or 4 almost-posts on here that I scrapped after typing up because I either figured them out or found confirmation that what I needed for a given solution was definitely impossible.

This was definitely a case of me seeing the "add a &self param!" and not parsing the remainder.

I probably should have just slept on it and come back in the morning, and I would have seen it.

2 Likes

Update: the output in nightly is still verbose, but it is now hopefully clearer:

error[E0038]: the trait `Item` cannot be made into an object
 --> src/lib.rs:7:6
  |
7 | impl dyn Item {
  |      ^^^^^^^^ `Item` cannot be made into an object
  |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
 --> src/lib.rs:2:8
  |
1 | pub trait Item {
  |           ---- this trait cannot be made into an object...
2 |     fn get_category()->String; // error
  |        ^^^^^^^^^^^^ ...because associated function `get_category` has no `self` parameter
help: consider turning `get_category` into a method by giving it a `&self` argument
  |
2 |     fn get_category(&self)->String; // error
  |                     ^^^^^
help: alternatively, consider constraining `get_category` so it does not apply to trait objects
  |
2 |     fn get_category()->String where Self: Sized; // error
  |                               ^^^^^^^^^^^^^^^^^

error[E0038]: the trait `Item` cannot be made into an object
 --> src/lib.rs:8:21
  |
8 |     pub fn do_thing(&self) {
  |                     ^^^^^ `Item` cannot be made into an object
  |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
 --> src/lib.rs:2:8
  |
1 | pub trait Item {
  |           ---- this trait cannot be made into an object...
2 |     fn get_category()->String; // error
  |        ^^^^^^^^^^^^ ...because associated function `get_category` has no `self` parameter
help: consider turning `get_category` into a method by giving it a `&self` argument
  |
2 |     fn get_category(&self)->String; // error
  |                     ^^^^^
help: alternatively, consider constraining `get_category` so it does not apply to trait objects
  |
2 |     fn get_category()->String where Self: Sized; // error
  |                               ^^^^^^^^^^^^^^^^^

error[E0038]: the trait `Item` cannot be made into an object
  --> src/lib.rs:12:20
   |
12 |     pub fn get_val(&self) -> u32 {
   |                    ^^^^^ `Item` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/lib.rs:2:8
   |
1  | pub trait Item {
   |           ---- this trait cannot be made into an object...
2  |     fn get_category()->String; // error
   |        ^^^^^^^^^^^^ ...because associated function `get_category` has no `self` parameter
help: consider turning `get_category` into a method by giving it a `&self` argument
   |
2  |     fn get_category(&self)->String; // error
   |                     ^^^^^
help: alternatively, consider constraining `get_category` so it does not apply to trait objects
   |
2  |     fn get_category()->String where Self: Sized; // error
   |                               ^^^^^^^^^^^^^^^^^

error: aborting due to 3 previous errors

I might take a look at deduplicating these at some point.

4 Likes