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 |
|
Run code once after a delay |
|
React to keyboard input |
|
Do background work between events |
|
Know when a UEFI protocol appears |
|
Integrate a TCP/custom EFI_EVENT |
|
Schedule work from a constrained context |
|
Decouple modules with events |
|
Run a simple event-driven app |
|
Build a FUSE-style driver loop |
|
Share a loop across modules |
|
Wait in a callback without freezing outer sources |
|
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 activeAXL_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:
Ignore it entirely —
axl_yield()still observes Ctrl-C by polling the break flag directly whenmDefaultLoop == NULL.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. Seedocs/AXL-Lifecycle.md§2.6.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 whereAxlLoop,AxlDefer, andAxlPubsubfit alongsideAxlEvent,AxlCancellable, and theAxlTaskpool.src/event/README.md—AxlEvent,AxlCancellable, and theaxl_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)
-
enumerator AXL_SOURCE_TIMER
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_runis the foreground driver — it ownsTPL_APPLICATIONand blocks ingBS->WaitForEvent. UEFI driver entry points have no foreground caller:DriverEntryreturns 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_driverinstalls a periodic firmware-managedEVT_TIMER | EVT_NOTIFY_SIGNALevent atTPL_CALLBACKwhose notify drains the loop in non-blocking mode everyinterval_ms. Idle callbacks, defer-queue work, and source events all dispatch from this notify exactly as they would insideaxl_loop_run.DriverEntrycalls this and returns;DriverUnloadcallsaxl_loop_detach_driver.TPL contract. UEFI 2.11 §7.1 allows only
TPL_CALLBACKorTPL_NOTIFYforEVT_NOTIFY_SIGNALevents — there is no signal queue atTPL_APPLICATION. We useTPL_CALLBACK. Co-located firmware drivers (TCP4 / MNP / SNP) run their own state machines at the sameTPL_CALLBACKlevel, 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_CALLBACKand drains every source with a signaled event, calling each callback exactly once before returning (capped at 2×AXL_MAX_SOURCESper 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 holdsTPL_CALLBACKfor 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 viaaxl_defer_call_laterto break it up across ticks.Boot Services TPL ceiling.
gBS->WaitForEventis unavailable aboveTPL_APPLICATION, so the dispatch is non-blocking-only. The sources you can use safely from driver mode are the same sourcesaxl_loop_runsupports (timers, idle, raw events, pubsub) — anything that would internally callWaitForEvent(notablyaxl_loop_iterate_untilwith 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_driverfirst 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
loopis 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_driverfromDriverUnload. 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>
Keyboard input. Mirrors UEFI EFI_INPUT_KEY layout.
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.
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.