I'm noticing more and more often that different rust crates use registration of event handlers with any set of parameters the user wants, not the library developer. I have seen this in the following crates: axum, bevy engine, teloxide.
I have a question, how exactly is this done under the hood, how does the library know what parameters the event handler needs, which can have a variable number of parameters, how are they passed to the function? I want to read something on this topic. I can't understand how the library accepts such handler methods in arguments of other methods and no macros are used.
How exactly this works depends on the specifics of the library. But the general pattern is to have a trait with a method called with some state, then implement it for every function with up to X parameters (using a macro) when parameters implement some other trait, that signifies the ability to extract the parameter from the state.
For example:
pub struct State {
// ...
}
pub trait Extractor: Sized {
fn extract(state: &mut State) -> Self;
}
pub trait Handler<Args> {
fn call(&mut self, state: &mut State);
}
// The below is usually done with a macro.
impl<F: FnMut()> Handler<()> for F {
fn call(&mut self, _state: &mut State) {
self()
}
}
impl<Arg1: Extractor, F: FnMut(Arg1)> Handler<(Arg1,)> for F {
fn call(&mut self, state: &mut State) {
self(Arg1::extract(state))
}
}
impl<Arg1: Extractor, Arg2: Extractor, F: FnMut(Arg1, Arg2)> Handler<(Arg1, Arg2)> for F {
fn call(&mut self, state: &mut State) {
self(Arg1::extract(state), Arg2::extract(state))
}
}
pub fn call_handler<Args, H: Handler<Args>>(handler: &mut H, state: &mut State) {
handler.call(state);
}
Type-erasing the handler (for example, in order to store all handlers together) can be done like the following:
pub fn handler_to_dyn<Args, H: Handler<Args> + 'static>(
mut handler: H,
) -> Box<dyn FnMut(&mut State)> {
Box::new(move |state| handler.call(state))
}
Although that is often done with an additional trait.