Demilade Sonuga's blog

All posts

Event Handling In Action

2023-05-02 · 20 min read

In this post, we're going to see the event hooker in action.

Preliminaries

But before we get to the action, we first need to do some setup. We need an instance of EventHooker with vectors and all. To instantiate the EventHooker's vectors, we need an allocator. And to instantiate an allocator, we need heap memory. The heap memory we'll use is already in efi_main:

In main.rs

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

let heap_mem = boot_services.alloc_pool(MemType::EfiLoaderData, HEAP_SIZE);
if heap_mem.is_err() {
panic!("Heap allocation failed with error status {}", heap_mem.unwrap_err());
}

boot_services.exit_boot_services(handle).unwrap();

// ...Others
}

A Global Allocator

As for the allocator, we're going to create a global instance. That is a single allocator that will serve the allocation needs of all the vectors and boxes we're going to use (because there is only one heap).

To do this, we first need to create space for the allocator.

In allocator.rs

// The global allocator to be used for allocation of memory for all
// boxes and vectors
static mut ALLOCATOR: Option<LinkedListAllocator> = None;

This statically allocated space will serve to hold the global allocator to be used for all allocation needs. To initialize it:

In allocator.rs

// Does a one-time initialization of the LinkedListAllocator
// with the heap
pub fn init(mem: MemChunk) {
// Remember that the LinkedListAllocator's head is a dummy node
let mut allocator = LinkedListAllocator { head: {
static mut head: ListNode = ListNode {
size: 0,
next: None
};
unsafe { &mut head as *mut _ }
} };
// Keeping track of the heap
unsafe { allocator.add_free_region(mem); }
if unsafe { ALLOCATOR.is_none() } {
unsafe { ALLOCATOR = Some(allocator); }
}
}

And a function to retrieve the global allocator:

In allocator.rs

// Retrieves a pointer to the allocator
pub fn get_allocator() -> *mut LinkedListAllocator {
unsafe { ALLOCATOR.as_mut().unwrap() as *mut _ }
}

To initialize the allocator in efi_main:

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

let heap_mem = boot_services.alloc_pool(MemType::EfiLoaderData, HEAP_SIZE);
if heap_mem.is_err() {
panic!("Heap allocation failed with error status {}", heap_mem.unwrap_err());
}

// NEW:
// Initializes the allocator with the heap memory
allocator::init(allocator::MemChunk {
start_addr: heap_mem.unwrap() as usize,
size: HEAP_SIZE
});

// Exiting Boot Services to gain full control over the system
boot_services.exit_boot_services(handle).unwrap();

// ...Others
}

For the above to compile, we need to make start_addr and size public:

In allocator.rs

pub struct MemChunk {
// DELETED: start_addr: usize,
pub start_addr: usize, // NEW
// DELETED: size: usize
pub size: usize // NEW
}

A Global Event Hooker

Now, to initialize the EventHooker, we create a similar single instance to do all the event handling.

In event_hook.rs

// The global event hooker, to be used for handling all events
static mut EVENT_HOOKER: Option<EventHooker> = None;

A function for initializing:

In event_hook.rs

use crate::keyboard::KeyEvent;
use crate::vec::Vec;
use crate::boxed_fn::BoxedFn;
// DELETED: use crate::allocator::Allocator;
use crate::allocator::{Allocator, get_allocator}; // NEW
// Initializes an EventHooker 
pub fn init() {
if unsafe { EVENT_HOOKER.is_none() } {
unsafe { EVENT_HOOKER = Some(EventHooker::new(get_allocator())); }
}
}

Function for sending events (notifying the EventHooker that an event has taken place):

pub fn send_event(event_info: EventInfo) {
let event_hooker = get_event_hooker().unwrap();
event_hooker.send_event(event_info);
}

Function for hooking events (telling the EventHooker to call a function when an event has taken place):

pub fn hook_event(event_kind: EventKind, func: BoxedFn) -> HandlerId {
let mut event_hooker = get_event_hooker().unwrap();
event_hooker.hook_event(event_kind, func)
}

Function for unhooking (unregistering) functions:

pub fn unhook_event(event_kind: EventKind, id: HandlerId) -> Result<(), ()> {
let mut event_hooker = get_event_hooker().unwrap();
event_hooker.unhook_event(event_kind, id)
}

