AxlLoop — Event Loop

Event loop with timer, keyboard, idle, protocol notification, and raw event sources. GLib-inspired main loop with FUSE-style primitives.

Also includes the deferred work queue (AxlDefer) and the publish/subscribe event bus (AxlPubsub), both integrated with the loop.

Headers:

  • <axl/axl-loop.h> — Event loop core

  • <axl/axl-defer.h> — Deferred work queue (ring buffer)

  • <axl/axl-pubsub.h> — Publish/subscribe event bus

When to Use What

I need to…

Use

Run code every N milliseconds

axl_loop_add_timer

Run code once after a delay

axl_loop_add_timeout

React to keyboard input

axl_loop_add_key_press

Do background work between events

axl_loop_add_idle

Know when a UEFI protocol appears

axl_loop_add_protocol_notify

Integrate a TCP/custom EFI_EVENT

axl_loop_add_event

Schedule work from a constrained context

axl_defer

Decouple modules with events

axl_pubsub_publish / axl_pubsub_subscribe

Run a simple event-driven app

axl_loop_run

Build a FUSE-style driver loop

axl_loop_next_event / axl_loop_dispatch_event

Share a loop across modules

axl_loop_default (runtime-owned singleton)

Wait in a callback without freezing outer sources

axl_loop_iterate_until

Overview

UEFI applications are single-threaded and event-driven. The event loop is the central dispatcher: it waits for events (timers, keyboard input, network I/O, custom events) and calls registered callbacks.

Basic Pattern

#include <axl.h>

static bool on_timer(void *data) {
    axl_printf("tick\n");
    return AXL_SOURCE_CONTINUE;  // keep firing
}

static bool on_timeout(void *data) {
    axl_loop_quit(data);
    return AXL_SOURCE_REMOVE;    // one-shot, auto-removed
}

int main(int argc, char **argv) {
    AXL_AUTOPTR(AxlLoop) loop = axl_loop_new();

    axl_loop_add_timer(loop, 1000, on_timer, NULL);    // every 1s
    axl_loop_add_timeout(loop, 5000, on_timeout, loop); // quit after 5s

    axl_loop_run(loop);  // blocks until axl_loop_quit
    return 0;
}

Callback Signatures

All loop callbacks return bool:

  • AXL_SOURCE_CONTINUE (true) — keep the source active

  • AXL_SOURCE_REMOVE (false) — remove it from the loop

// Generic callback (timers, timeouts, idle, protocol, raw events)
typedef bool (*AxlLoopCallback)(void *data);

// Key press callback (receives the key)
typedef bool (*AxlKeyCallback)(AxlInputKey key, void *data);

Every axl_loop_add_* function returns an AxlSourceId (a 64-bit handle; 0 means failure). Ids come from a single process-global counter, so a stale id never collides with a source on another loop. Use it with axl_loop_remove_source(loop, id) to remove a source early:

AxlSourceId timer_id = axl_loop_add_timer(loop, 1000, on_tick, NULL);
// ...later...
axl_loop_remove_source(loop, timer_id);  // stop the timer

Source Types

Timer (repeating)

Fires every N milliseconds. Returns CONTINUE to keep firing.

static bool heartbeat(void *data) {
    send_keepalive(data);
    return AXL_SOURCE_CONTINUE;
}
axl_loop_add_timer(loop, 30000, heartbeat, conn);  // every 30s

Timeout (one-shot)

Fires once after a delay, then auto-removes. Useful for deadlines.

static bool connection_timeout(void *data) {
    axl_warning("connection timed out");
    axl_loop_quit(data);
    return AXL_SOURCE_REMOVE;
}
axl_loop_add_timeout(loop, 10000, connection_timeout, loop);

Idle

Runs on every loop iteration before the blocking wait. Use for background work (progress updates, polling, animations).

static bool update_progress(void *data) {
    int *pct = data;
    axl_printf("\rprogress: %d%%", *pct);
    return (*pct < 100) ? AXL_SOURCE_CONTINUE : AXL_SOURCE_REMOVE;
}
axl_loop_add_idle(loop, update_progress, &percent);

Key Press

Fires on console keyboard input with the key data.

static bool on_key(AxlInputKey key, void *data) {
    if (key.unicode_char == 'q') {
        axl_loop_quit(data);
        return AXL_SOURCE_REMOVE;
    }
    axl_printf("key: %c\n", (char)key.unicode_char);
    return AXL_SOURCE_CONTINUE;
}
axl_loop_add_key_press(loop, on_key, loop);

