Demilade Sonuga's blog

All posts

Event Handling II

2023-04-26

In this post, we're going to finish implementing the event-handling scheme

Implementation

Currently, event_hook.rs looks like this:

use crate::keyboard::KeyEvent;

/* COMMENTING THIS OUT, FOR NOW TO AVOID FUTURE ERRORS
// Mediator between the game code and the interrupt service routines
pub struct EventHooker {
    // The functions that will be called when an event occurs
    handlers: Vec<Box<dyn FnMut(EventInfo)>>
}
*/

// The info about an event that will be passed into a handler
// when it is called
pub enum EventInfo {
    Timer,
    Keyboard(KeyEvent)
}

The first thing to do is to uncomment EventHooker. Then we change handlers's type to reflect the new stuff we've created.

use crate::keyboard::KeyEvent;
use crate::vec::Vec;
use crate::boxed_fn::BoxedFn;

// Mediator between the game code and the interrupt service routines
pub struct EventHooker {
    // The functions that will be called when an event occurs
    handlers: Vec<BoxedFn>>
}

// The info about an event that will be passed into a handler
// when it is called
pub enum EventInfo {
    Timer,
    Keyboard(KeyEvent)
}

When we started in the first event handling post, it was mentioned that EventHooker, the main player in this game will have 3 functions:

  1. send_event, to notify the EventHooker that an event has occurred
  2. hook_event, to tell the EventHooker that a function should be called when an event occurs
  3. unhook_event, to tell the EventHooker to no longer call a function when an event occurs

We'll also need a new function, for creating new EventHooker instances.

Okay, this is all wonderful. But before we get on with the code, we need to really think through this scheme before continuing, so we can identify the problems with it before getting on. Ideally, you should come up with the problems yourself before reading on.

Consider a scenario: A keyboard event occurs and the send_event function is called. All the handlers are called. Okay, nice. But then a timer event occurs, and... Which functions in the handler's vector were hooked for the timer event. This is a problem. How can we determine which functions belong to which event?

Well, like a lot of things in programming, there are many ways of approaching this problem, but the way we'll go here is to create two separate vectors for each event. This will work because there are only two events. And even if we do add more events, they can't be that many.

So, our EventHooker becomes:

pub struct EventHooker {
    // DELETED: handlers: Vec<BoxedFn>>
    // Functions to be called when the timer event occurs
    timer_handlers: Vec<BoxedFn>,
    // Functions to be called when the keyboard event occurs
    keyboard_handlers: Vec<BoxedFn>
}

Now, consider this:

index:                  0      1      2      3      4
timer_handlers: vec [ func0, func1, func2, func3, func4 ]

The above represents a vector of boxed functions for handling timer events. Suppose we want to remove func2. How do we do it? Of course, we use the unhook_event function, but how exactly will unhook_event do it? How will it be able to identify func2?

One way we can do this is to return the function's vector index when we add it to the vector with hook_event. This way, we can pass the index to unhook_event which will then use the index to identify the correct function to remove.

But there is another problem with this index-as-identifier scheme. In this scheme, func0, func1, func2, func3, and func4 will have identifiers of 0, 1, 2, 3, and 4 respectively. But when func2 is removed, the functions func3 and func4 will have their indexes changed to 2 and 3.

index:                  0      1      2      3 
timer_handlers: vec [ func0, func1, func3, func4 ]

But the code that calls hook_event will still think 3 and 4 are the ids for func3 and func4. This will lead to chaos later because when unhook_event is called for func3 afterward, the number 3 will be passed as the ID because the calling code still thinks that that is the index. But that's func4's index now, so func4 gets removed instead of func3.

To solve this, we can solve this identifier problem in another way: keep track of a single next ID and whenever a new function is added with hook_event, we assign that ID to it and increment it. That assigned ID is returned from the hook_event function as the function's identifier. The ID is also saved with the function itself in the handlers vector. This way, even if a function is removed, the identifier associated with a function in addition to the handlers vector will remain the same.

Instead of having a vector of BoxedFns, we create a new structure:

#[derive(Clone)]
struct Handler {
    // The identifier for this handler
    id: usize,
    // A function called when an event occurs
    func: BoxedFn
}

And handlers will be vectors of these instead:

pub struct EventHooker {
    /* DELETED:
    timer_handlers: Vec<BoxedFn>,
    keyboard_handlers: Vec<BoxedFn>
    */
    timer_handlers: Vec<Handler>,
    keyboard_handlers: Vec<Handler>,
    // The identifier to be used for the next function added
    next_id: usize
}

There are certainly other problems with the scheme I haven't identified yet and I won't cover them. Do that yourself and fix the problems. But what we have now should be enough to get down with the code.

Let's start with new, to create new EventHooker instances.

use crate::allocator::Allocator;
impl EventHooker {
    // Creates a new EventHooker instance
    pub fn new(allocator: *mut dyn Allocator) -> Self {
        EventHooker {
            timer_handlers: Vec::with_capacity(10, allocator),
            keyboard_handlers: Vec::with_capacity(10, allocator),
            next_id: 0
        }
    }
}

