Event bus with callbacks that take differently typed parameters

Hi there,

I'm trying to implement a simple event bus system with an interface kind of like this:

struct EventBus {
	...
}

impl EventBus {
	fn new() -> Self {
		...
	}
	
	fn listen<T>(&mut self, callback: &dyn FnMut(&mut T)) {
		// register a callback for events with type T
		...
	}
	
	fn emit<T>(&self, event: &mut T) {
		// invoke all callbacks registered for events with type T
		...
	}
}

To be used like this:

let mut event_bus = EventBus::new();
event_bus.listen(|event: &mut TestEvent| {
	println!("test event has been invoked!");
});
event_bus.emit(&mut TestEvent::new()); // should cause "test event has been invoked!" to be printed

But I can't find a way to properly store, retrive, and invoke the callbacks passed into the listen method, given that all of them may have a different type for their parameter.

Is there a good way that I could do this, or am I better off taking a different approach?

Thank you! :slight_smile:

You're going to need an enum to hold different number of arguments, and probably emit1, emit2, emit3, etc. Rust can't abstract number of function arguments. Alternatively, use one argument that is a tuple.

Your T type isn't type safe, because the whole construct is entirely dynamic. There's nothing stopping anyone from calling emit with wrong number of arguments, and wrong types of arguments.

You'll probably need to use dyn Any. I haven't checked, but it would be cool if dyn Any's downcast worked for dyn FnMut(type supplied by the caller of emit) to check that the callback type matches.

If you made events into types, then you could make the T an associated type of the event.

struct Hello;
impl Event for Hello {
   type Arg: String,
}
…
fn emit<E: Event>(&self, arg: E::Arg) {…}
…
event_bus.emit::<Hello>("World".to_string());
1 Like

@kornel already mentioned Any and you will need some sort of any to make this work.

I would add a second trait

trait AnyEventBus {
    fn listen_any(&mut self, callback: Box<dyn FnMut(&mut Any)>);
    fn emit_any(&self, event: &mut Any);
}

Note the boxed callback because it needs to be stored somewhere.

And then you can implement the AnyEventBus trait for EventBus:

impl<E:EventBus> AnyEventBus for E {
    fn listen_any(&mut self, callback: Box<dyn FnMut(&mut Any)>) {
       self.listen(Box::new(|any| {
           if let Some(data) = any.downcast_mut() {
               callback(data);
           }
       }));
     
           
    fn emit_any(&self, event: &mut Any) {
        if let Some(data) = event.downcast_ref() {
           self.emit(data);
        }
    }
}
1 Like

There is most definitely room for optimization. For example you could store callbacks for each T in a typemap and only invoke those with the correct type. That avoids invoking callbacks that do nothing because the downcast fails and the repeated downcasts because the type matches.

2 Likes

you could store callbacks for each T in a typemap and only invoke those with the correct type

It turns out that is exactly what I needed to do this! After reading your reply, I found the typemap crate and used it to make this implementation:

extern crate typemap;

use typemap::{ Key, TypeMap };

pub struct EventBusKey<T> {
	callbacks: Vec<Box<dyn FnMut(&mut T)>>
}

impl<T> EventBusKey<T> {
	fn new() -> Self {
		Self { callbacks: Vec::new() }
	}
}

impl<T: 'static> Key for EventBusKey<T> {
	type Value = Self;
}

pub struct EventBus {
	events: TypeMap
}

impl EventBus {
	pub fn new() -> Self {
		Self { events: TypeMap::new() }
	}
	
	pub fn listen<T: 'static, F: 'static + FnMut(&mut T)>(&mut self, callback: F) {
		self.events.entry::<EventBusKey<T>>()
			.or_insert_with(|| EventBusKey::new())
			.callbacks.push(Box::new(callback));
	}
	
	pub fn emit<T: 'static>(&mut self, event: &mut T) {
		match self.events.get_mut::<EventBusKey<T>>() {
			Some(key) => {
				for callback in &mut key.callbacks {
					callback(event);
				}
			},
			None => ()
		}
	}
}

Which works exactly how I want!

You were both right about how this would require using Any, but TypeMap abstracts that away
and allows me to keep it out of my interface as I had originally wanted.

So problem solved! I honestly didn't think this would be possible.

Thanks so much for your replies! ^^

2 Likes

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.