Event handlers with generic arguments in a HashMap – is this even possible?

Hi,

since a few hours, I try to overcome the following problem:

I have a struct with a method that registers event handlers. Every event handler handles a different event type, which can't be united as enum, but have a trait mark implemented.

The problem is, that I can't figure out a good way to store the event handlers.

Here is what I'd like to archive:

fn main() {
    let br = Browser{};
    
    // register event handlers:
    br.on<EventA>(|a| println!("{}", a.foo));
    br.on<EventB>(|b| println!("{}", b.bar));
    // `a` is of type `EventA` and `b` is of type `EventB`
    // Simplified example. Events have all very different fields and event handlers need access to all
}

And here is how I tried to archive it (minimal example). This code is not working, but I hope you can see my approach.

trait DevToolsProtocolEvent {
    const IDENT: &'static str;
}

struct EventA {
    foo: String
}

impl DevToolsProtocolEvent for EventA {
    const IDENT: &'static str = "Domain.name1";
}

struct EventB {
    bar: String
}

impl DevToolsProtocolEvent for EventB {
    const IDENT: &'static str = "Domain.name2";
}

// There is a high number of events, and events can get removed or added, so an enum is probably not a good solution

struct Browser<Event, EventHandler>
where
    Event: DevToolsProtocolEvent,
    EventHandler: FnMut(Result<Event>), 
{
    event_handlers: HashMap<&'static str, Box<EventHandler>>,
    // ^ This is where my problem is. How do I define `event_handlers`?
    // Ideally, without type parameters that I have to copy everywhere.
    // It seems if I use `Browser` anywhere, that thing also needs to implement these type parameters (for example, I have a builder struct with different `build` methods which all have to implement these type parameters, which is a major pain point, to be honest.)
    // For example `pub fn start_and_connect<E, EH>(&self) -> Result<Browser<E, EH>> {`, which results in a "trait bound E is not satisfied" error (so it seems even more code is needed)
}

impl<Event, EventHandler> Browser<Event, EventHandler> where
    Event: DevToolsProtocolEvent,
    EventHandler: FnMut(Result<Event>), 
{
    fn on<E>(&mut self, callback: &mut EventHandler)
        where
            E: DevToolsProtocolEvent
            // Is it somehow possible to reuse the `Event` type parameter?
            // I tries `fn on<E: Event>`, but that is not allowed (in the full code there are more constraints, so that would be useful)
    {
        self.event_handlers.entry(E::IDENT).insert(Box::new(callback));
        // By the way, here I'd like to use `E` itself, instead of `E::IDENT` is that somehow possible? My understanding is, that `E` is something that is not available at run time, but maybe I'm wrong and there is a way.
    }
}

What would be the best way to implement event handlers like that? Is there a better way to structure my code?

You could use Any.

⟶ example of how you could do it

3 Likes

Thank you, @steffahn! :slight_smile:

I'm curious if there are any other possible solutions, so I will let this be "without a solution" for now.

Because this is part of the public API, I tried to eliminate the , _> part of br.on::<EventA, _>.

Because there anyway needs to be a way to remove event handlers and check if an event handler exists, I decided to return a struct similar to hash_map::Entry:

fn main() {
    let mut br = Browser::new();

    br.event_handler::<EventA>()
        .insert(|a| println!("Event A: {}", a.foo));
    br.event_handler::<EventB>()
        .insert(|b| println!("Event B: {}", b.bar));
}

impl Browser {

    // ..

    fn event_handler<E: 'static>(&mut self) -> EventHandlerEntry<E> {
        EventHandlerEntry {
            event_handlers: &mut self.event_handlers,
            event_type: std::marker::PhantomData {},
        }
    }

    // ..

}

pub struct EventHandlerEntry<'a, E: 'static>
{
    event_handlers: &'a mut EventHandlers,
    event_type: std::marker::PhantomData<E>,
        // The compiler complained about unused parameters (even so they are use within the impl block below). Is this the best way to handle this situation?
}

