AxlRuntime – CRT0-owned runtime glue

Runtime Glue – CRT0-owned init, signals, exit, atexit

The pieces an AXL app interacts with around its own lifecycle and interruptibility. CRT0 (src/crt0/axl-crt0-native.c) bridges the UEFI entry point to int main(int argc, char **argv) by calling _axl_init before main and _axl_cleanup after. This module owns those two bookends plus 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.

Four sub-modules, each a single concern:

  • axl-runtime.c_axl_init / _axl_cleanup, the singleton axl_loop_default(), and axl_yield.

  • axl-registry.c – tier-1 firmware-resource registry. Internal-only API (_axl_registry_*) called by the _new_impl / _free paths 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.caxl_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> – includes axl_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

axl_signal_install(fn)

Ask “did Ctrl-C happen yet?” from a CPU loop

axl_interrupted()

Exit with cleanup guaranteed (vs raw gBS->Exit)

axl_exit(rc)

Keep a tight CPU loop responsive to Ctrl-C

axl_yield() every N iters

Free a long-lived resource on any exit path

axl_atexit(fn, data)

Share a loop across modules

axl_loop_default()

Wait inside a callback without starving the outer loop

axl_loop_iterate_until(loop, done, timeout_us)

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:

  1. atexit callbacks (LIFO) – _axl_atexit_run_all. User callbacks may free resources that would otherwise show up in the sweep.

  2. argv strings_axl_args_free in src/posix/axl-app.c.

  3. 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).

  4. Tier-1 registry sweep_axl_registry_sweep. LIFO walk of live entries, each logged with user file:line and closed via the appropriate _free.

  5. Heap leak report (AXL_MEM_DEBUG only) – axl_mem_dump_leaks. Release-mode auto-free of heap is deferred (see docs/AXL-Runtime.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

API Reference

AxlSignal (interrupts + blessed exit)

Typedefs

typedef void (*AxlSignalHandler)(void)

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) &#8212; 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 &#8212; 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)

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

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:

  1. If any immediately-ready work is pending on the default loop (timers, deferred callbacks), dispatch it &#8212; bounded to one iteration.

  2. If Ctrl-C was observed during that dispatch, sets the interrupted flag so axl_interrupted() returns true.

  3. Otherwise returns immediately.

size_t axl_registry_count(void)

Return the number of tier-1 resources currently registered.

Purely informational &#8212; mostly useful in tests to verify resource-balancing. Returns 0 if the registry has not been initialized yet.