Demilade Sonuga's blog

All posts

Refactoring III

2022-12-06

We've done a lot of refactoring but there's still more we need to do.

Firstly:

fn pre_graphics_print_char(simple_text_output: *mut SimpleTextOutput, c: char) {
    // The UTF-16 encoded string which contains the single character to be printed
    let mut utf16_encoded_buffer = [c as u16, 0];
    unsafe {
        ((*simple_text_output).output_string)(simple_text_output, utf16_encoded_buffer.as_mut_ptr())
    };
}

This unsafety is avoidable and that last line in the function is really unreadable, that is, the intent of that line is not immediately clear. To rectify the unsafety:

// DELETED: fn pre_graphics_print_char(simple_text_output: *mut SimpleTextOutput, c: char) {
fn pre_graphics_print_char(simple_text_output: &SimpleTextOutput, c: char) {
    // The UTF-16 encoded string which contains the single character to be printed
    let mut utf16_encoded_buffer = [c as u16, 0];
    /* DELETED:
    unsafe {
        ((*simple_text_output).output_string)(simple_text_output, utf16_encoded_buffer.as_mut_ptr())
    };
    */
    // NEW:
    (simple_text_output.output_string)(simple_text_output, utf16_encoded_buffer.as_mut_ptr());
}

To make our code valid again:

// DELETED: fn pre_graphics_print_str(simple_text_output: *mut SimpleTextOutput, s: &str) {
fn pre_graphics_print_str(simple_text_output: &SimpleTextOutput, s: &str) {
    for c in s.chars() {
        pre_graphics_print_char(simple_text_output, c);
    }
}

In our efi_main, every instance where this pre_graphics_print_str is called, a raw pointer is passed instead of a reference:

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

    if locate_gop_status != STATUS_SUCCESS {
        let simple_text_output = sys_table.simple_text_output;
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }

    // ... Others

    for mode_number in 0..max_mode {
        // ... Others

        if query_status != STATUS_SUCCESS {
            let simple_text_output = sys_table.simple_text_output;
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }

        // ... Others

        if mode_number == max_mode - 1 {
            let simple_text_output = sys_table.simple_text_output;
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }
    }

    // ... Others

    if set_mode_status != STATUS_SUCCESS {
        let simple_text_output = sys_table.simple_text_output;
        pre_graphics_print_str(simple_text_output, "Failed to set the desired mode\n");
        loop {}
    }

    // ... Others

}

The sys_table.simple_text_output results in a raw pointer to SimpleTextOutput. To get a reference instead of a raw pointer, we just do what we did for the GraphicsOutput's mode and the SystemTable's boot_services. Do the unsafe dereferencing in a single area within a safe function, then take a reference to the underlying data and return that reference:

#[repr(C)]
struct SystemTable {
    unneeded: [u8; 60],
    simple_text_output: *mut SimpleTextOutput,
    unneeded2: [u8; 24],
    boot_services: *const BootServices,
}

impl SystemTable {
    fn boot_services(&self) -> &BootServices {
        unsafe { &*self.boot_services }
    }

    // NEW:
    // Returns a reference to the Simple Text Output instance in the System Table
    fn simple_text_output(&self) -> &SimpleTextOutput {
        unsafe { &*self.simple_text_output }
    }
}

We now refactor to use this function:

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

    if locate_gop_status != STATUS_SUCCESS {
        // DELETED: let simple_text_output = sys_table.simple_text_output;
        let simple_text_output = sys_table.simple_text_output(); // NEW
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }

    // ... Others

    for mode_number in 0..max_mode {
        // ... Others

        if query_status != STATUS_SUCCESS {
            // DELETED: let simple_text_output = sys_table.simple_text_output;
            let simple_text_output = sys_table.simple_text_output(); // NEW
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }

        // ... Others

        if mode_number == max_mode - 1 {
            // DELETED: let simple_text_output = sys_table.simple_text_output;
            let simple_text_output = sys_table.simple_text_output(); // NEW
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }
    }

    // ... Others

    if set_mode_status != STATUS_SUCCESS {
        // DELETED: let simple_text_output = sys_table.simple_text_output;
        let simple_text_output = sys_table.simple_text_output(); // NEW
        pre_graphics_print_str(simple_text_output, "Failed to set the desired mode\n");
        loop {}
    }

    // ... Others

}