Protocol Notify

Fires when a UEFI protocol is installed on any handle. Use this to react to hot-plug events (NIC driver loaded, new filesystem mounted).

static bool on_nic_ready(void *data) {
    axl_info("network interface appeared");
    start_network(data);
    return AXL_SOURCE_REMOVE;  // only need the first one
}

// Watch for the SNP (Simple Network Protocol) GUID
axl_loop_add_protocol_notify(loop, &gEfiSimpleNetworkProtocolGuid,
                             on_nic_ready, app_ctx);

Raw Event

Integrates a UEFI event into the loop. The entry point takes an AxlEventHandle (raw EFI_EVENT) so the same API works for both AXL-managed events (AxlEvent *, via axl_event_handle(e)) and firmware-owned handles (TCP completion tokens, protocol-notify events). The caller owns the event.

// AXL-managed event (new/free/signal/reset state machine in AXL):
AxlEvent *my_event = axl_event_new();

axl_loop_add_event(loop, axl_event_handle(my_event),
                   on_custom_event, ctx);

// From another context (e.g., a protocol callback):
axl_event_signal(my_event);  // triggers on_custom_event on next tick

// Cleanup (after removing from loop):
axl_event_free(my_event);

See ../event/README.md for AxlEvent semantics (signal / reset / is_set / wait_timeout) and its typed stop-token cousin, AxlCancellable.

Lifecycle & Cleanup

Tear down caller-owned resources — sockets, async ops, custom AxlEvent sources — before the loop they were registered against. If axl_loop_free finds a raw AxlEvent source still active it logs an error naming the source id, which usually points at a resource freed in the wrong order (e.g. the loop outlived by a lingering async op’s completion event). Source types owned by the loop (timers, idle, key-press, protocol-notify, defer) are cleaned up automatically.

Run vs. Next+Dispatch

axl_loop_run blocks until axl_loop_quit is called. For manual control (e.g., FUSE-style drivers), use the step API:

while (running) {
    int rc = axl_loop_next_event(loop, true);  // block until event
    if (rc == -1) break;                        // Ctrl-C
    axl_loop_dispatch_event(loop);              // fire callbacks
    // ... do other work between iterations ...
}

Use axl_loop_dispatch(loop, false) for a non-blocking single step (check + dispatch if ready, return immediately if not).

Driver Mode (axl_loop_attach_driver)

axl_loop_run is the foreground driver — it owns TPL_APPLICATION and blocks in gBS->WaitForEvent. UEFI driver entry points have no foreground caller: DriverEntry returns to the firmware after publishing protocols. Without a foreground caller, sources never dispatch and timers never fire — anything async in the loop is dead. axl_loop_attach_driver is the bridge for the DXE-driver use case (HTTP server inside a driver image, async pubsub-driven worker, etc.).

EFI_STATUS EFIAPI DriverEntry(EFI_HANDLE image, EFI_SYSTEM_TABLE *st) {
    axl_driver_init(image, st);
    axl_driver_set_unload(MyUnload);

    AxlLoop *loop = axl_loop_new();
    AxlHttpServer *server = axl_http_server_new(...);
    axl_http_server_start(server, loop);
    axl_http_server_listen(server, 80);

    /* Hand the loop to firmware-managed dispatch. 50 ms is the
       typical period — frequent enough for a responsive HTTP
       server, sparse enough to leave headroom. */
    if (axl_loop_attach_driver(loop, 50) != AXL_OK) {
        axl_printf("FAIL: loop attach\n");
        return EFI_ABORTED;
    }
    return EFI_SUCCESS;
}

EFI_STATUS EFIAPI MyUnload(EFI_HANDLE image) {
    /* Detach BEFORE freeing the loop so no notify is in flight
       when consumer state goes away. */
    axl_loop_detach_driver(loop);
    axl_http_server_free(server);
    axl_loop_free(loop);
    return EFI_SUCCESS;
}

The TPL Contract — and Why You Don’t Roll Your Own

