Storing reference to generic trait objects in a vec

Hello!
I'm trying to build simple message bus where there will be one bus to which I can add multiple message handlers, that can handle different type of messages.

My initial idea was to use HashMap to store TypeId of a message as a key and vector filled with reference to all handlers that can handle that one type of a message.
Unfortunately it doesn't work as compiler is telling me that I should define handlers with 'static lifetime and I don't know how to work around it.

Here is the link to playground example that shows what I'm trying to do:
Playground

You need to take Box<dyn Handler> as an argument, or impl Handler. If you take a temporary reference, it's cursed to be temporary until the very end (of its short life), and that can't be undone.

If I'll use Box<dyn Handler> or impl Handler it would mean I'm passing ownership of handler to bus, right? If that's the case then I won't be able to add one handler as handler of multiple messages.

To be honest I don't understand why I have to define handlers as static to make this works. Even when I specify 'a life time like below it doesn't work.

fn add_handler<'a, M: Message>(&'a mut self, handler: &'a dyn Handler<M>) {
        let message_type_id = TypeId::of::<M>();
        let boxed_handler = Box::new(handler);

        match self.handlers.get_mut(&message_type_id) {
            Some(handlers) => {
                handlers.push(boxed_handler);
            }
            None => {
                self.handlers.insert(message_type_id, vec![boxed_handler]);
            }
        };
    }

If you want to store the same handler in multiple places, then use Arc<dyn Handler>.

The 'static requirement here is only a symptom. Directly, the problem is caused by a hidden default of Box. Your Vec<Box<dyn Handler>> actually means Vec<Box<dyn Handler + 'static>>. You could change that to Vec<Box<dyn Handler + 'a>>, and with some tweaks that could even make your code compile, but that would be a design error, for two reasons:

  1. There's no point boxing a temporary reference, since it's impossible to make a short-lived reference live longer, and reference and box have identical representation in memory. So you're just wasting time on allocating space for a pointer you already have. If you're going the route of having unmovable handlers strictly tied to a single scope, you may as well use Vec<&'a dyn Handler>.

  2. References can't ever store data. They only temporarily grant permission to view data stored in a parent scope. Their main purpose is not owning anything, and restricting access to the data they refer to to a scope known at compile time. Unless your whole message bus is meant to spend its entire lifetime inside a single function call, and never leave it, tying it to a scope will be painfully inflexible. It's very important to understand that Rust references aren't ageneral purpose of using things "by reference", they are compile-time locks that add static restrictions. If you don't want to restrict something to a scope of a variable or a function call, don't use temporary references.

4 Likes

OK, so it looks like my whole implementation concept is flawed and can't be implemented. How should I tackle it then? What is the recommended "Rust way" to build simple message bus pattern?

As @kornel suggested, you can use Arc instead of Box to solve your ownership and lifetime problems. But doing so reveals a bigger problem.

error[E0308]: mismatched types
  --> src/main.rs:27:31
   |
27 |                 handlers.push(handler);
   |                               ^^^^^^^ expected trait `Any`, found trait `Handler`
   |
   = note: expected struct `Arc<(dyn Any + 'static)>`
              found struct `Arc<(dyn Handler<M> + 'static)>`

You're using HashMap<TypeId, Vec<Arc<dyn Any>>> to store your handlers. Here, the TypeId key refers to the original message type M, but the value type, Arc<dyn Any>, doesn't have any mention of M or its TypeId. The type Arc<dyn Any> is a fat pointer with a pointer to the data and a pointer to a vtable for the Any trait. There would be no way to recover Arc<dyn Handler<M>> from Arc<dyn Any>, because the vtable is lost.

There might be a way to solve this problem, but I would first question why you need to in the first place. What are you trying to accomplish with this message bus? It seems like you're trying to use an approach rooted in dynamic typing, but Rust doesn't really do dynamic typing very well. By throwing away your compile-time knowledge of the types, you're also throwing away a big strength of the language.

I want it to be able to "deliver" a message to all handlers which are able to handle that particular message.
If I dispatch message MessageABC I want it to be delivered to all handlers that can process MessageABC, if I dispatch MessageXYZ and there are no handlers for this type of message I want it to be ignored/dropped.

My initial thought process was that for each message type I will create an entry in HashMap, and store in it Vec<&Handler<M>> and when dispatch method will be called I'll just retrieve a pointers to handlers and call handle method in each one of them.
I couldn't find a way to make it work and I then I have found that there is Box and that I could use Any to achieve some form of dynamic typing and later use down-casting to get back concrete type.

In the end I'm not trying to solve any business problem or build something new - just trying to learn Rust by re-creating solutions that I know from other languages. And if something can't be transferred 1:1 I'm trying to find out why and what is the correct way to achieve the same result doing it using proper Rust way.

Sure. Understood

A few more questions:

  • Will the message bus be used as a library by downstream users? Or will it only be used in your own code?
  • Can you enumerate all of the possible message types upfront? Or do you need to leave the set of message types completely open, so that downstream users can add their own?
  • Who defines the handlers? You? Downstream users? Both?
  • Message types must at least be Clone, right? Otherwise, how would you send a message to multiple handlers? That, or you would need to change the signature of your handle function to take a reference to the message rather than consume it.

I haven't really built a message bus like this before, but my instincts tell me the dynamic typing approach will be difficult. The more static type information you can leverage, the better. Even if you, as the library author, can't know all of the message types, I would think users of the library should be able to know all message types. If that's true, then there's probably a way to do this without dynamic typing.

Here is a playground that shows one way to implement something like this. It uses Arcs for handlers and clones sent messages. The handlers are stored as HashMap<TypeId, Box<dyn Any>> where the Box really contains a Vec<Arc<dyn Handler<M>>>.

1 Like

Interesting. I didn't realize you can downcast to dyn Handler<M> like that, but it does make sense.

Here's a similar example that compiles, where the downcasting is pushed into the Handler trait. It uses Box instead of Arc and doesn't require messages to be Clone. I'm not sure whether that would work for you or not.

Actually, it doesn't make sense. At least not in the way I initially thought. But I understand more now.

At first, I thought this meant it would be possible to do some sort of run-time introspection, where you could examine a dyn Any trait object to see what traits it implements. But that's not true and not possible as far as I can tell.

Instead, I missed the extra layer of indirection. The Vec<Arc<dyn Handler<M>>> is boxed before it is added to the HashMap. The last Box<dyn Any> is what stores the type information for dyn Handler<M>. It's a cool trick, but it's not what I initially thought it was.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.