We repeat the same for the printdigit and printint functions:

// DELETED: fn printdigit(n: u32, simple_text_output: *mut SimpleTextOutput) {
fn printdigit(n: u32, simple_text_output: &SimpleTextOutput) { // NEW
    // The code for the digit that will be printed
    let mut digit_u16 = [48 + n as u16, 0];
    // Printing the digit with the Simple Text Output protocol's output_string function
    /* DELETED:
    unsafe {
        (simple_text_output.output_string)(simple_text_output, digit_u16.as_mut_ptr());
    }
    */
    (simple_text_output.output_string)(simple_text_output, digit_u16.as_mut_ptr()); // NEW
}

// DELETED: fn printint(n: u32, simple_text_output: *mut SimpleTextOutput) {
fn printint(n: u32, simple_text_output: &SimpleTextOutput) { // NEW
    if n >= 10 {
        let quotient = n / 10;
        let remainder = n % 10;
        printint(quotient, simple_text_output);
        printdigit(remainder, simple_text_output);
    } else {
        printdigit(n, simple_text_output);
    }
}

For our code to compile again:

#[repr(C)]
struct SimpleTextOutput {
    unneeded: [u8; 8],
    // DELETED: output_string: extern "efiapi" fn(this: *mut SimpleTextOutput, *mut u16),
    output_string: extern "efiapi" fn(this: &SimpleTextOutput, *mut u16) // NEW
}

We're almost done (for now) minimizing unsafe code. The remaining avoidable unsafety:

fn print_char(
    screen: *mut Screen,
    font_description: &[[bool; 16]; 16],
    curr_screen_pos: (usize, usize),
) {
    for i in 0..16 {
        for j in 0..16 {
            if font_description[i][j] {
                unsafe {
                    (*screen).pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                        red: 255,
                        green: 255,
                        blue: 0,
                        reserved: 0,
                    };
                }
            } else {
                unsafe {
                    (*screen).pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                        red: 0,
                        green: 0,
                        blue: 0,
                        reserved: 0,
                    };
                }
            }
        }
    }
}

Rather than accepting a raw pointer to a screen, this function should accept a mutable reference:

fn print_char(
    // DELETED: screen: *mut Screen,
    screen: &mut Screen, // NEW
    font_description: &[[bool; 16]; 16],
    curr_screen_pos: (usize, usize),
) {
    for i in 0..16 {
        for j in 0..16 {
            if font_description[i][j] {
                /* DELETED:
                unsafe {
                    (*screen).pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                        red: 255,
                        green: 255,
                        blue: 0,
                        reserved: 0,
                    };
                }
                */
                screen.pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                    red: 255,
                    green: 255,
                    blue: 0,
                    reserved: 0,
                };
            } else {
                /* DELETED:
                unsafe {
                    (*screen).pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                        red: 0,
                        green: 0,
                        blue: 0,
                        reserved: 0,
                    };
                }
                */
                screen.pixels[curr_screen_pos.0 + i][curr_screen_pos.1 + j] = Pixel {
                    red: 0,
                    green: 0,
                    blue: 0,
                    reserved: 0,
                };
            }
        }
    }
}

The reference has to be mutable because we're mutating (modifying, changing, writing to) the screen. Refactoring to use this modified print_char:

// DELETED: fn print_str(screen: *mut Screen, s: &str) {
fn print_str(screen: &mut Screen, s: &str) { // NEW
    let mut screen_pos = (0, 0);
    for c in s.as_bytes() {
        print_char(screen, &FONT[char_to_font_index(*c)], screen_pos);
        screen_pos.1 += 16;
        if screen_pos.1 >= NO_OF_PIXELS_IN_A_ROW {
            screen_pos.0 += 16;
            screen_pos.1 = 0;
        }
        if screen_pos.0 >= NO_OF_PIXELS_IN_A_COLUMN {
            break;
        }
    }
}