UEFI 2.11 §7.1 allows only TPL_CALLBACK or TPL_NOTIFY for EVT_NOTIFY_SIGNAL events — there is no signal queue at TPL_APPLICATION. axl_loop_attach_driver uses TPL_CALLBACK, the same TPL that co-located firmware drivers (TCP4, MNP, SNP) use for their own state-machine notifies. Because they share the TPL, the firmware’s FIFO notify queue alternates between them and us — as long as no one holds TPL_CALLBACK for too long, everyone makes progress.

The notify-budget rule. The consumer’s loop source callbacks run inside axl_loop_dispatch at TPL_CALLBACK. If a callback does heavy work — large allocation, synchronous I/O, a multi-millisecond loop, a blocking protocol call — it holds TPL_CALLBACK for that whole duration. While we’re holding TPL_CALLBACK, TCP4 / MNP / SNP cannot advance their own notifies (same level, no preemption). At best you see latency spikes; at worst, a co-located TCP4 listener can’t progress its accept-rearm state machine and connections start failing.

The pre-built helper drains every signaled event per tick (capped at 2 × AXL_MAX_SOURCES as a runaway guard; hitting the cap is logged). Per-tick drain is what matches the consumer’s expected contract — under HTTP load a recv-data callback synchronously submits axl_tcp_send_async, TCP4 typically completes the Transmit inline, and the tx-event needs to be drained the same tick or the on_response_sent callback queues behind whatever else fires next. A naive one-axl_loop_dispatch-per-tick loop quietly starves completion handlers under sequential request load — accept (slot 0) keeps preempting, conn-pool slots fill with active=true connections whose response-completion never fires, and the listener appears wedged after exactly HTTP_DEFAULT_MAX_CONNS requests. Rolling your own with gBS->CreateEvent(EVT_TIMER | EVT_NOTIFY_SIGNAL, TPL_CALLBACK, ...) calling axl_loop_dispatch directly can work — but you need the same drain pattern AND the same notify-budget discipline.

Boot-Services TPL ceiling. gBS->WaitForEvent returns EFI_UNSUPPORTED above TPL_APPLICATION, so the dispatch is non-blocking-only. Don’t call axl_loop_iterate_until with a non-zero timeout from inside a source callback — it would try to WaitForEvent at the wrong TPL.

What to do with slow work. Break it up. Use axl_defer_call_later to schedule work for the next tick instead of running it inline. Use a one-shot timer if the work needs delaying. Either pattern lets TPL_CALLBACK drop back to the firmware between iterations so co-located drivers can progress.

Cleanup

axl_loop_detach_driver cancels the timer, drains any in-flight notify, and frees the bridging context. If DriverUnload forgets to call it, axl_loop_free will detach as a safety net (with a warning) — but the right place is DriverUnload, BEFORE freeing the loop and BEFORE unregistering protocols, so no notify is mid- dispatch when consumer state goes away.

Nested Waits (axl_loop_iterate_until)

The standard ephemeral-loop approach for waiting (axl_event_wait_timeout, axl_wait_*) creates a throwaway loop for the duration of the wait – the caller’s outer loop is paused, and its sources (timers, idle, etc.) don’t fire until the wait returns. That’s usually what you want; it’s also clean because the inner loop’s sources can’t leak into the outer.

But sometimes a source callback needs to wait on an async producer and keep the outer loop’s own sources alive. For that, use axl_loop_iterate_until on the outer loop directly:

int rc = axl_loop_iterate_until(
    outer,             /* the caller's own loop */
    done_event,        /* NULL OK -- only timeout wakes */
    timeout_us);       /* 0 = wait forever */

Drives outer until done is signalled, the timeout elapses, or Ctrl-C. Does NOT set outer->quit_requested, so the enclosing axl_loop_run resumes normally afterwards. Returns 0 on done, -1 on timeout, AXL_CANCELLED on interrupt. See docs/AXL-Lifecycle.md §5.6.

Default Loop (axl_loop_default)

The runtime (see src/runtime/README.md) exposes a shared singleton loop, created lazily on the first axl_loop_default() call (CRT0 does not pre-create it) and freed during _axl_cleanup if it was ever materialized. Apps can:

  1. Ignore it entirely — axl_yield() still observes Ctrl-C by polling the break flag directly when mDefaultLoop == NULL.

  2. Register sources on it and call axl_yield() in a tight CPU loop — yields dispatch the loop non-blocking, so timers, timeouts, defers, and raw events fire in line. Idle sources are a footgun in this mode: they run on every yield, not just when the loop is genuinely idle. See docs/AXL-Lifecycle.md §2.6.

  3. Call axl_loop_run(axl_loop_default()) to hand control to the loop — appropriate for event-driven servers.

