Passing `Any` payload in events

I would like to pass events (Events in the example below) though message queues which can carry arbitrary payload (Event::payload). I intend to use trait objects and the Any trait for this.

This is what I do, basically:

pub trait EventPayload: Any + Debug {}
impl<T: ?Sized + Any + Debug> EventPayload for T {}

#[derive(Clone, Debug)]
pub struct Event {
    pub payload: Arc<dyn EventPayload>,
    // more fields go here
}

So some 3rd party crate can simply define:

#[derive(Debug)]
struct SomeEvent;

And some function could look like this:

fn some_handler(event: Event) {
    // I can make use of the `Debug` trait:
    println!("DEBUG: {event:?}");
    // But these fail:
    //if event.payload.is::<SomeEvent>() {
    //if (&*event.payload as &dyn Any).is::<SomeEvent>() {
    // And this compiles but produces the wrong output:
    if (&event.payload as &dyn Any).is::<SomeEvent>() {
        println!("Got SomeEvent.");
    } else {
        println!("Got a different event.");
    }
}

fn main() {
    some_handler(Event { payload: Arc::new(SomeEvent) });
    some_handler(Event { payload: Arc::new(()) });
}

(Playground)

But as you see, it doesn't work:

DEBUG: Event { payload: SomeEvent }
Got a different event.
DEBUG: Event { payload: () }
Got a different event.

I assume that's because Arc<dyn EventPayload> is a different type than SomeEvent.

There is a workaround I found, but I'm not really happy with it:

pub trait EventPayload: Any + Debug {
    fn type_id(&self) -> TypeId {
        Any::type_id(self)
    }
}

fn some_handler(event: Event) {
    println!("DEBUG: {event:?}");
    if Any::type_id(&*event.payload) == Any::type_id(&SomeEvent) {
        println!("Got SomeEvent.");
    } else {
        println!("Got a different event.");
    }
}

(Playground)

Output:

DEBUG: Event { payload: SomeEvent }
Got SomeEvent.
DEBUG: Event { payload: () }
Got a different event.

However, this doesn't allow downcasting, plus it's uglier.

I have several questions:

  • Can I use an Arc<dyn EventPayload> use Any's methods somehow to do type checks and downcasting? Or do I need to implement those methods redundantly on EventPayload? If I have to do it, how can I implement downcast, which has a type argument and is thus not object safe?
  • Is using an Arc the idiomatic way to permit cloning of unknown/dynamic types? (assuming I won't mutate those objects)
  • Is downcasting considered to be sound at all? I'm asking because of #10389.

[quote]

if (&event.payload as &dyn Any).is::<SomeEvent>() {

...
I assume that's because Arc<dyn EventPayload> is a different type than SomeEvent.
[/quote

Yes, exactly. Any time you use as to obtain a dyn value, you're constructing one whose concrete type is the type that is statically known at that call site, which is Arc<dyn EventPayload>.

Hopefully soon, Rust will have trait upcasting which will allow you to go from &dyn EventPayload to &dyn Any and use Any's functions to downcast. For now, you have to explicitly provide upcastability, such as with a method on EventPayload like fn as_any(&self) -> &dyn Any;. Here is some more information on the as_any pattern.

Thank you. I tried to apply everything and ended up with this:

use std::any::Any;
use std::fmt::Debug;
use std::sync::Arc;

pub trait EventPayload: Any + Debug {
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
    fn into_boxed_any(self) -> Box<dyn Any>;
    fn into_arc_any(self) -> Arc<dyn Any>;
    // What about `Rc`, etc.? This is endless, isn't it?
}

impl<T: Any + Debug> EventPayload for T {
    fn as_any(&self) -> &dyn Any {
        self
    }
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
    fn into_boxed_any(self) -> Box<dyn Any> {
        Box::new(self)
    }
    fn into_arc_any(self) -> Arc<dyn Any> {
        Arc::new(self)
    }
}

#[derive(Clone, Debug)]
pub struct Event {
    pub payload: Arc<dyn EventPayload>,
    // more fields go here
}

#[derive(Debug)]
struct SomeEvent;

fn some_handler(event: Event) {
    println!("DEBUG: {event:?}");
    if (&*event.payload).as_any().is::<SomeEvent>() {
        println!("Got SomeEvent.");
    } else {
        println!("Got a different event.");
    }
}

fn main() {
    some_handler(Event { payload: Arc::new(SomeEvent) });
    some_handler(Event { payload: Arc::new(()) });
}

(Playground)

Some questions remain:

  • Is this idiomatic?
  • Why don't I need it when using the Debug trait in my original example(s)?
  • Should I also provide as_any_mut? Then what about into_boxed_any or others?
  • Note the &* in some_handler. Without it, I get the wrong method chosen (because Arc<dyn EventPayload> implements EventPayload as well, I think). Is there anything to make this less error-prone?

Another thing I noticed: There is an impl Debug for dyn Any + 'static. So I don't really need the EventPayload trait to imply Debug:

#[derive(Clone, Debug)]
pub struct Event {
    pub payload: Arc<dyn Any>,
    // more fields go here
}

#[derive(Debug)]
struct SomeEvent;

fn some_handler(event: Event) {
    println!("DEBUG: {event:?}");
    if event.payload.is::<SomeEvent>() {
        println!("Got SomeEvent.");
    } else {
        println!("Got a different event.");
    }
}

(Playground)

This works just as fine (though removing the supertrait from EventPayload makes the code fail to compile).

So I guess I will just use the EventPayload trait ("alias") if I need to use any other traits than Any and Debug. Is that the right way to go, i.e. idiomatic?

Well, you've implemented lots of kinds of Any transformation. I'd expect to see a narrower set pertaining to what you actually need. If every possibility might appear maybe the EventPayload shouldn't be a trait at all, just a struct containing an ordinary Box<dyn Any> (or Arc<dyn Any>) — that way, you're not reimplementing all of Any, just providing it.

Better to provide an existing type than to invent a copy of it of unclear scope.

Why don't I need it when using the Debug trait in my original example(s)?

Because Debug isn't going through the Any functions which need to receive a dyn Any — it's just an ordinary trait method being invoked through the dyn EventPayload vtable.

  • Note the &* in some_handler. Without it, I get the wrong method chosen (because Arc<dyn EventPayload> implements EventPayload as well, I think). Is there anything to make this less error-prone?

No; since you've blanket-implemented EventPayload for very many types, there is no way to automatically nail down which type you mean to invoke as_any on.

My idea was that I would like some traits to be required to be implemented, i.e. Debug. But since I don't need Debug being implemented, I think I might just use Any instead of making my own trait.

So I likely will end up with just this:

#[derive(Clone, Debug)]
pub struct Event {
    pub payload: Arc<dyn Any>,
    // more fields go here
}

How about the Arc? I guess that's easiest if I need Event to be Clone, right? I've seen there is also dyn-clone, which I guess would be an alternative to use Arc?

In my experience Arc is fine. If you don't need send, then there is also Rc, but I believe that Arc is a well-established workhorse. Only in case of e.g. measured (!) performance bottlenecks I would rework this later

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.