Now efi_main:

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

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    // NEW:
    // Obtain a mutable reference from the screen's raw pointer
    let screen = unsafe { &mut *screen };

    print_str(screen, "Hello World!");

    0
}

At this point, unsafety in our code has been minimized to a reasonable extent. We can still do better, but we have other things to get on to.

Now that we're done with unsafety, let's look at clarity.

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

    let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
    let guid_ptr = &GOP_GUID as *const Guid;
    let registration = core::ptr::null_mut();
    let gop_ptr = &mut gop as *mut _;
    let locate_gop_status = (boot_services.locate_protocol)(guid_ptr, registration, gop_ptr);

    if locate_gop_status != STATUS_SUCCESS {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }

    let gop = gop as *mut GraphicsOutput;
    let gop = unsafe { &*gop };

    // ... Others
}

The above lines of code simply retrieve the Graphics Output Protocol with the Boot Services but the intent is not clear at all when we first look at it. The meaning of these lines has been clustered by the unnecessarily long setup. The aim of these lines of code is simple:

  1. Attempt to get the GOP with the Boot Services.
  2. If the attempt is unsuccessful, print a message and halt (loop forever).
  3. If the attempt is successful, continue.

To resolve this, we need to move all the details somewhere else and leave in its place something that makes it clear what exactly we're doing here. To get a clearer picture of what I'm talking about, rather than the above code, we need something like this:

// ... Before

attempt = locate_gop()
if attempt failed
    print "Failed to locate GOP\n"

// ... After

The question we have to answer now: move the details to where?

Taking a good look at our code, we see that the BootServices already has a locate_protocol function which we're already using. We just need another function that will encapsulate all these details to make locating protocols clearer.

#[repr(C)]
struct BootServices {
    unneeded: [u8; 320],
    locate_protocol: extern "efiapi" fn(
        protocol: *const Guid,
        registration: *const core::ffi::c_void,
        interface: *mut *mut core::ffi::c_void,
    ) -> Status,
}

// NEW:
impl BootServices {
    // Finds a protocol with it's unique GUID
    fn locate_protocol(&self, protocol_guid: &Guid) -> Result<*mut core::ffi::c_void, usize> {
        // The location which will hold a pointer to a protocol on a successful call to locate_protocol
        let mut protocol: *mut core::ffi::c_void = core::ptr::null_mut();
        // The raw pointer to the protocol Guid
        let guid_ptr = protocol_guid as *const Guid;
        // An optional argument which we're just going to pass null into
        let registration = core::ptr::null_mut();
        // Location where the protocol pointer should be placed into on a successful locate_protocol invocation
        let protocol_ptr = &mut protocol as *mut _;
        // Invoking the Boot Services locate_protocol function to find the protocol
        let locate_protocol_status = (self.locate_protocol)(guid_ptr, registration, protocol_ptr);

        if locate_protocol_status != STATUS_SUCCESS {
            // If the attempt failed, return the failed error code
            Err(locate_protocol_status)
        } else {
            // If the attempt didn't fail, return the pointer to the protocol
            Ok(protocol)
        }
    }
}

The new locate_protocol function above wraps the more primitive one, taking care of a lot of details we don't need to know about when we're calling it, accepting only the protocol GUID as a parameter. The function returns a result indicating the possibility of an error. If the function is successful, it returns a pointer to the protocol with Result's success variant (Ok) and if the function fails, the failure status code is returned with the Result's error variant (Err).

Refactoring our efi_main with this function:

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

    /* DELETED:
    let mut gop: *mut core::ffi::c_void = core::ptr::null_mut();
    let guid_ptr = &GOP_GUID as *const Guid;
    let registration = core::ptr::null_mut();
    let gop_ptr = &mut gop as *mut _;
    let locate_gop_status = (boot_services.locate_protocol)(guid_ptr, registration, gop_ptr);

    if locate_gop_status != STATUS_SUCCESS {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }
    */
    // NEW:
    // Locate the Graphics Output Protocol
    let gop = boot_services.locate_protocol(&GOP_GUID);
    // If location failed, print error and halt
    if gop.is_err() {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }

    let gop = gop.unwrap() as *mut GraphicsOutput;
    let gop = unsafe { &*gop };

    // ... Others
}