Private loops via axl_loop_new() remain first-class and are often the right choice for scoped work.

AxlDefer

Deferred work queue — schedules a function to run on the next loop iteration. Use in constrained contexts where complex work isn’t safe:

  • Protocol notification callbacks (UEFI restricts what you can call)

  • Nested callbacks (avoid re-entrancy)

  • Interrupt-like handlers (need to return quickly)

Callback Signature

typedef void (*AxlDeferCallback)(void *data);

Usage

// Called from a protocol notification (constrained — can't do Boot Services)
void on_protocol_installed(void *ctx) {
    axl_defer(loop, initialize_new_protocol, ctx);
}

// Runs safely on the next main loop tick (full Boot Services available)
void initialize_new_protocol(void *ctx) {
    locate_and_configure(ctx);
}

Cancellation

uint32_t handle = axl_defer(loop, some_work, ctx);
// ... changed my mind ...
axl_defer_cancel(loop, handle);  // no-op if already fired

The queue is a fixed-capacity ring buffer with no dynamic allocation in the hot path. Deferred work is drained automatically at the start of each loop iteration.

AxlPubsub

Publish/subscribe event bus for decoupling modules. Modules publish on named topics; other modules subscribe with callbacks. Delivery is deferred (via AxlDefer) so handlers always run in a safe main-loop context.

When to Use Pub/sub

  • Decoupling — a producer doesn’t know (or care) who its consumers are

  • Multiple consumers — adding a new subscriber requires zero changes to the producer

  • Cross-module events — “network is ready”, “config changed”, “shutdown requested”

For point-to-point communication (one caller, one callee), use a direct function call or a callback pointer instead.

Callback Signature

typedef void (*AxlPubsubCallback)(
    void *event_data,  // from axl_pubsub_publish (may be NULL)
    void *user_data    // from axl_pubsub_subscribe
);

Producer / Consumer Example

// --- Producer (network module) ---

typedef struct {
    char ip[16];
    char gateway[16];
} NetConfig;

void on_dhcp_complete(AxlLoop *loop, NetConfig *cfg) {
    // Publish to all subscribers — producer doesn't know who listens
    axl_pubsub_publish(loop, "ip-changed", cfg);
}

// --- Consumer 1 (splash screen) ---

void on_ip_changed(void *event_data, void *user_data) {
    NetConfig *cfg = event_data;
    update_splash_ip(cfg->ip);
}

uint32_t handle = axl_pubsub_subscribe(loop, "ip-changed", on_ip_changed, NULL);

// --- Consumer 2 (REST API) --- completely independent

void on_ip_changed_api(void *event_data, void *user_data) {
    NetConfig *cfg = event_data;
    restart_http_server(cfg->ip);
}

axl_pubsub_subscribe(loop, "ip-changed", on_ip_changed_api, NULL);

Data Lifetime

Important: event_data passed to axl_pubsub_publish must remain valid until the next loop tick, because delivery is deferred. Stack variables are fine if publish and the next loop_dispatch happen in the same function scope. For longer lifetimes, heap-allocate or use a static.

Unsubscribe

uint32_t handle = axl_pubsub_subscribe(loop, "ip-changed", on_ip_changed, NULL);
// ...later (e.g., on module shutdown)...
axl_pubsub_unsubscribe(loop, handle);

Always unsubscribe before freeing the user_data pointer, or the callback will fire with a dangling pointer.

Topics are auto-created on first subscribe or publish. axl_pubsub_reset(loop) clears all topics and subscribers (for shutdown or between test runs).

See also

  • docs/AXL-Concurrency.md — the full primitive-selection taxonomy across dispatch / coordination / notification / offload, including where AxlLoop, AxlDefer, and AxlPubsub fit alongside AxlEvent, AxlCancellable, and the AxlTask pool.

  • src/event/README.mdAxlEvent, AxlCancellable, and the axl_wait_* helpers.

API Reference

AxlLoop

Defines

AXL_SOURCE_CONTINUE

Return from callback to keep the source active.

AXL_SOURCE_REMOVE

Return from callback to remove the source from the loop.

axl_loop_new()

Captures the caller’s file/line for leak reporting via the tier-1 resource registry. See docs/AXL-Lifecycle.md §4.2.1.

