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 a uint32_t source ID. Use it with axl_loop_remove_source(loop, id) to remove a source early:

uint32_t 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 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_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 the dispatch is non-blocking-only. The sources you can use safely from driver mode are the same sources axl_loop_run supports (timers, idle, raw events, pubsub) — anything that would internally call WaitForEvent (notably axl_loop_iterate_until with a non-zero timeout) is not safe inside a source callback.

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.

uint32_t 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.

uint32_t 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.

uint32_t 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.

uint32_t 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.

uint32_t 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.

uint32_t 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, uint32_t 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. Mirrors UEFI EFI_INPUT_KEY layout.

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)

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.