The code now is so much clearer. What we intend on doing is immediately apparent from just looking at the code. Locate GOP. If the location failed, halt.

We can make this even better by creating a locate_gop function, just for the convenience of it:

impl BootServices {
    fn locate_protocol(&self, protocol_guid: &Guid) -> Result<*mut core::ffi::c_void, Status> {
        /* OTHERS */
    }

    // NEW:
    // Convenience function to locate the Graphics Output Protocol
    fn locate_gop(&self) -> Result<&GraphicsOutput, Status> {
        // Attempt to locate the GOP
        let locate_gop = self.locate_protocol(&GOP_GUID);
        if locate_gop.is_ok() {
            // Return a reference to the GOP, instead of a raw pointer
            let gop_ptr = locate_gop.unwrap() as *mut GraphicsOutput;
            let gop = unsafe { &*gop_ptr };
            Ok(gop)
        } else {
            // Return the error code it failed with
            Err(locate_gop.unwrap_err())
        }
    }
}

Refactoring again:

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    // ... Others
    
    // DELETED: let gop = boot_services.locate_protocol(&GOP_GUID);
    let gop = boot_services.locate_gop(); // NEW
    
    if gop.is_err() {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }
    
    /* DELETED:
    let gop = gop.unwrap() as *mut GraphicsOutput;
    let gop = unsafe { &*gop };
    */

    // Extract the GOP from the result
    let gop = gop.unwrap(); // NEW

    // ... Others
}

With our new locate_gop function, we've been able to reduce the number of details we needed to remember to locate the GOP. The unsafe that we needed to dereference the GOP pointer is now out of our efi_main and in the small, manually verified, safe locate_gop function, making our code cleaner, easier to reason with, easier to modify and harder to screw up.

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

    for mode_number in 0..max_mode {
        let size_of_info = core::mem::size_of::<GraphicsModeInfo>();
        let mut mode: *const GraphicsModeInfo = core::ptr::null_mut();
        let query_mode = gop.query_mode;
        let query_status = (query_mode)(
            gop,
            mode_number,
            &size_of_info as *const _,
            &mut mode as *mut _,
        );
        if query_status != STATUS_SUCCESS {
            let simple_text_output = sys_table.simple_text_output();
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }

        let mode = unsafe { &*mode };

        // ... Others
    }


    // ... Others
}

Our query_mode is suffering from the same detail overload our GOP location was suffering from a few moments ago. The only information we actually need to remember whenever we want to call query_mode is the mode number. Everything else is a detail that can be hidden.

impl GraphicsOutput {
    fn mode(&self) -> &GraphicsMode {
        unsafe { &*self.mode }
    }

    // NEW:
    // Gets information about a mode
    fn query_mode(&self, mode_number: u32) -> Result<&GraphicsModeInfo, Status> {
        // The size of our `GraphicsModeInfo` structure
        let size_of_info = core::mem::size_of::<GraphicsModeInfo>();
        // The location that will hold the pointer to the `GraphicsModeInfo`
        // for the current `mode_number` on a successful call to the
        // `GraphicsOutput` query_mode`
        let mut mode: *const GraphicsModeInfo = core::ptr::null_mut();
        // Calling `query_mode` to get information about the mode associated
        // with `mode_number`
        let query_status = (self.query_mode)(
            // The pointer to the GOP instance
            self,
            // The mode number associated with the mode we want information about
            mode_number,
            // The size of the `GraphicsModeInfo` structure
            &size_of_info as *const _,
            // The pointer to the location to be mutated to hold the pointer to the
            // `GraphicsModeInfo` instance associated with the current `mode_number`
            // on a successful function execution
            &mut mode as *mut _,
        );
        if query_status == STATUS_SUCCESS {
            // Return a reference to the mode info
            Ok(unsafe { &*mode })
        } else {
            // Return the failure status
            Err(query_status)
        }
    }
}