Typedefs

typedef struct AxlLoop AxlLoop

axl-loop.h:

AxlLoop — event loop with timer, keyboard, idle, protocol notification, and raw event sources. The model maps directly onto GLib: AxlLoop is the AXL counterpart of GMainLoop, axl_loop_run / axl_loop_quit play the role of g_main_loop_run / g_main_loop_quit, axl_loop_add_timer is g_timeout_add, and so on. If you have written a GLib daemon, the shape is the same — what differs is the source kinds: AXL adds raw-EFI-event sources (axl_loop_add_event) so any UEFI event (TCP completion tokens, protocol-notify, AxlEvent instances via axl_event_handle) drops straight into the loop without polling.

typedef uint64_t AxlSourceId

AxlSourceId:

Opaque handle for a registered loop source, returned by the axl_loop_add_* functions and passed to axl_loop_remove_source. 0 is never a valid id (it means “no source”).

Ids are allocated from a single PROCESS-GLOBAL monotonic counter, so every live source across every loop has a distinct id. This is what makes a stale id (one that outlived its loop) safe to pass to axl_loop_remove_source on a different loop: it matches nothing, so the removal is a no-op rather than deleting an unrelated source. 64-bit so the counter never wraps in any realistic process lifetime.

typedef bool (*AxlLoopCallback)(void *data)

AxlLoopCallback:

Generic event callback. Return AXL_SOURCE_CONTINUE to keep the source active, or AXL_SOURCE_REMOVE to remove it. To quit the loop, call axl_loop_quit() from inside the callback.

typedef bool (*AxlKeyCallback)(AxlInputKey key, void *data)

AxlKeyCallback:

Key press callback. Return AXL_SOURCE_CONTINUE to keep the source active, or AXL_SOURCE_REMOVE to remove it. To quit the loop, call axl_loop_quit() from inside the callback.

Enums

enum AxlSourceType

AxlSourceType:

Identifies the kind of event source in the loop.

Values:

enumerator AXL_SOURCE_TIMER

repeating timer

enumerator AXL_SOURCE_TIMEOUT

one-shot timer (auto-removed after firing)

enumerator AXL_SOURCE_KEYPRESS

console keyboard input

enumerator AXL_SOURCE_IDLE

fires every iteration before blocking wait

enumerator AXL_SOURCE_PROTOCOL

UEFI protocol install notification.

enumerator AXL_SOURCE_EVENT

raw EFI event handle (caller-owned)

Functions

AxlLoop *axl_loop_new_impl(const char *file, int line)

Create a new event loop.

Returns:

new AxlLoop, or NULL on failure.

void axl_loop_free(AxlLoop *loop)

Free an event loop and close all internal events.

Parameters:
  • loop – loop to free (NULL-safe)

void axl_loop_quit(AxlLoop *loop)

Signal the loop to quit. Safe to call from callbacks.

Parameters:
  • loop – loop to quit

bool axl_loop_is_running(AxlLoop *loop)

Check if the loop is running.

Parameters:
  • loop – loop to check

Returns:

true if running and not quit-requested.

void axl_loop_set_intercept_break(AxlLoop *loop, bool intercept)

Control whether a bare Ctrl-C quits the loop.

By default (true) the loop treats a modifier-less Ctrl-C (UnicodeChar == 0x03, KeyShiftState == 0 — what a serial/TerminalDxe console emits) and the shell break event as “quit the loop”. A GUI app that wants Ctrl+C for its own use (an editor mapping it to Copy) sets this off: the 0x03 byte is then delivered to the app’s keypress source instead, and the shell break event is ignored. The app is then responsible for its own exit affordance (a Quit command / Ctrl+Q).

Note

Modified-bit Ctrl+C (a real keyboard reporting the CTRL state) was never intercepted and is unaffected by this flag.

Parameters:
  • loop – event loop

  • intercept – true (default) = Ctrl-C quits; false = deliver

bool axl_loop_intercept_break(AxlLoop *loop)

Query the Ctrl-C intercept flag (see axl_loop_set_intercept_break).

Parameters:
  • loop – event loop

void axl_loop_add_cleanup(AxlLoop *loop, AxlLoopCallback cb, void *data)

Add a cleanup callback fired on exit (FIFO order).

Parameters:
  • loop – loop

  • cb – callback fired on exit (FIFO order)

  • data – opaque data