We initialize our handler vectors to be able to hold up to 10 handlers. And initialize next_id to 0. I think that makes sense because 0 is the smallest usize and the next_id field will be increased anytime a new handler is added.

Next: hook_event. With hook_event, we need the caller to tell us the type of event, timer or keyboard, that we're going to be setting up a handler for. We could create the function to accept EventInfo as an argument, but, that's not what it's for. EventInfo is for telling a registered event handler what happened during an event. For the caller to tell us what kind of event they're registering a function for, we can create another enum:

pub enum EventKind {
    Timer,
    Keyboard
}

As you can see, the Keyboard variant does not have any associated info. That is because this enum is solely for telling the kind of event and not what happened during one.

The event_hook function can then accept this and return a usize, which is the new handler's ID.

impl EventHooker {
    // Used by client code to tell the EventHooker to call function `func`
    // whenever an event of the kind `event_kind` takes place.
    // The returned ID is used to identify the handler when removing it
    pub fn hook_event(&mut self, event_kind: EventKind, func: BoxedFn) -> usize {

    }
}

To give our code more meaning, we can use a type alias instead of directly writing usize:

type HandlerId = usize;
struct Handler {
    // DELETED: id: usize,
    id: HandlerId, // NEW
    func: BoxedFn
}
impl EventHooker {
    // DELETED: pub fn hook_event(&mut self, event_kind: EventKind, func: BoxedFn) -> usize {
    pub fn hook_event(&mut self, event_kind: EventKind, func: BoxedFn) -> HandlerId { // NEW

    }
}

Now, what exactly happens here?

  1. First, the appropriate handler vector is determined from the event_kind argument.
  2. Then a new Handler instance is created with the given function as the handler's func field and the current value of next_id as the handler id.
  3. The newly created Handler is pushed into the appropriate vector.
  4. Then the next_id is incremented.
  5. Finally, the newly created Handler's id is returned.

In code:

impl EventHooker {
    pub fn hook_event(&mut self, event_kind: EventKind, func: BoxedFn) -> HandlerId {
        let handler_vec = match event_kind {
            EventKind::Timer => &mut self.timer_handlers,
            EventKind::Keyboard => &mut self.keyboard_handlers
        };
        let new_handler = Handler { func, id: self.next_id };
        handler_vec.push(new_handler);
        self.next_id += 1;
        handler_vec[handler_vec.len() - 1].id
    }
}

Now, we get on to send_event. This is the function that is called from Interrupt Service Routines to notify the EventHooker that an event has occurred. The EventHooker then executes all the functions associated with that event. In more concrete steps:

  1. Determine the right handler vector from the given EventInfo.
  2. Call all the functions in the handler vector, passing the given EventInfo as an argument.

In code:

impl EventHooker {
    // Used by Interrupt Service Routines to tell the EventHooker
    // that an event has occured. The `event_info` provides information
    // about the event that is passed to the handler function
    pub fn send_event(&self, event_info: EventInfo) {
        let handler_vec = match event_info {
            EventInfo::Timer => &self.timer_handlers,
            EventInfo::Keyboard(_) => &self.keyboard_handlers
        };
        for handler in handler_vec.iter() {
            (handler.func)(event_info);
        }
    }
}

For this to work, we need to implement Clone and Copy for the EventInfo structure:

#[derive(Clone, Copy)] // NEW
pub enum EventInfo {
    Timer,
    Keyboard(KeyEvent)
}

And for this, too, to work we need to implement Clone and Copy for KeyEvent.

In keyboard.rs

// DELETED: #[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub struct KeyEvent {
    pub keycode: KeyCode,
    pub direction: KeyDirection
}

And for this to work, we need to implement the same for KeyCode and KeyDirection.

// DELETED: #[derive(Debug)]
#[derive(Debug, Clone, Copy)] // NEW
pub enum KeyCode { /* ...Others */ }
// DELETED: #[derive(Debug)]
#[derive(Debug, Clone, Copy)] // NEW
pub enum KeyDirection { /* ...Others */ }

And finally, the unhook_event function. This function first has to determine which handler vector to remove the function from (from the given EventKind), then search for a handler with the given id, then remove that handler.

The function will also return a result, just to indicate whether or not it successfully found and removed a handler with the given id. This can help with debugging later if we ever find ourselves in a position where we have a problem with this function.

impl EventHooker {
    // Used by client code to tell the EventHooker to stop calling a handler with
    // with ID `id` when an event of kind `event_kind` occurs.
    // A return value of `Ok(())` means that the handler was found and removed.
    // A return value of `Err(())` means that the handler was not found.
    pub fn unhook_event(&mut self, event_kind: EventKind, id: HandlerId) -> Result<(), ()> {
        let handler_vec = match event_kind {
            EventKind::Timer => &mut self.timer_handlers,
            EventKind::Keyboard => &mut self.keyboard_handlers
        };
        for i in 0..handler_vec.len() {
            if handler_vec[i].id == id {
                handler_vec.remove(i);
                return Ok(())
            }
        }
        Err(())
    }
}

And that's it for our event handling implementation.

Take Away

For the full code, go to the repo

In The Next Post

We'll be testing the EventHooker