Now, we refactor with this function:

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

    for mode_number in 0..max_mode {
        /* DELETED:
        let size_of_info = core::mem::size_of::<GraphicsModeInfo>();
        let mut mode: *const GraphicsModeInfo = core::ptr::null_mut();
        let query_mode = gop.query_mode;
        let query_status = (query_mode)(
            gop,
            mode_number,
            &size_of_info as *const _,
            &mut mode as *mut _,
        );
        if query_status != STATUS_SUCCESS {
            let simple_text_output = sys_table.simple_text_output();
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }

        let mode = unsafe { &*mode };
        */

        // NEW:
        // Get the mode info
        let mode = gop.query_mode(mode_number);
        if mode.is_err() {
            let simple_text_output = sys_table.simple_text_output();
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }
        // Extract the mode info from the result
        let mode = mode.unwrap();

        // ... Others
    }


    // ... Others
}

Similarly, we have:

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

    let set_mode_status = (gop.set_mode)(gop, desired_mode);

    if set_mode_status != STATUS_SUCCESS {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to set the desired mode\n");
        loop {}
    }

    // ... Others
}

This will be much better as:

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

    // DELETED: let set_mode_status = (gop.set_mode)(gop, desired_mode);
    let set_mode_result = gop.set_mode(desired_mode); // NEW

    // DELETED: if set_mode_status != STATUS_SUCCESS {
    if set_mode_result.is_err() {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to set the desired mode\n");
        loop {}
    }

    // ... Others
}

We just add another function in our GraphicsOutput implementation block:

impl GraphicsOutput {
    fn mode(&self) -> &GraphicsMode {
        unsafe { &*self.mode }
    }

    fn query_mode(&self, mode_number: u32) -> Result<&GraphicsModeInfo, Status> {
        /* OTHERS */
    }

    // Set a mode to the mode with the mode number desired_mode
    fn set_mode(&self, desired_mode: u32) -> Result<(), Status> {
        let set_mode_status = (self.set_mode)(self, desired_mode);
        if set_mode_status == STATUS_SUCCESS {
            Ok(())
        } else {
            Err(set_mode_status)
        }
    }
}

And again, our code is cleaner, more concise and safer. We can still do better, but we have lots of other things to do.

To finalize our refactor (for now), let's a look at our main.rs. It's clustered. We need to separate code with different concerns into different modules. To do this, we first need to identify the functionality that the code in our main.rs has.

All the code in the efi_main is the driver code, the code that actually gets executed when the app starts and runs to the end. Everything else in the main.rs is just utilities we use to interact with the UEFI firmware.

From this, it's clear that we have to separate out the UEFI interfacing utilities into a separate module and leave the driver code in our main.rs.

Create a new file, uefi.rs. Your directory should be looking like this now:

blasterball/
| .cargo/
| | config.toml
| src/
| | font.rs
| | main.rs
| | uefi.rs
| .gitignore
| Cargo.lock
| Cargo.toml

Cut out all the structures and functions and constants, apart from the efi_main and panic_handler, and paste them into uefi.rs.

Now, we have to change the visibility of the structs and functions and constants:

// DELETED: const STATUS_SUCCESS: Status = 0;
pub const STATUS_SUCCESS: Status = 0; // NEW

// DELETED: const GOP_GUID: Guid = Guid { /* OTHERS */ };
pub const GOP_GUID: Guid = Guid { /* OTHERS */ };

/* DELETED:
fn pre_graphics_print_char(simple_text_output: &SimpleTextOutput, c: char) {
    /* OTHERS */
}
*/
// NEW:
pub fn pre_graphics_print_char(simple_text_output: &SimpleTextOutput, c: char) {
    /* OTHERS */
}

/* DELETED:
fn pre_graphics_print_str(simple_text_output: &SimpleTextOutput, s: &str) {
    /* OTHERS */
}
*/
// NEW:
pub fn pre_graphics_print_str(simple_text_output: &SimpleTextOutput, s: &str) {
    /* OTHERS */
}

/* DELETED:
fn print_str(screen: &mut Screen, s: &str) {
    /* OTHERS */
}
*/
// NEW:
pub fn print_str(screen: &mut Screen, s: &str) {
    /* OTHERS */
}