int axl_loop_next_event(AxlLoop *loop, bool blocking)

Wait for (or check) the next event.

Parameters:
  • loop – event loop

  • blocking – true to block until event, false to return immediately

Returns:

0 if event pending (call axl_loop_dispatch_event), 1 if non-blocking and nothing ready, -1 if Ctrl-C detected (loop should exit).

void axl_loop_dispatch_event(AxlLoop *loop)

Dispatch the pending event from the last axl_loop_next_event call.

Parameters:
  • loop – event loop

int axl_loop_dispatch(AxlLoop *loop, bool blocking)

Single iteration: axl_loop_next_event + axl_loop_dispatch_event.

Parameters:
  • loop – event loop

  • blocking – true to block, false for non-blocking

Returns:

0 on event dispatched, 1 if not ready, -1 on Ctrl-C.

int axl_loop_run(AxlLoop *loop)

Run the event loop until quit. Fires cleanup callbacks on exit.

Parameters:
  • loop – event loop

Returns:

0 on normal exit, -1 on Ctrl-C.

int axl_loop_attach_driver(AxlLoop *loop, uint64_t interval_ms)

Drive the loop’s dispatch from a firmware-managed periodic timer (DXE driver mode).

axl_loop_run is the foreground driver — it owns TPL_APPLICATION and blocks in gBS->WaitForEvent. UEFI driver entry points have no foreground caller: DriverEntry returns to the firmware after publishing protocols. Without a foreground caller, sources never dispatch and timers never fire, so anything async in the loop is dead.

axl_loop_attach_driver installs a periodic firmware-managed EVT_TIMER | EVT_NOTIFY_SIGNAL event at TPL_CALLBACK whose notify drains the loop in non-blocking mode every interval_ms. Idle callbacks, defer-queue work, and source events all dispatch from this notify exactly as they would inside axl_loop_run. DriverEntry calls this and returns; DriverUnload calls axl_loop_detach_driver.

TPL contract. UEFI 2.11 §7.1 allows only TPL_CALLBACK or TPL_NOTIFY for EVT_NOTIFY_SIGNAL events — there is no signal queue at TPL_APPLICATION. We use TPL_CALLBACK. Co-located firmware drivers (TCP4 / MNP / SNP) run their own state machines at the same TPL_CALLBACK level, so the FIFO notify queue alternates fairly between them and us as long as our notify stays short.

Notify-budget rule. The consumer’s loop sources must run fast. Each tick runs at TPL_CALLBACK and drains every source with a signaled event, calling each callback exactly once before returning (capped at 2× AXL_MAX_SOURCES per tick as a runaway guard — hitting the cap is logged). If a source callback does heavy work (large allocation, synchronous I/O, blocking protocol calls), it holds TPL_CALLBACK for that whole duration and starves co-located firmware drivers that need the same TPL — at best you see latency spikes, at worst connection-refused on a co-located TCP4. Keep source callbacks under ~1 ms; defer slow work via axl_defer_call_later to break it up across ticks.

Boot Services TPL ceiling. gBS->WaitForEvent is unavailable above TPL_APPLICATION, so this tick’s own dispatch is non-blocking-only. A nested blocking wait reached from a source callback — a synchronous network op (axl_udp_send, axl_http_post, a DNS lookup) or axl_loop_iterate_until with a timeout, both of which spin up a nested axl_loop_run — is still safe: the backend wait detects the raised TPL and falls back to a CheckEvent sweep instead of WaitForEvent, so it makes progress and returns rather than wedging. It becomes a latency concern instead of a safety one, but the blast radius is wide: the nested wait busy-holds TPL_CALLBACK for its whole duration (the notify-budget rule above), which stalls every other connection serviced by the same pump, not just the current one. A wait that carries a timeout self-limits to that deadline (the sync network ops all pass one); a deadline-less wait whose condition never resolves holds TPL_CALLBACK indefinitely — so always give such waits a timeout, keep them short, or gate the slow op off the driver pump.

Typical period: 50 ms — frequent enough for a responsive HTTP server, sparse enough to leave headroom. Pick lower for latency-sensitive pubsub delivery; pick higher for cost-sensitive idle workloads.

Idempotent-fail: returns AXL_ERR if the loop is already attached (call axl_loop_detach_driver first to change the period).

