AxlRuntime — lifecycle services
AXL Runtime — lifecycle services for an AXL app
The pieces an AXL app interacts with around its own lifecycle and
interruptibility. The CRT0 entry stub
(src/crt0/axl-crt0-native.c, ~17 lines) bridges the UEFI entry
point to int main(int argc, char **argv) by calling
_axl_init before main and _axl_cleanup after. This
module — the AXL runtime — implements those two bookends and
everything they wire up: the default event loop, the cooperative
yield, the interrupt handler registry, the tier-1 resource-leak
sweep, and the LIFO atexit callback registry. CRT0 holds none of
that state itself; it just calls in and back out.
The full lifecycle (init → main → cleanup → exit) and the
runtime-vs-CRT0 split are documented in
docs/AXL-Lifecycle.md. This README is the
module-level reference for the runtime sources.
Four sub-modules, each a single concern:
axl-runtime.c—_axl_init/_axl_cleanup, the singletonaxl_loop_default(), andaxl_yield.axl-registry.c— tier-1 firmware-resource registry. Internal-only API (_axl_registry_*) called by the_new_impl/_freepaths of AxlEvent, AxlLoop, AxlCancellable, and AxlArena. Sweeps leaked resources during_axl_cleanup.axl-atexit.c— POSIX-flavored cleanup registry (axl_atexit/axl_atexit_remove). LIFO drain during_axl_cleanup, before the tier-1 sweep.axl-signal.c—axl_signal_install/axl_interrupted/axl_exit. Hooked into loop break detection; invokes the user handler once per Ctrl-C and sets the interrupted flag.
Headers:
<axl/axl-runtime.h>— default loop, yield, registry count<axl/axl-signal.h>— interrupt API + blessed exit<axl/axl-atexit.h>— LIFO cleanup callbacks<axl/axl-loop.h>— includesaxl_loop_iterate_until(nested-wait primitive), the loop-module partner for callers inside a callback that need to wait without freezing outer sources
When to Use What
I need to… |
Use |
|---|---|
Handle Ctrl-C with a custom callback |
|
Ask “did Ctrl-C happen yet?” from a CPU loop |
|
Exit with cleanup guaranteed (vs raw |
|
Keep a tight CPU loop responsive to Ctrl-C |
|
Free a long-lived resource on any exit path |
|
Share a loop across modules |
|
Wait inside a callback without starving the outer loop |
|
Interrupt lifecycle
user presses Ctrl-C
|
v
shell signals its ExecutionBreak event
|
+--- loop observes via axl_backend_shell_break_flag /
| axl_backend_shell_break_event (in axl_loop_next_event)
|
+--- OR axl_yield observes via a non-blocking loop dispatch
| (or a direct poll when no default loop exists)
|
v
_axl_signal_on_break() fires once:
- sets g_axl_interrupted = true
- if axl_signal_install(fn) was called, invokes fn()
- idempotent (subsequent observations no-op)
|
v
yield / loop_run / wait_* return with status indicating interrupt
|
+--- if user handler installed: caller unwinds main, CRT0
| runs _axl_cleanup on main's return
|
+--- if no handler: next axl_yield() auto-calls axl_exit(1),
which takes the same _axl_cleanup path + gBS->Exit
The two exit paths (return from main, axl_exit(rc)) both
converge on _axl_cleanup, so cleanup output is byte-identical
between them. _axl_cleanup has a reentrancy guard: if
axl_exit fires mid-main and the firmware somehow returns from
gBS->Exit (paranoia), CRT0’s post-main cleanup is a no-op.
Cleanup order
_axl_cleanup runs these in order:
atexit callbacks (LIFO) —
_axl_atexit_run_all. User callbacks may free resources that would otherwise show up in the sweep.argv strings —
_axl_args_freeinsrc/posix/axl-app.c.Default loop — explicit
axl_loop_free(mDefaultLoop)so its registry entry comes off cleanly (otherwise sweep would flag it as a leak on every exit).Tier-1 registry sweep —
_axl_registry_sweep. LIFO walk of live entries, each logged with userfile:lineand closed via the appropriate_free.Heap leak report (AXL_MEM_DEBUG only) –
axl_mem_dump_leaks. Release-mode auto-free of heap is deferred (seedocs/AXL-Lifecycle.md§10.1).
Caller attribution
Every tier-1 resource allocation records the caller’s file/line at macro-expansion time:
AxlEvent *e = axl_event_new(); // expands to:
// axl_event_new_impl(__FILE__, __LINE__)
When the sweep fires on a leak, the warning names that file/line
– the app developer’s call site, not the library’s internal
wrapper. Library-internal callers (e.g., axl_cancellable_new
internally calling axl_event_new) record the library’s own file/
line by design; library code that correctly frees never reaches
the sweep, so the only way those appear is if the library itself
leaks — in which case the library source is the correct
attribution.
See also
docs/AXL-Lifecycle.md— the design doc, now describing what landed.docs/AXL-Concurrency.md– primitive taxonomy; the runtime sits under these primitives.sdk/examples/runtime-demo.c— eight subcommand scenarios exercising every facet.src/loop/README.md—AxlLoop,AxlDefer,AxlPubsub, and the nested-wait primitive.
API Reference
AxlSignal (interrupts + blessed exit)
Typedefs
-
typedef void (*AxlSignalHandler)(void)
axl-signal.h:
POSIX-flavored interrupt handler API. The axl_signal_* namespace was freed up pre-1.0 (see the axl_pubsub_* rename) specifically to host this surface.
At runtime:
CRT0 (via _axl_init) installs a notify path on the shell ExecutionBreak event. When the user presses Ctrl-C, the runtime sets a global “interrupted” flag and, if the app has installed a handler via axl_signal_install, invokes it.
The handler runs in a limited context (raised TPL). It is expected to set an app flag, log, and return — not to allocate, block, or call Boot Services that mutate state. Cleanup happens at the next yield point or inside the app’s own axl_atexit callback chain.
If no handler is installed, the default behavior is to terminate the app cleanly at the next axl_yield observation (runs _axl_cleanup, then gBS->Exit via axl_backend_boot_exit).
static volatile bool g_should_quit; static void on_interrupt(void) { g_should_quit = true; } int main(int argc, char **argv) { axl_signal_install(on_interrupt); while (!g_should_quit && more_work()) { do_work(); axl_yield(); } return g_should_quit ? 1 : 0; }See docs/AXL-Lifecycle.md §2.2 and §4.4 for the design. AxlSignalHandler:
Interrupt-time callback. Runs at raised TPL when the shell ExecutionBreak event fires. Do not allocate, do not block, do not call Boot Services that mutate state. Set a flag, log, return.
Functions
-
void axl_signal_install(AxlSignalHandler on_interrupt)
Install a handler fired on Ctrl-C. Overrides the default exit-on-interrupt behavior.
Passing NULL is equivalent to axl_signal_default(): no user handler, the runtime will exit cleanly at the next yield point.
-
void axl_signal_default(void)
Restore the default handler (auto-exit on next yield).
Equivalent to axl_signal_install(NULL) — named for readability.
-
bool axl_interrupted(void)
Poll the interrupted flag.
True between the break event firing and _axl_cleanup clearing it (which only happens as part of axl_exit). App code reads this to decide whether to keep working or unwind.
-
void axl_exit(int rc)
Terminate the app with guaranteed cleanup. Does not return.
Runs atexit callbacks (LIFO), sweeps the tier-1 resource registry, then calls gBS->Exit via the backend. This is the ONE blessed exit path — returning from main takes the same route via CRT0. App code that calls gBS->Exit directly, or aborts through some other path, bypasses cleanup and leaks firmware resources; don’t.
Convention: rc == 0 -> EFI_SUCCESS, any other value -> EFI_ABORTED.
AxlAtexit (LIFO cleanup callbacks)
Typedefs
-
typedef void (*AxlAtexitFn)(void *data)
axl-atexit.h:
POSIX-flavored cleanup registry. Callbacks registered via axl_atexit run in LIFO order (last-registered-first-run) during _axl_cleanup, before the tier-1 resource-registry sweep. Matches C’s atexit(3) contract and gives library consumers a place to release long-lived resources (top-level loops, HTTP clients, caches) without hand-threading cleanup through main’s tail.
static void free_app_state(void *data) { app_state_free((AppState *)data); } int main(int argc, char **argv) { AppState *s = app_state_new(); axl_atexit(free_app_state, s); return run(argc, argv, s); }
See docs/AXL-Lifecycle.md §4.3 for the design rationale. AxlAtexitFn:
Cleanup callback. data is the opaque pointer supplied at registration time. Runs on the main thread during _axl_cleanup.
Functions
-
uint32_t axl_atexit(AxlAtexitFn fn, void *data)
Register a callback to run during _axl_cleanup.
Callbacks fire in LIFO order on every exit path (return from main, axl_exit, or Ctrl-C through the default signal handler).
- Parameters:
fn – cleanup function
data – opaque user data passed to fn
- Returns:
non-zero handle on success, 0 on failure (fn is NULL or registration-time allocation failed).
-
void axl_atexit_remove(uint32_t handle)
Remove a previously-registered atexit callback.
Safe to call with handle==0 or with an already-removed handle (no-op in both cases). Useful when a resource is freed explicitly before exit and the atexit entry would otherwise run with a dangling pointer.
- Parameters:
handle – handle returned by axl_atexit
AxlRuntime (default loop, yield, registry inspection)
Typedefs
-
typedef struct AxlLoop AxlLoop
axl-runtime.h:
AXL runtime surface — the pieces an app interacts with around its own lifecycle and interruptibility. The runtime is the library code in src/runtime/ that powers the program lifecycle; the CRT0 entry stub (src/crt0/axl-crt0-native.c) invokes it at two boundaries:
_axl_init (before main): default loop singleton, shell-break notify, tier-1 resource registry, atexit registry.
_axl_cleanup (after main, or on axl_exit): drain atexit, sweep tier-1 leaks, free the default loop if created.
Full design and the runtime-vs-CRT0 split: docs/AXL-Lifecycle.md.
Related headers:
axl-signal.h Ctrl-C / interrupt handler API + axl_exit
axl-atexit.h POSIX-flavored cleanup callback registry
axl-loop.h Loop primitives, including axl_loop_iterate_until
Functions
-
AxlLoop *axl_loop_default(void)
Return the runtime’s default loop, lazy-creating on first call.
The default loop is owned by the runtime and freed during _axl_cleanup. Apps can run it directly (axl_loop_run(axl_loop_default())), add their own sources to it, or ignore it entirely and create private loops. axl_yield() dispatches immediately-ready work on this loop opportunistically.
- Returns:
the default loop, or NULL if allocation failed.
-
void axl_yield(void)
Cooperative yield point.
Call inside CPU-bound loops to keep the app interruptible. Cost is ~nanoseconds when the default loop is idle. Safe from any context except a raised-TPL notify handler.
Behavior:
If any immediately-ready work is pending on the default loop (timers, deferred callbacks), dispatch it — bounded to one iteration.
If Ctrl-C was observed during that dispatch, sets the interrupted flag so axl_interrupted() returns true.
Otherwise returns immediately.
-
void *axl_efi_find_config_table(const AxlGuid *guid)
Look up a UEFI Configuration Table entry by VendorGuid.
Walks the EFI System Table’s ConfigurationTable for an entry whose VendorGuid matches
guid. Returns the corresponding VendorTable pointer (typedvoid *— caller casts to the spec-defined struct type for the GUID).Common GUIDs and their published types:
EFI_ACPI_20_TABLE_GUID→ RSDPSMBIOS3_TABLE_GUID→ SMBIOS3 entry-point structEFI_SYSTEM_RESOURCE_TABLE_GUID→ ESRTEFI_DEBUG_IMAGE_INFO_TABLE_GUID→ debug image info
Modules with their own typed lookups (axl-acpi, axl-smbios) call this internally. New code that needs a one-shot lookup of an uncommon table (ESRT, MEMATTR, dmar, etc.) should use this directly instead of duplicating the configuration-table walk.
- Parameters:
guid – guid to match (NULL → returns NULL)
- Returns:
the matching VendorTable, or NULL if no match or
guidis NULL.
-
size_t axl_registry_count(void)
Return the number of tier-1 resources currently registered.
Purely informational — mostly useful in tests to verify resource-balancing. Returns 0 if the registry has not been initialized yet.
AxlApp (program invocation path)
Functions
-
const char *axl_app_argv0(void)
Return the program’s invocation path (argv[0]) as captured by the runtime at startup.
axl-app.h:
Application-runtime accessors. Today: argv[0] capture for tools that need their own invocation path (sidecar discovery, “where am
I” diagnostics). The runtime owns the parsed argv array; this header exposes read-only views of it that don’t get clobbered when subcommand dispatchers shift argv inside the program.
Tools that use axl_subcommand_dispatch see
argv[0]rewritten to the verb name inside each handler. To find the original program path (e.g. for loading a sidecar file from the binary’s directory), call axl_app_argv0 — it returns the value the runtime captured at startup, regardless of subsequent argv mutation.The returned pointer is owned by the runtime — never freed by the caller, valid until the runtime’s cleanup phase (which runs after
mainreturns). Stable across axl_subcommand_dispatch and similar argv-shifting helpers.Don’t call this from atexit handlers registered before runtime cleanup runs — the pointer is invalidated when
_axl_args_freefires during_axl_cleanup.- Returns:
invocation path, or NULL if the runtime received no arguments (extremely unusual — only happens on init OOM; the POSIX shim normally supplies at least argv[0] = “app” as a fallback).
-
const char *axl_app_image_path(void)
Return the canonical filesystem path of the running .efi image, decoded from EFI_LOADED_IMAGE_PROTOCOL.FilePath.
Orthogonal to axl_app_argv0. argv[0] reflects whatever the shell typed — often a basename when the user typed
app.efirather thanfs0:\app.efi, sometimes a full path, sometimes (in thestartup.nshinvocation case) just the name even when the script wrote the full path. The image path returned here is decoded from the device-path nodes UEFI used to actually find the binary, so it’s reliable regardless of how the shell was invoked.The right anchor for sidecar discovery (
pci-ids.json5, theclasses[]section ofpci-ids.json5,jedec.json5, etc.) — see axl_resolve_data_file, which prefers this over argv[0] when available.- Returns:
UTF-8 path string owned by the runtime, or NULL if the loaded-image protocol was unavailable or had no FILEPATH nodes (rare; only seen with synthetic load contexts that bypass the usual file-load path).
-
int axl_app_boot_path(const char *relative_path, char *out, size_t out_size)
Build a path on the boot volume the current image was loaded from.
Concatenates the volume prefix from
axl_app_image_path()(e.g."fs0:") withrelative_pathso the result is a fully-qualified path the rest of the AXL filesystem API understands. Convenience for tools that want to write logs / reports / sidecars next to their.efiwithout parsing the image path themselves.Path-separator and
\are normalized — both"crash-report.txt"and"\\crash-report.txt"produce e.g."fs0:\\crash-report.txt".- Parameters:
relative_path – path relative to boot-volume root (with or without leading ‘')
out – output buffer
out_size – capacity of
outin bytes
- Returns:
AXL_OK on success (
outis NUL-terminated); AXL_ERR if the image source has no filesystem prefix (network boot, RAM disk with no source volume), the output buffer is too small, orout/relative_pathis NULL.