/* DELETED:
fn print_char(
    screen: &mut Screen,
    font_description: &[[bool; 16]; 16],
    curr_screen_pos: (usize, usize),
) {
    /* OTHERS */
}
*/
// NEW:
pub fn print_char(
    screen: &mut Screen,
    font_description: &[[bool; 16]; 16],
    curr_screen_pos: (usize, usize),
) {
    /* OTHERS */
}

// ... Others

// DELETED: struct Screen { /* OTHERS */ }

pub struct Screen { /* OTHERS */ }

/* DELETED:
fn printdigit(n: u32, simple_text_output: &SimpleTextOutput) {
    /* OTHERS */
}
*/
// NEW:
pub fn printdigit(n: u32, simple_text_output: &SimpleTextOutput) {
    /* OTHERS */
}

// Prints an integer n
/* DELETED:
fn printint(n: u32, simple_text_output: &SimpleTextOutput) {
    /* OTHERS */
}
*/
// NEW:
pub fn printint(n: u32, simple_text_output: &SimpleTextOutput) {
    /* OTHERS */
}

// DELETED: struct SystemTable { /* OTHERS */ }
// NEW:
pub struct SystemTable { /* OTHERS */ }

impl SystemTable {
    /* DELETED:
    fn boot_services(&self) -> &BootServices {
        /* OTHERS */
    }
    */
    pub fn boot_services(&self) -> &BootServices {
        /* OTHERS */
    }

    /* DELETED:
    fn simple_text_output(&self) -> &SimpleTextOutput {
        /* OTHERS */
    }
    */
    pub fn simple_text_output(&self) -> &SimpleTextOutput {
        /* OTHERS */
    }
}

// DELETED: struct Guid { /* OTHERS */ }
pub struct Guid { /* OTHERS */ }

// DELETED: type Status = usize;
pub type Status = usize; // NEW

// DELETED: struct BootServices { /* OTHERS */ }
pub struct BootServices { /* OTHERS */ } // NEW

impl BootServices {
    /* DELETED:
    fn locate_protocol(&self, protocol_guid: &Guid) -> Result<*mut core::ffi::c_void, usize> {
        /* OTHERS */
    }
    */
    // NEW:
    pub fn locate_protocol(&self, protocol_guid: &Guid) -> Result<*mut core::ffi::c_void, usize> {
        /* OTHERS */
    }
    
    /* DELETED:
    fn locate_gop(&self) -> Result<&GraphicsOutput, Status> {
        /* OTHERS */
    }
    */
    // NEW:
    pub fn locate_gop(&self) -> Result<&GraphicsOutput, Status> {
        /* OTHERS */
    }
}

// DELETED: struct SimpleTextOutput { /* OTHERS */ }
pub struct SimpleTextOutput { /* OTHERS */ } // NEW

// DELETED: struct GraphicsOutput { /* OTHERS */ }
pub struct GraphicsOutput { /* OTHERS */ } // NEW

impl GraphicsOutput {
    /* DELETED:
    fn mode(&self) -> &GraphicsMode {
        /* OTHERS */
    }
    */
    // NEW:
    pub fn mode(&self) -> &GraphicsMode {
        /* OTHERS */
    }

    /* DELETED:
    fn query_mode(&self, mode_number: u32) -> Result<&GraphicsModeInfo, Status> {
        /* OTHERS */
    }
    */
    // NEW:
    pub fn query_mode(&self, mode_number: u32) -> Result<&GraphicsModeInfo, Status> {
        /* OTHERS */
    }
    /* DELETED:
    fn set_mode(&self, desired_mode: u32) -> Result<(), Status> {
        /* OTHERS */
    }
    */
    // NEW:
    pub fn set_mode(&self, desired_mode: u32) -> Result<(), Status> {
        /* OTHERS */
    }
}

/* DELETED:
struct GraphicsModeInfo {
    version: u32,
    horizontal_resolution: u32,
    vertical_resolution: u32,
    pixel_format: PixelFormat,
    pixel_info: PixelBitmask,
    pixels_per_scan_line: u32,
}
*/
// NEW:
pub struct GraphicsModeInfo {
    pub version: u32,
    pub horizontal_resolution: u32,
    pub vertical_resolution: u32,
    pub pixel_format: PixelFormat,
    pub pixel_info: PixelBitmask,
    pub pixels_per_scan_line: u32,
}

