Generics in structs and how to erase them

Let's say I want to build a simple callback system:

pub struct PropertyCallback<T> {
	object: Weak<RefCell<T>>,
	callback: Box<dyn FnMut(&mut T, &EventData)>,
}

impl<T> PropertyCallback<T> {
	fn call(&mut self, event: &EventData) {
		(*self.callback)(
			&mut *self.object.upgrade()
				.expect("Callback object has been dropped")
				.borrow_mut(),
			event,
		);
	}
}

The listener is wrapped in a RefCell, we store a weak reference to it and the callback takes the event and may modify itself [the listener].

Now, let's have a struct that can store a callback in order to notify it if something interesting happens:

pub struct EventGenerator {
	callback: PropertyCallback<???>
}

That's where the trouble starts. I don't want EventGenerator to be generic, as any RefCell<T> could be used as callback, but I still the generics inside PropertyCallback.

Non-solution

I've tried using trait objects:

pub struct EventGenerator {
	callback: PropertyCallback<Box<dyn Any>>
}

This does not work because it means that the only thing I can call back is a RefCell<Box<Any>>. It also means that the &mut T inside the calback closure becomes a Box<Any> forcing a downgrade in every callback implementation.

Ugly solution

I can take the call method and put it into a trait:

pub trait PropertyCallbackTrait {
	fn call(&mut self, old_value: &PropertyValue, new_value: &PropertyValue);
}

this allows me to use callback: Box<dyn PropertyCallbackTrait> in my struct. While this does the trick, I really dislike the extra boilerplate for defining a separate trait for each of my structs where I want to erase generics.

What I want

Ideally, I'd like to have some type like Box<dyn PropertyCallback<_>> that acts mostly like a trait object, but without the requirement of an extra trait.

I'll happily take alternative design suggestions for my callback system that don't suffer from this problem.

Since the only method of a callback is call, can you use a closure directly instead of wrapping it in another object? Any extra context can be moved into the closure at creation time.

type PropertyCallback = Box<dyn FnMut(&PropertyValue, &PropertyValue)>;
3 Likes

What about putting this ErasedEventHandler inside your EventGenerator?

use std::{any::Any, rc::Rc};

pub trait EventHandler<E: ?Sized> {
    fn handle_event(&self, event: &E);
}

pub struct ErasedEventHandler(Box<dyn Fn(&dyn Any)>);

impl ErasedEventHandler {
    pub fn new<H, E>(handler: &Rc<H>) -> Self
    where
        H: EventHandler<E> + 'static,
        E: 'static,
    {
        let weak = Rc::downgrade(handler);

        ErasedEventHandler(Box::new(move |event| {
            if let Some(event) = event.downcast_ref::<E>() {
                if let Some(handler) = weak.upgrade() {
                    handler.handle_event(event);
                }
            }
        }))
    }
}

impl EventHandler<dyn Any> for ErasedEventHandler {
    fn handle_event(&self, event: &dyn Any) {
        (self.0)(event);
    }
}

(playground)

Some key differences:

  1. I've removed the RefCell and &mut self. If people want mut then they can do the interior mutability themselves. Us doing it unconditionally has implications for re-entrant code (i.e. your callback can do something which would trigger the callback), and the non-Rc version would need to pay for a Mutex
  2. Because we're using &dyn Any for the event type, events must be 'static (i.e. can't contain borrowed data like &str strings). This is required for downcasting.
  3. I stole @2e71828's idea of using a boxed closure to hide the underlying handler type

This probably isn't what you want though.

Although it succeeds in removing all the generics, our event type is now &dyn Any which means it's easy to forget which type of event you are meant to pass in. You probably want to "remember" the event type and convert a Rc<H> where H: EventHandler<E> into a Rc<dyn EventHandler<E>>, which happens automatically thanks to something called CoerceUnsize.

1 Like

Thank you for your input. The trick with capturing the value into the closure is really cool! I wrapped that type with a constructor that still takes an Rc and a closure with &mut T to hide the upgrade+borrow from the callback.

@Michael-F-Bryan I don't fully understand what you are doing. Why is the EventHandler generic over the type of the event? I'm (relatively) fine with the event type being fixed. What I care about is that the callee could be of any type.

Ah okay, you can drop the generic E then.

My thoughts were that an event handler is such a common construct that you'll want to create reusable code for it (e.g. imagine a "broadcast" EventHandler which contains a Vec<Weak<dyn EventHandler>> and sends the event to all of them), and have it work for any event type.

Here's an amended version without the generic:

struct Event;

trait EventHandler {
  fn handle_event(&self, event: &Event);
}

struct EventGenerator {
  callback: Weak<dyn EventHandler>,
}

impl EventGenerator {
    pub fn new<H>(handler: &Rc<H>) -> Self
    where
        H: EventHandler + 'static,
    {
        let callback = Rc::downgrade(handler);
        EventGenerator { callback }
    }
}
1 Like

Thank you, I understand now.

This has the advantage of being able to have multiple methods in the callback. But I see that fn handle_event(&self, event: &Event); does not take a &mut self. Is this a mistake or by design?

By design. You'll typically register an event handler while also using it from somewhere else, so you can't take a unique (&mut self) reference.

If things are shared it's always better to let the implementor decide whether to use internal mutability when they need to mutate things. That way they can choose when/how to pay the cost of synchronisation, and will be conscious that the event handler isn't re-entrant (i.e. you lock a Mutex inside the handler and do something which would trigger the event handler again, then we deadlock while handling the event because the Mutex has already been locked).

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.