Introduction

Previous discussions established that a Protocol's essence consists of a C language structure containing a series of function pointers. For example, the Simple Text Output Protocol appears as follows:

typedef struct {
    EFI_TEXT_RESET Reset;
    EFI_TEXT_OUTPUT_STRING OutputString;
    EFI_TEXT_TEST_STRING TestString;
    EFI_TEXT_QUERY_MODE QueryMode;
    // ... additional function pointers
    EFI_SIMPLE_TEXT_OUTPUT_MODE *Mode;  // Data pointer to protocol state information
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

This article demonstrates how to call functions defined within Protocols from UEFI applications, providing practical guidance for firmware developers.

Example Program: Graphics Output Protocol

We'll use the Graphics Output Protocol (GOP) as our working example, writing code that demonstrates how applications locate and utilize specific protocols.

Understanding Handles and Protocols

A Handle represents an instance of a particular object. Protocols typically attach to corresponding Handles, indicating the capabilities that Handle possesses. For instance, a hard drive Handle requires block read/write functionality, which corresponds to the Block IO Protocol.

This relationship between Handles and Protocols forms the foundation of UEFI's extensible architecture.

Program Logic Overview

Our example code follows this logical flow:

  1. Locate Handles: Find handles supporting the specified protocol using the Protocol's GUID. Note that multiple handles may support the same protocol.
  2. Obtain Protocol Interface: After acquiring handles, retrieve the specific protocol interface to call its functions.

This two-step process—location followed by interface acquisition—represents the standard pattern for protocol access in UEFI applications.

Key Boot Service Functions

This article introduces two critical Boot Service functions used throughout the example:

LocateHandleBuffer and LocateHandle

Both functions search for handles within the UEFI system, differing primarily in memory management approach.

Function Signatures

typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE)(
    IN EFI_LOCATE_SEARCH_TYPE SearchType,    // Search method
    IN EFI_GUID *Protocol OPTIONAL,          // Protocol GUID to locate
    IN VOID *SearchKey OPTIONAL,             // Search key (related to SearchType)
    IN OUT UINTN *BufferSize,                // Input: buffer size, Output: required size
    OUT EFI_HANDLE *Buffer                   // Output buffer
);

typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_HANDLE_BUFFER)(
    IN EFI_LOCATE_SEARCH_TYPE SearchType,
    IN EFI_GUID *Protocol OPTIONAL,
    IN VOID *SearchKey OPTIONAL,
    OUT UINTN *NoHandles,                    // Output: number of handles found
    OUT EFI_HANDLE **Buffer                  // Output: allocated buffer pointer
);

LocateHandle Usage Pattern

LocateHandle requires the caller to allocate the buffer. The usage flow proceeds as follows:

First Call: Pass a small BufferSize. The function returns EFI_BUFFER_TOO_SMALL and populates the required size.

UINTN BufferSize = 0;
EFI_HANDLE *Buffer;

// First call to get required size
LocateHandle(ByProtocol, &gEfiDriverBindingProtocolGuid,
             NULL, &BufferSize, NULL);

// Allocate memory based on returned size
Buffer = AllocatePool(BufferSize);

// Second call to get actual handles
LocateHandle(ByProtocol, &gEfiDriverBindingProtocolGuid,
             NULL, &BufferSize, Buffer);

This two-call pattern ensures proper buffer allocation without wasting memory.

LocateHandleBuffer Usage Pattern

LocateHandleBuffer internally allocates the buffer automatically:

UINTN NoHandles;
EFI_HANDLE *Buffer;

// Single call completes the operation
LocateHandleBuffer(ByProtocol, &gEfiDriverBindingProtocolGuid,
                   NULL, &NoHandles, &Buffer);

// Use Buffer...

FreePool(Buffer);  // Don't forget to release!

This simplified interface reduces code complexity but requires careful memory management to prevent leaks.

OpenProtocol Function

OpenProtocol obtains protocol interface functions, opening an installed protocol to gain access to its function pointers or data structures.

Function Signature

typedef
EFI_STATUS
(EFIAPI *EFI_OPEN_PROTOCOL)(
    IN EFI_HANDLE Handle,              // Handle where protocol resides
    IN EFI_GUID *Protocol,             // Protocol GUID
    OUT VOID **Interface OPTIONAL,     // Output: protocol interface pointer
    IN EFI_HANDLE AgentHandle,         // Requester handle (driver image handle)
    IN EFI_HANDLE ControllerHandle,    // Controller handle
    IN UINT32 Attributes               // Open mode
);

Attribute Modes

The Attributes parameter controls how the protocol opens:

ModeMeaningTypical Scenario
EFI_OPEN_PROTOCOL_BY_DRIVERDriver exclusive controlWhen driver begins managing device
`EFI_OPEN_PROTOCOL_BY_DRIVER \EFI_OPEN_PROTOCOL_EXCLUSIVE`Forced exclusiveExclusive access required
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLERChild controller openBus driver opens for child device
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOLSimple acquisition (no relationship establishment)Temporary information reading
EFI_OPEN_PROTOCOL_GET_PROTOCOLGet only (no registration to protocol database)Internal queries

Simple Usage Example

EFI_STATUS Status;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;

// Open Console Output Protocol from system table handle
Status = gBS->OpenProtocol(
    gST->ConsoleOutHandle,              // Handle
    &gEfiSimpleTextOutProtocolGuid,     // Protocol GUID
    (VOID **)&ConOut,                   // Output interface
    gImageHandle,                       // This driver handle
    NULL,                               // No controller
    EFI_OPEN_PROTOCOL_GET_PROTOCOL      // Simple acquisition
);

Relationship Between OpenProtocol, HandleProtocol, and LocateProtocol

Understanding these three related functions prevents confusion:

  • HandleProtocol: A simplified wrapper around OpenProtocol, using EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL mode. Suitable for straightforward protocol access.
  • LocateProtocol: Combines two steps—first locating the handle, then opening the protocol. It directly finds the first handle supporting the specified Protocol in the system and opens it. This convenience comes with less control over the process.
  • OpenProtocol: The lowest-level function, enabling precise control over driver-device relationships (who opened whom). Essential for driver start/stop management where relationship tracking matters.

Protocol Reference Counting Mechanism

OpenProtocol implements an important mechanism: protocol reference counting. Each successful OpenProtocol call increments the protocol's reference count. A paired CloseProtocol call must decrement this count. Only when the count reaches zero can the protocol unload.

This mechanism prevents premature protocol卸载 while drivers still depend on it, ensuring system stability.

Complete Example Code

The following complete example demonstrates locating and opening the Graphics Output Protocol:

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>

EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_STATUS Status;
    UINTN NoHandles;
    EFI_HANDLE *Buffer;
    EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput;

    // Locate all handles that support the Graphics Output Protocol
    Status = gBS->LocateHandleBuffer(
        ByProtocol,
        &gEfiGraphicsOutputProtocolGuid,
        NULL,
        &NoHandles,
        &Buffer
    );

    Print(L"Status of LocateHandleBuffer: %r\n", Status);

    if (EFI_ERROR(Status)) {
        Print(L"Failed to locate handles for Graphics Output Protocol: %r\n", Status);
        return Status;
    }

    Print(L"Hello, Protocol!\n");
    Print(L"Number of handles that support Graphics Output Protocol: %d\n", NoHandles);

    // Open the Graphics Output Protocol on the first handle found
    Status = gBS->OpenProtocol(
        Buffer[0],
        &gEfiGraphicsOutputProtocolGuid,
        (VOID**)&GraphicsOutput,
        ImageHandle,
        NULL,
        EFI_OPEN_PROTOCOL_GET_PROTOCOL
    );

    Print(L"Status of OpenProtocol: %r\n", Status);

    if (EFI_ERROR(Status)) {
        Print(L"Failed to open Graphics Output Protocol: %r\n", Status);
        return Status;
    }

    // At this point, GraphicsOutput pointer can be used to call protocol functions
    // For example: GraphicsOutput->QueryMode(), GraphicsOutput->SetMode(), etc.

    // Remember to close the protocol when done
    gBS->CloseProtocol(
        &gEfiGraphicsOutputProtocolGuid,
        Buffer[0],
        ImageHandle,
        NULL
    );

    // Free the handle buffer
    FreePool(Buffer);

    return EFI_SUCCESS;
}

Code Analysis

This example demonstrates several important patterns:

Error Handling: Each operation checks the returned Status and handles errors appropriately. This defensive programming style proves essential in firmware development where failures can have serious consequences.

Resource Management: The code properly frees allocated memory (FreePool(Buffer)) and closes opened protocols (CloseProtocol). Neglecting these cleanup steps leads to memory leaks and resource exhaustion.

Protocol Usage: After successfully opening the protocol, the GraphicsOutput pointer enables calling all protocol-defined functions such as QueryMode() and SetMode().

Common Protocols in UEFI Development

Essential Protocols

UEFI defines numerous protocols for various system functions. Commonly encountered protocols include:

  • Simple Text Output Protocol: Console text display
  • Simple Text Input Protocol: Keyboard input handling
  • Graphics Output Protocol: Video mode and framebuffer access
  • Block I/O Protocol: Block device read/write operations
  • Simple File System Protocol: File system access
  • Loaded Image Protocol: Information about loaded images
  • Device Path Protocol: Device path representation