// DELETED: enum PixelFormat { /* OTHERS */ }
pub enum PixelFormat { /* OTHERS */ } // NEW

/* DELETED:
struct Pixel {
    blue: u8,
    green: u8,
    red: u8,
    reserved: u8,
}
*/
// NEW:
pub struct Pixel {
    pub blue: u8,
    pub green: u8,
    pub red: u8,
    pub reserved: u8,
}

/* DELETED:
struct PixelBitmask {
    red_mask: u32,
    green_mask: u32,
    blue_mask: u32,
    reserved: u32,
}
*/
// NEW:
pub struct PixelBitmask {
    pub red_mask: u32,
    pub green_mask: u32,
    pub blue_mask: u32,
    pub reserved: u32,
}

// DELETED: type PhysAddr = u64;
pub type PhysAddr = u64; // NEW

/* DELETED:
struct GraphicsMode {
    max_mode: u32,
    mode: u32,
    info: *const GraphicsModeInfo,
    size_of_info: usize,
    framebuffer_base: PhysAddr,
    framebuffer_size: usize,
}
*/
// NEW:
pub struct GraphicsMode {
    pub max_mode: u32,
    pub mode: u32,
    pub info: *const GraphicsModeInfo,
    pub size_of_info: usize,
    pub framebuffer_base: PhysAddr,
    pub framebuffer_size: usize,
}

The print_str function uses the FONT array, so we have to import it in uefi.rs. At the top of the file:

use crate::FONT;

For our code to compile again, we have to import the stuff we used in main.rs.

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

mod font;
use font::FONT;

// NEW:
mod uefi;
use uefi::{SystemTable, Screen, PixelFormat, pre_graphics_print_str, print_str};

And we're done (for now).

Take Away

Code till now:

Directory view:

blasterball/
| .cargo/
| | config.toml
| src/
| | font.rs
| | main.rs
| | uefi.rs
| .gitignore
| Cargo.lock
| Cargo.toml

main.rs contents

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

mod font;
use font::FONT;

mod uefi;
use uefi::{SystemTable, Screen, PixelFormat, pre_graphics_print_str, print_str};

#[no_mangle]
extern "efiapi" fn efi_main(
    handle: *const core::ffi::c_void,
    sys_table: *mut SystemTable,
) -> usize {
    let sys_table = unsafe { &*sys_table };
    let boot_services = sys_table.boot_services();
    let gop = boot_services.locate_gop();
    if gop.is_err() {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
        loop {}
    }
    let gop = gop.unwrap();
    let mode = gop.mode();
    let max_mode = mode.max_mode;
    let mut desired_mode = 0;
    for mode_number in 0..max_mode {
        let mode = gop.query_mode(mode_number);
        if mode.is_err() {
            let simple_text_output = sys_table.simple_text_output();
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }
        let mode = mode.unwrap();

        let horizontal_resolution = mode.horizontal_resolution;
        let vertical_resolution = mode.vertical_resolution;
        let pixel_format = mode.pixel_format;

        if horizontal_resolution == 640
            && vertical_resolution == 480
            && pixel_format == PixelFormat::BlueGreenRedReserved
        {
            desired_mode = mode_number;
            break;
        }
        if mode_number == max_mode - 1 {
            let simple_text_output = sys_table.simple_text_output();
            pre_graphics_print_str(simple_text_output, "Failed to locate GOP\n");
            loop {}
        }
    }

    let set_mode_result = gop.set_mode(desired_mode);

    if set_mode_result.is_err() {
        let simple_text_output = sys_table.simple_text_output();
        pre_graphics_print_str(simple_text_output, "Failed to set the desired mode\n");
        loop {}
    }

    let framebuffer_base = mode.framebuffer_base;

    let screen = framebuffer_base as *mut Screen;
    
    let screen = unsafe { &mut *screen };

    print_str(screen, "Hello World!");

    0
}

#[panic_handler]
fn panic_handler(panic_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

For the full code, go to the repo

In the Next Post

We'll learn a few things about bitmap images