impl<'a, E: 'static> EventHandlerEntry <'a, E>
{
    pub fn insert<F>(&mut self, callback: F) 
    where
        F: Fn(E) + 'static,
    {
        self.event_handlers.insert(
            TypeId::of::<E>(),
            Box::new(move |e| callback(e.downcast_mut::<Option<E>>().unwrap().take().unwrap())),
        );
    }

    pub fn remove(&mut self) {
        self.event_handlers.remove(&TypeId::of::<E>());
    }

    pub fn exists(&self) -> bool {
        self.event_handlers.contains_key(&TypeId::of::<E>())
    }
}

But this isn't a good way to do it, right? Because EventHandlerEntry needs mutable access to event_handlers, it wouldn't be possible to add any other event handler if someone holds on any EventHandlerEntry, right?

I tried, I couldn’t find a way to let you only give a single type parameter. You could always use it like .on(|a: EventA| ...). Perhaps on is not the most fitting name if no type arguments are provided.

Yes, but AFAIR similar concerns apply to the Entry API from HashMap. I suppose you should only use this kind of API if you actually want to (potentially) modify the table. It does not try to replace all the other methods for accessing entries of the table, its main purpose is better performance due to not needing to traverse the table to search for the entry twice in a use case where you want to first look up and then insert/modify/remove an entry. This means that you still need a non-Entry-based API for situations where mutation is not desired.

1 Like

Wow, I didn't realize this was possible :slight_smile:

This is the best design I can think of.

In case someone is interested in the implementation:

I'm still interested in other solutions, but I will mark this as "solved" now (in case someone doesn't want to waste their time on solved questions).

@steffahn: I'm trying to completly understand how your solution works, and I have a followup question regarding the following lines:

Box::new(move |e| callback(e.downcast_mut::<Option<E>>().unwrap().take().unwrap()))

and

handler(&mut Some(e));

(a more complete sample is at the end)

The benefit of handler(&mut Some(e)) (instead of passing e directly) is that the event handler could take ownership if it wants, right?

This pattern probably comes in handy often, so thanks for showing me this.

In this example, however, there wouldn't be any need for using this patter, right? Or is there a benefit or reason that I don't see?

    fn on<E: 'static, F>(&mut self, callback: F)
    where
        F: Fn(E) + 'static,
    {
        self.event_handlers.insert(
            TypeId::of::<E>(),
            Box::new(move |e| callback(e.downcast_mut::<Option<E>>().unwrap().take().unwrap())), // XXX
        );
    }

    fn handle<E: 'static>(&self, e: E) -> bool {
        match self.event_handlers.get(&e.type_id()) {
            Some(handler) => {
                handler(&mut Some(e)); // XXX
                true
            }
            None => false,
        }
    }

It is not easy to get rid of the Option here even though we don’t fully use/need all the capabilities that it offers (we always expect a Some and always replace it with a None). The problem is that I wanted to allow the callbacks to take the events by value but avoid creating unnecessary boxes. I haven’t found a way around the Option that doesn’t involve boxing yet. Of course if the handler doesn’t need ownership over the events then it is easy to do; which is a reasonable setting, I guess.

So why can’t we just get rid of the Option here and pass the events directly? Currently the type inserted into the HashMap is

Box<dyn Fn(&mut dyn Any)>

where the dyn Any is always Option<E> for some event type E. If we would want to pass the event by-value directly, one would perhaps need something like

Box<dyn Fn(dyn Any)>

instead, which is not a legal type since dyn Any is unsized, so a different approach could use

Box<dyn Fn(Box<dyn Any>)>

but then you would need to put all the incoming events on the heap first before passing them.

1 Like

Well one way is to use unsafe code, in particular replacing the Option with ManuallyDrop, which offers a take() method function, too.

1 Like

Thanks for the explanation.

It seems there is a lot that I still don't understand very well (or at all)... :slight_smile:

Minor correction, apparently the type is “legal”, however you cannot really create a function that takes a dyn Any by value (without unstable features), so it wouldn’t really be possible to instantiate this type.

1 Like

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.