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 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:
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 uint64_t AxlSourceId
AxlSourceId:
Opaque handle for a registered loop source, returned by the
axl_loop_add_*functions and passed toaxl_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_sourceon 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)
-
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_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 thisoff: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_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 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) oraxl_loop_iterate_untilwith a timeout, both of which spin up a nestedaxl_loop_run— is still safe: the backend wait detects the raised TPL and falls back to aCheckEventsweep instead ofWaitForEvent, 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-holdsTPL_CALLBACKfor 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 holdsTPL_CALLBACKindefinitely — 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_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.
-
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>
Keyboard input.
scan_code/unicode_charmirror UEFI EFI_INPUT_KEY;modifierscarries 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).
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.