Helper function for getting the EventHooker:

fn get_event_hooker() -> Option<&'static mut EventHooker> {
unsafe { EVENT_HOOKER.as_mut() }
}

And initializing it in efi_main

In main.rs

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

allocator::init(allocator::MemChunk {
start_addr: heap_mem.unwrap() as usize,
size: HEAP_SIZE
});

event_hook::init(); // NEW

boot_services.exit_boot_services(handle).unwrap();

// ...Others
}

There is still yet one more thing that needs to be done before we can start hooking events. We need to set up the interrupt service routines to send events when they're called. That is, when the timer interrupt occurs, a send_event call must be made with the EventInfo::Timer. And when the keyboard interrupt occurs, a send_event call must be made with an EventInfo holding information about the key press.

In main.rs

use event_hook::{EventInfo, EventKind};
extern "x86-interrupt" fn timer_handler(frame: interrupts::InterruptStackFrame) {
let screen = get_screen().unwrap();
// DELETED: write!(screen, "!");
// Notifying the event hooker that the timer event has occured
event_hook::send_event(EventInfo::Timer); // NEW
get_pics().unwrap().end_of_interrupt(0);
}

extern "x86-interrupt" fn keyboard_handler(frame: interrupts::InterruptStackFrame) {
let port = port::Port::new(0x60);
let scancode = port.read();
// DELETED: let screen = get_screen().unwrap();
if let Ok(Some(event)) = get_keyboard().unwrap().interpret_byte(scancode) {
// DELETED: write!(screen, "{:?}", event);
// Notifying the event hooker that the keyboard event has occured
event_hook::send_event(EventInfo::Keyboard(event)); // NEW
}
get_pics().unwrap().end_of_interrupt(1);
}

With these preliminaries in place, we can now check out the event handling in action.

In Action

Let's print something on every timer tick:

In main.rs

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

setup_keyboard();
setup_pics();

event_hook::hook_event(EventKind::Timer, boxed_fn::BoxedFn::new(|_| {
write!(screen, "Tick");
}, allocator::get_allocator()));

game::blasterball(screen);

// ...Others
}

Before running the code, you need to remember that we disabled timer interrupts in the pics.rs module.

To re-enable it:

impl PICs {
pub fn init(&mut self) {
// ...Others
// DELETED: self.first.data.write(0b11111101);
self.first.data.write(0b11111100); // NEW
self.second.data.write(0b11111111);
}
}

If you run the code now, you might get a double fault error that looks like this:

Double Fault

This is something we'll get to later. But if you keep running the code, you'll eventually get a screen like this:

Tick

But you'll also get a lot of double-fault errors before seeing this. If you didn't, keep rerunning the code until you do. You'll notice that in some runs you'll get double-fault panics, and in the rest, the code runs as expected without any modification as if the bug mysteriously vanished. This is a non-deterministic bug, meaning that you can't predict whether or not it will happen and if it does, when it will happen.

This sort of bug is a common symptom of race conditions, which is something we'll get to later. But for now, ignore the double-fault errors and let's just keep going.

Another test we can do is to print out keys pressed on a keyboard:

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

/* DELETED:
event_hook::hook_event(EventKind::Timer, boxed_fn::BoxedFn::new(|_| {
write!(screen, "Tick");
}, allocator::get_allocator()));
*/


// NEW:
event_hook::hook_event(EventKind::Keyboard, boxed_fn::BoxedFn::new(|event_info| {
if let EventInfo::Keyboard(key_event) = event_info {
if key_event.direction == keyboard::KeyDirection::Down {
write!(screen, "{:?}", key_event.keycode);
}
}
}, allocator::get_allocator()));

// ...Others
}

For this code to run without errors, we first need to implement PartialEq for KeyDirection.

In keyboard.rs

// DELETED: #[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)] // NEW
pub enum KeyDirection {
Up,
Down
}

Once again, keep running the code until you don't see a double-fault error. When you finally get the game screen, type something.

Tick

I got the above screen from typing "hello keyboard".

You can try other tests yourself see how they work out, or see if there is a bug to fix.

Take Away

For the full code, go to the repo

In The Next Post

We're going to be doing some refactoring