Parameters:
  • loop – loop to attach (must already exist)

  • interval_ms – dispatch period in ms (typical: 50)

Returns:

AXL_OK on success, AXL_ERR if loop is NULL, already attached, or the firmware refused the timer.

int axl_loop_detach_driver(AxlLoop *loop)

Tear down a driver-mode loop attachment.

Cancels the periodic timer, drains any in-flight notify, and frees the timer’s bridging context. Pair with axl_loop_attach_driver from DriverUnload. NULL-safe; safe to call on a loop that was never attached (returns AXL_ERR).

Order in DriverUnload: detach the loop FIRST, then unregister any protocols, then free the loop. Detaching first guarantees no notify is in flight when consumer state goes away.

Parameters:
  • loop – loop to detach

Returns:

AXL_OK on success, AXL_ERR if not currently attached.

AxlSourceId axl_loop_add_timer(AxlLoop *loop, uint32_t interval_ms, AxlLoopCallback cb, void *data)

Add a repeating timer.

Parameters:
  • loop – event loop

  • interval_ms – timer interval in milliseconds

  • cb – callback fired each interval

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

AxlSourceId axl_loop_add_timeout(AxlLoop *loop, uint32_t delay_ms, AxlLoopCallback cb, void *data)

Add a one-shot timeout (auto-removed after firing).

Parameters:
  • loop – event loop

  • delay_ms – timeout delay in milliseconds

  • cb – callback fired on timeout (one-shot, auto-removed)

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

AxlSourceId axl_loop_add_key_press(AxlLoop *loop, AxlKeyCallback cb, void *data)

Add a key press handler.

Parameters:
  • loop – event loop

  • cb – key press callback

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

AxlSourceId axl_loop_add_idle(AxlLoop *loop, AxlLoopCallback cb, void *data)

Add an idle callback (fired every iteration before wait).

Parameters:
  • loop – event loop

  • cb – idle callback (fired every iteration before wait)

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

AxlSourceId axl_loop_add_protocol_notify(AxlLoop *loop, void *guid, AxlLoopCallback cb, void *data)

Add a protocol install notification.

Parameters:
  • loop – event loop

  • guid – protocol GUID to watch (void* to avoid EFI_GUID in header)

  • cb – callback on protocol install

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

AxlSourceId axl_loop_add_event(AxlLoop *loop, AxlEventHandle event, AxlLoopCallback cb, void *data)

Add a raw event handle to the loop.

Fires cb when the event is signaled. The caller owns the event — the loop does NOT close it on removal. Use this to integrate TCP completion tokens, custom protocol events, or any EFI_EVENT into the main loop without polling.

Parameters:
  • loop – event loop

  • event – event handle (from axl_event_handle or a firmware-owned EFI_EVENT)

  • cb – callback when event is signalled

  • data – opaque data

Returns:

source ID for axl_loop_remove_source, or 0 on failure.

void axl_loop_remove_source(AxlLoop *loop, AxlSourceId source_id)

Remove an event source by ID.

Parameters:
  • loop – event loop

  • source_id – ID returned by axl_loop_add_*

int axl_loop_iterate_until(AxlLoop *loop, AxlEvent *done, uint64_t timeout_us)

Iterate a running loop until an event fires or a timeout elapses, without quitting the loop.

This is the nested-wait primitive for callers inside a loop callback that need to wait for a producer to signal completion. Unlike axl_event_wait_timeout (which spins up a throwaway loop and freezes the caller’s outer loop), this function drives the caller’s own loop — the outer loop’s existing sources keep firing for the duration of the wait. It does NOT set the loop’s quit flag, so the enclosing axl_loop_run (if any) resumes normally after this returns.

Typical use: a source callback that needs to wait on an async producer without starving the rest of the loop’s timers.

Parameters:
  • loop – loop to drive (caller’s outer loop, typically)

  • done – event to wait on (NULL = only timeout/cancel wakes)

  • timeout_us – timeout in microseconds (0 = no timeout, wait forever)

Returns:

0 if done was signalled, -1 on timeout, AXL_CANCELLED on Ctrl-C or invalid argument.

struct AxlInputKey
#include <axl-loop.h>

AxlInputKey:

Keyboard input. scan_code / unicode_char mirror UEFI EFI_INPUT_KEY; modifiers carries the normalized held-modifier and lock state (AXL_INPUT_MOD_* bits from <axl/axl-input.h>), or 0 when the platform can’t report it (no SimpleTextInputEx / serial).

