Implementing Asynchronous I/O Callbacks in UEFI

The Unified Extensible Firmware Interface (UEFI) operates in a single-threaded, cooperative execution environment, yet it frequently requires non-blocking hardware operations to maintain performance. This article explains how UEFI drivers implement asynchronous I/O completion callbacks using the native EFI_EVENT architecture, Task Priority Levels (TPL), and token-based driver protocols. By leveraging these primitives, drivers can initiate hardware requests, register completion handlers, and invoke callbacks when data transfers finish without stalling execution.

The UEFI Event-Driven Architecture

Because UEFI does not support native preemptive multithreading, asynchronous operations rely entirely on an event-driven architecture managed by UEFI Boot Services. The foundation of this system is the EFI_EVENT type.

To implement an asynchronous model, a driver or caller creates an event associated with a notification function (the callback) and assigns it a specific Task Priority Level (TPL). The firmware core executes this callback only when the event is explicitly signaled and the current execution context operates at a lower priority level.

Creating and Registering Completion Events

An asynchronous workflow begins with the creation of an event using the CreateEvent or CreateEventEx Boot Services API:

EFI_STATUS Status;
EFI_EVENT  CompletionEvent;

Status = gBS->CreateEvent (
                EVT_NOTIFY_SIGNAL,
                TPL_CALLBACK,
                MyIoCompletionCallback,
                ContextData,
                &CompletionEvent
                );
  • EVT_NOTIFY_SIGNAL: Specifies that the event's callback function will execute when the event transitions to the signaled state via gBS->SignalEvent().
  • TPL_CALLBACK: Sets the execution priority of the callback. UEFI defines hierarchical levels (TPL_APPLICATION, TPL_CALLBACK, TPL_NOTIFY, and TPL_HIGH_LEVEL). Using TPL_CALLBACK ensures the completion routine can execute safely without interrupting critical system state changes or high-level timer interrupts.
  • MyIoCompletionCallback: The function pointer executed upon completion.
  • ContextData: A pointer to caller-defined state information passed directly to the callback routine.

Token-Based Asynchronous Protocols

Modern UEFI specifications standardize asynchronous operations using token structures. Protocols such as EFI_BLOCK_IO2_PROTOCOL, EFI_DISK_IO2_PROTOCOL, and EFI_SIMPLE_NETWORK_PROTOCOL implement this pattern.

For example, EFI_BLOCK_IO2_PROTOCOL defines operations that take an EFI_BLOCK_IO2_TOKEN:

typedef struct {
    EFI_EVENT   Event;
    EFI_STATUS  TransactionStatus;
} EFI_BLOCK_IO2_TOKEN;

When an asynchronous read or write is issued:

  1. The consumer allocates the token, initializes Token->Event (either via a registered callback or as an event that can be polled/waited upon), and sets Token->TransactionStatus to EFI_NOT_READY.
  2. The consumer calls the protocol method (e.g., ReadBlocksEx), passing the token.
  3. The underlying driver enqueues the request to the hardware queue (e.g., NVMe submission queue or AHCI command list) and returns EFI_SUCCESS immediately, signaling that the operation has been queued rather than completed.

Hardware Monitoring and Completion Signaling

Because traditional hardware interrupts (IRQs) are often disabled or severely restricted in UEFI environments, drivers typically monitor asynchronous requests using periodic timer events or driver-level polling hooks.

  1. Timer-Based Polling: When an asynchronous transaction is queued, the underlying driver maintains an internal list of pending requests. The driver registers a periodic timer event (EVT_TIMER) that fires at TPL_NOTIFY.

  2. Checking Hardware Status: Inside the timer callback, the driver queries the hardware's completion ring or status register (such as an NVMe Completion Queue).

  3. Updating Status and Signaling: When the hardware indicates the transfer has completed, the driver updates Token->TransactionStatus with the final status code (such as EFI_SUCCESS or EFI_DEVICE_ERROR) and calls:

    gBS->SignalEvent(Token->Event);

Callback Execution Lifecycle

Once gBS->SignalEvent is called:

  • The UEFI core places the associated callback function on the event queue at the priority level defined during CreateEvent (e.g., TPL_CALLBACK).
  • If the processor is currently running at TPL_APPLICATION, the core immediately raises the TPL and dispatches the callback function.
  • If the processor is executing at an equal or higher TPL (such as inside the driver's polling routine at TPL_NOTIFY), the callback remains queued until the TPL drops back below TPL_CALLBACK.
  • The callback executes with the provided ContextData, allowing the consumer to process received data, handle errors, free resources, or trigger subsequent I/O operations.