Protocol Discovery Strategy

When developing UEFI applications, understanding which protocols your system supports proves valuable. The enumeration pattern demonstrated in our example applies to any protocol:

// Generic pattern for protocol enumeration
EFI_GUID *TargetProtocol = &gYourProtocolGuid;
UINTN HandleCount;
EFI_HANDLE *Handles;

Status = gBS->LocateHandleBuffer(
    ByProtocol,
    TargetProtocol,
    NULL,
    &HandleCount,
    &Handles
);

if (!EFI_ERROR(Status)) {
    for (UINTN i = 0; i < HandleCount; i++) {
        // Open and use protocol on each handle
        // ...
    }
    FreePool(Handles);
}

Best Practices

Always Check Return Status

UEFI functions return status codes indicating success or failure types. Never ignore these returns:

// Bad practice - ignoring status
gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface, ...);

// Good practice - checking status
Status = gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface, ...);
if (EFI_ERROR(Status)) {
    // Handle error appropriately
    return Status;
}

Match OpenProtocol with CloseProtocol

Every successful OpenProtocol requires a corresponding CloseProtocol:

// Open
Status = gBS->OpenProtocol(Handle, &ProtocolGuid, &Interface, 
                           AgentHandle, ControllerHandle, Attributes);
if (!EFI_ERROR(Status)) {
    // Use protocol...
    
    // Close when done
    gBS->CloseProtocol(&ProtocolGuid, Handle, AgentHandle, ControllerHandle);
}

Free Allocated Memory

Memory allocated through UEFI services must be freed:

// Allocate
Buffer = AllocatePool(Size);

// Use buffer...

// Free when done
FreePool(Buffer);

Choose Appropriate Open Mode

Select the OpenProtocol attribute matching your use case:

  • Use GET_PROTOCOL for simple, temporary access
  • Use BY_DRIVER when your driver manages the device
  • Use EXCLUSIVE when exclusive access is required

Debugging Tips

Using Debug Print

The Print function provides basic output capability. For more sophisticated debugging, consider:

#ifdef DEBUG
    Print(L"Debug: Value = %d\n", Value);
#endif

Status Code Interpretation

UEFI status codes follow conventions:

  • EFI_SUCCESS (0): Operation succeeded
  • EFI_ERROR codes (non-zero): Various failure conditions
  • Use EFI_ERROR(Status) macro to test for failures

Common Failure Modes

  • EFI_NOT_FOUND: Protocol or handle doesn't exist
  • EFI_ACCESS_DENIED: Insufficient permissions
  • EFI_ALREADY_STARTED: Protocol already opened exclusively
  • EFI_BUFFER_TOO_SMALL: Provided buffer insufficient (for LocateHandle)

Advanced Topics

Protocol Notification

UEFI supports protocol installation notifications, enabling drivers to respond when new protocols appear:

EFI_VOID *Notification;

Status = gBS->RegisterProtocolNotify(
    &gYourProtocolGuid,
    YourNotificationFunction,
    &Notification
);

Multiple Protocol Interfaces

Some handles support multiple protocols. Iterate through all protocols on a handle using ProtocolsPerHandle:

EFI_GUID **Protocols;
UINTN Count;

Status = gBS->ProtocolsPerHandle(Handle, &Protocols, &Count);

Driver Binding Protocol

For driver development, the Driver Binding Protocol provides start/stop callbacks for device management. This represents a more advanced pattern beyond simple protocol access.

Conclusion

Understanding how to call Protocol functions in UEFI applications forms a fundamental skill for firmware developers. The pattern—locate handles, open protocol, use interface, close protocol, free resources—applies consistently across all protocol interactions.

Key takeaways from this guide:

  1. Handles represent objects, Protocols represent capabilities
  2. LocateHandleBuffer simplifies handle discovery with automatic memory allocation
  3. OpenProtocol provides flexible access control through attribute modes
  4. Reference counting ensures protocols remain available while in use
  5. Proper cleanup prevents resource leaks and system instability

The example code provided offers a template adaptable to any protocol. By understanding these patterns, developers can confidently interact with the rich ecosystem of UEFI protocols, enabling sophisticated firmware functionality.

For further learning, explore the UEFI Specification documentation and examine open-source UEFI implementation code. Practical experimentation with QEMU or physical hardware solidifies these concepts through hands-on experience.

Remember: firmware development demands careful attention to detail. Every allocation requires freeing, every open requires closing, and every status code deserves checking. These disciplines separate reliable firmware from problematic implementations.


Reference: Additional video tutorial available at Bilibili for visual learners seeking complementary explanation of these concepts.