Using Protocols

The open a protocol, you must first get a handle, then open a protocol on that handle. See Handles and Protocols for an overview of what these terms mean.

To get a handle you can use:

Once you have obtained a handle, use BootServices::open_protocol_exclusive to open a protocol on that handle. This returns a ScopedProtocol, which automatically closes the protocol when dropped.

Using BootServices::open_protocol_exclusive is the safest way to open a protocol, but in some cases a protocol cannot be opened in exclusive mode. The unsafe BootServices::open_protocol can be used in that case.

Example

For this example we'll look at a program that opens a couple different protocols. This program opens the LoadedImage protocol to get information about an executable (the currently-running program in this case). It also opens the DevicePathToText protocol to get the file system path that the program was launched from.

We'll walk through the details of this program shortly, but first here's the whole thing:

#![no_main]
#![no_std]

use log::info;
use uefi::prelude::*;
use uefi::proto::device_path::text::{
    AllowShortcuts, DevicePathToText, DisplayOnly,
};
use uefi::proto::loaded_image::LoadedImage;
use uefi::table::boot::SearchType;
use uefi::{Identify, Result};

#[entry]
fn main(image_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
    uefi::helpers::init(&mut system_table).unwrap();
    let boot_services = system_table.boot_services();

    print_image_path(boot_services).unwrap();

    boot_services.stall(10_000_000);
    Status::SUCCESS
}

fn print_image_path(boot_services: &BootServices) -> Result {
    let loaded_image = boot_services
        .open_protocol_exclusive::<LoadedImage>(boot_services.image_handle())?;

    let device_path_to_text_handle = *boot_services
        .locate_handle_buffer(SearchType::ByProtocol(&DevicePathToText::GUID))?
        .first()
        .expect("DevicePathToText is missing");

    let device_path_to_text = boot_services
        .open_protocol_exclusive::<DevicePathToText>(
            device_path_to_text_handle,
        )?;

    let image_device_path =
        loaded_image.file_path().expect("File path is not set");
    let image_device_path_text = device_path_to_text
        .convert_device_path_to_text(
            boot_services,
            image_device_path,
            DisplayOnly(true),
            AllowShortcuts(false),
        )
        .expect("convert_device_path_to_text failed");

    info!("Image path: {}", &*image_device_path_text);
    Ok(())
}

When the program is run it will print something like this:

[ INFO]: example.rs@058: Image path: \EFI\BOOT\BOOTX64.EFI

Walkthrough

The main function looks much like the "Hello world!" example. It sets up logging, calls print_image_path, and pauses for ten seconds to give you time to read the output. Let's look at print_image_path:

#![allow(unused)]
fn main() {
fn print_image_path(boot_services: &BootServices) -> Result {
}

The return type is a uefi::Result, which is a Result alias that combines uefi::Status with the error data. Both the success and error data types are () by default.

The function starts by opening the LoadedImage protocol:

#![allow(unused)]
fn main() {
    let loaded_image = boot_services
        .open_protocol_exclusive::<LoadedImage>(boot_services.image_handle())?;
}

The open_protocol_exclusive method takes a type parameter, which is the type of Protocol you want to open (LoadedImage in this case). It also takes one regular argument of type Handle. For this example we want the handle of the currently-running image, which was passed in as the first argument to main. The handle is conveniently accessible through BootServices::image_handle, so we use that here.

Next the program opens the DevicePathToText protocol:

#![allow(unused)]
fn main() {
    let device_path_to_text_handle = *boot_services
        .locate_handle_buffer(SearchType::ByProtocol(&DevicePathToText::GUID))?
        .first()
        .expect("DevicePathToText is missing");

    let device_path_to_text = boot_services
        .open_protocol_exclusive::<DevicePathToText>(
            device_path_to_text_handle,
        )?;
}

This protocol isn't available for the image_handle, so we start by using locate_handle_buffer to find all handles that support DevicePathToText. We only need one handle though, so we call first() and discard the rest. Then we call open_protocol_exclusive again. It looks more or less like the previous time, but with DevicePathToText as the type parameter and device_path_to_text_handle as the handle.

Now that we have both protocols open, we can use them together to get the program's path and convert it to text:

#![allow(unused)]
fn main() {
    let image_device_path =
        loaded_image.file_path().expect("File path is not set");
    let image_device_path_text = device_path_to_text
        .convert_device_path_to_text(
            boot_services,
            image_device_path,
            DisplayOnly(true),
            AllowShortcuts(false),
        )
        .expect("convert_device_path_to_text failed");

    info!("Image path: {}", &*image_device_path_text);
    Ok(())
}
}

Since protocols do a wide range of different things, the methods available to call are very specific to each individual protocol. The best places to find out what each protocol can do are the uefi-rs reference documentation and the UEFI Specification.