Public Members

uint16_t scan_code

function/arrow key scan code (0 for printable chars)

uint16_t unicode_char

printable character (0 for special keys)

uint32_t modifiers

AXL_INPUT_MOD_* held + lock state (0 if unavailable)

AxlDefer

Typedefs

typedef struct AxlLoop AxlLoop

axl-defer.h:

Deferred work queue owned by the event loop.

Allows code in constrained contexts (protocol notifications, nested callbacks, interrupt-like handlers) to schedule work for “next tick” without blocking or re-entering the loop.

// In a protocol notification (constrained context):
axl_defer(loop, initialize_protocol, ctx);

// Fires safely on the next main loop iteration.
typedef void (*AxlDeferCallback)(void *data)

AxlDeferCallback:

Deferred work function. Runs on the BSP main loop thread.

Functions

uint32_t axl_defer(AxlLoop *loop, AxlDeferCallback fn, void *data)

Schedule deferred work for the next loop tick.

Safe to call from protocol notifications, nested callbacks, or any context where complex work should not run immediately.

Parameters:
  • loop – event loop

  • fn – work function

  • data – opaque data passed to fn

Returns:

handle for axl_defer_cancel(), or 0 if the queue is full.

bool axl_defer_cancel(AxlLoop *loop, uint32_t handle)

Cancel pending deferred work before it fires.

No-op if the handle is invalid or already fired.

Parameters:
  • loop – event loop

  • handle – handle from axl_defer()

Returns:

true if the work was cancelled, false if already fired or invalid.

AxlPubsub

Typedefs

typedef struct AxlLoop AxlLoop

axl-pubsub.h:

Publish/subscribe event bus with deferred delivery, owned by the event loop.

Decouples event producers from consumers. Modules publish on named topics; other modules subscribe with callbacks. Callbacks are dispatched via the loop’s defer queue so they always run in a safe main-loop context.

// Publisher (network module):
axl_pubsub_publish(loop, "ip-changed", &new_ip);

// Subscriber (splash screen):
axl_pubsub_subscribe(loop, "ip-changed", on_ip_changed, splash_ctx);

Topics are auto-created on first subscribe. Callers must ensure event_data passed to axl_pubsub_publish remains valid until the next loop tick (when deferred callbacks fire).

typedef void (*AxlPubsubCallback)(void *event_data, void *user_data)

AxlPubsubCallback:

Subscriber callback. Runs on the BSP main loop thread (via defer queue).

Functions

bool axl_pubsub_register(AxlLoop *loop, const char *name)

Explicitly register a named topic.

Optional — topics are auto-created on first subscribe or publish.

Parameters:
  • loop – event loop

  • name – topic name (pointer stored, not copied)

Returns:

true if registered (or already exists), false if table full.

void axl_pubsub_reset(AxlLoop *loop)

Reset the pub/sub system — free all subscribers and topics.

Called automatically by axl_loop_free(). Call explicitly only for between-test-run cleanup.

Parameters:
  • loop – event loop

uint32_t axl_pubsub_subscribe(AxlLoop *loop, const char *name, AxlPubsubCallback cb, void *data)

Subscribe to a named topic.

The callback fires (via defer queue) each time the topic is published. Auto-creates the topic if it doesn’t exist yet.

Parameters:
  • loop – event loop

  • name – topic name

  • cb – callback (fires on publish, deferred)

  • data – opaque data passed to cb

Returns:

handle for axl_pubsub_unsubscribe, or 0 on failure.

bool axl_pubsub_unsubscribe(AxlLoop *loop, uint32_t handle)

Unsubscribe from a topic.

Parameters:
  • loop – event loop

  • handle – handle from axl_pubsub_subscribe

Returns:

true if unsubscribed, false if handle invalid or already removed.

bool axl_pubsub_publish(AxlLoop *loop, const char *name, void *event_data)

Publish on a named topic.

Schedules all subscribers’ callbacks via the loop’s defer queue. Safe to call from constrained contexts.

The caller must ensure event_data remains valid until the next loop tick (when deferred callbacks fire).

Parameters:
  • loop – event loop

  • name – topic name

  • event_data – data passed to all subscribers (may be NULL)

Returns:

true if topic exists and had subscribers, false otherwise.