AxlService — Long-Running Services
AxlService
Structured-lifecycle wrapper over AxlLoop. A service is a
long-running event loop with a setup callback that builds
sources/timers/handlers, a teardown that releases them, and an
options descriptor that auto-applies values into a consumer struct
via offsetof.
This is not a Unix daemon — UEFI has no fork/setsid/chdir/
umask. AxlService is closer in shape to a systemd unit: a thing
with a defined lifecycle, supervised by an external dispatcher
(the shell’s foreground caller, or the firmware’s notify-timer in
driver mode).
Shape
AxlService is a DXE driver. The consumer’s main() is a launcher
/ supervisor; the actual service body — setup, the long-running
loop, teardown — lives in a separate driver .efi image,
typically embedded into the foreground via .incbin.
#include <axl.h>
typedef struct { uint16_t port; bool verbose; } MyOpts;
static MyOpts opts;
static const AxlConfigDesc opts_descs[] = {
{ "port", AXL_CFG_UINT, "8080", "Port",
offsetof(MyOpts, port), sizeof(uint16_t) },
{ "verbose", AXL_CFG_BOOL, "false", "Verbose",
offsetof(MyOpts, verbose), sizeof(bool) },
{ 0 }
};
static int my_setup(AxlLoop *loop, void *user) {
/* build sources against `loop`, e.g. axl_http_server_start */
return AXL_OK;
}
static int my_teardown(void *user) { /* release */ return AXL_OK; }
static const AxlService my_service = {
.name = "my-service",
.opts_descs = opts_descs,
.setup = my_setup,
.teardown = my_teardown,
.user = &opts,
.driver_tick_ms = 50,
};
/* Identity GUID is derived from `.name` via axl_guid_v5 — no
uuidgen required. Same name in launcher + driver image →
same derived GUID. */
The driver image’s only line is the AXL_SERVICE_DRIVER macro:
/* my-service-dxe.c, compiled as the embedded driver */
#include <axl.h>
#include "shared.h" /* same my_service descriptor as the foreground */
AXL_SERVICE_DRIVER(my_service);
Driver-tick mode (raw). Used by the macro to attach the service to the firmware notify-timer:
AxlLoop *loop = axl_loop_new();
axl_service_attach_driver(loop, &my_service, 50); /* 50 ms tick */
/* ...firmware drives the loop until... */
axl_service_detach_driver(loop, &my_service);
axl_loop_free(loop);
Direct calls are uncommon — AXL_SERVICE_DRIVER covers the
DriverEntry/Unload boilerplate.
Embedded-driver launch (foreground side). Foreground app
ships the driver image as an embedded blob (via axl-cc --embed
or the Makefile’s EMBED_BLOB), serializes its current options
to LoadOptions, and axl_service_start_embedded hands them to
the driver. AXL_EMBED_* hides the .incbin symbol-naming
convention:
AXL_EMBED_DECLARE(my_driver); /* from <axl/axl-embed.h> */
/* AXL_EMBED_SIZE is a runtime pointer subtraction (not a constant
expression), so build the deploy at runtime rather than as a
static const initializer. The protocol GUID lives on the inner
AxlService — both binaries share the same descriptor, so they
agree on identity by construction. */
AxlServiceDeploy d = {
.service = &my_service,
.driver_blob = AXL_EMBED_DATA(my_driver),
.driver_blob_len = AXL_EMBED_SIZE(my_driver),
.driver_name = "my-service-dxe.efi",
};
if (axl_service_is_running(&d)) {
axl_printf("Already running\n");
return 0;
}
return axl_service_start_embedded(&d);
Build:
axl-cc --embed my-service-dxe.efi=my_driver launch.c -o launch.efi
The symmetric stop verb is axl_service_stop(&deploy) — resolves
the running image’s handle via the protocol GUID and unloads it.
AXL_SERVICE_DRIVER publishes the GUID on the driver image’s own
handle (not a sentinel), so stop’s LocateHandleBuffer returns
the image handle directly. Stop is idempotent — calling it on a
not-running deploy returns AXL_OK without doing anything.
return axl_service_stop(&deploy);
The driver image’s DriverEntry is one line — AXL_SERVICE_DRIVER
emits the boilerplate (decode LoadOptions magic-prefixed UTF-8
query string back into opts via axl_config_from_string, create
loop, attach):
#include <axl.h>
/* same opts_descs and my_service descriptor as the foreground side.
Tick period comes from my_service.driver_tick_ms (0 = 50 ms
default); the macro takes only the descriptor. */
AXL_SERVICE_DRIVER(my_service);
Setup-failure contract
setup owns its own unwind on failure (axl_free anything it
allocated, axl_protocol_unregister anything it published). The
framework calls teardown only after a successful setup.
This rule is the same in foreground and driver modes.
Held-protocol hazard
Any UEFI protocol the setup callback opens with OpenProtocol
(including the implicit opens that axl_http_server_start,
axl_tcp_listen, etc. do via service-binding) MUST be closed
before the matching teardown returns. The firmware’s
post-callback refcount check at the end of gBS->UnloadImage
will refuse the unload with EFI_ACCESS_DENIED otherwise —
making the service un-stoppable for the lifetime of the
process. axl_service_stop will return AXL_ERR and the SDK
will log:
axl_driver_unload: UnloadImage(handle=0x...) returned
EFI_ACCESS_DENIED (0x800000000000000F) — image still holds open
protocol references; if your service opens UEFI protocols in
setup, ensure teardown closes every one (axl_http_server_free,
axl_tcp_close, etc.) before returning
The SDK’s wrappers (axl_http_server_free, axl_tcp_close,
etc.) close the protocols they opened. If your setup uses
OpenProtocol directly, your teardown owns the matching
CloseProtocol.
Teardown is invoked at two sites — all visible
The teardown callback fires from one of:
axl_service_attach_driver’s failure path (rolls back setup)AXL_SERVICE_DRIVER’s unload stub afteraxl_service_detach_driver
Each call goes through axl_service_teardown, which logs
teardown ENTER / teardown EXIT rc=N at debug level so
consumers can confirm without adding their own printfs. Bump
AXL_LOG_LEVEL=debug to see the trace.
axl_service_detach_driver does NOT call teardown — the caller
follows up with axl_service_teardown explicitly. This is the
P1 contract change; previously detach_driver invoked teardown
internally, which made the macro unreadable (no literal
teardown(...) call appeared in the unload stub source) and
silently skipped teardown if axl_loop_detach_driver returned
ERR.
Cross-binary ABI tripwire
When the same static const AxlService descriptor is initialized
in two binaries (foreground app + embedded driver image), the
option struct’s layout MUST match across both. Build both from
the same source tree with identical compile flags (AXL_TLS,
AXL_MEM_DEBUG, arch). The Makefile’s AXL_TLS toggle
detection wipes AXL-internal stale objects automatically; it
cannot see consumer-side struct shifts.
The wire format prefixes each LoadOptions payload with the
8-byte magic AXLSVC1\0 so a shell-launched UCS-2 LoadOptions
buffer doesn’t get misparsed as the AXL UTF-8 format. The
driver-image macro logs and falls back to descriptor defaults
on missing-magic.
See sdk/examples/service-demo.c for the one-line AXL_SERVICE
shape and sdk/examples/service-demo-custom.c for a hand-written
main() mixing the standard verbs with consumer-specific verbs.
<axl/axl-service.h> has the full API doxygen.
API Reference
Defines
-
AXL_SERVICE_DEFAULT_TICK_MS
Default driver-tick dispatch period (milliseconds).
Used when AxlService.driver_tick_ms is 0. 50 ms balances loop responsiveness against the firmware notify-timer’s overhead.
Typedefs
-
typedef int (*AxlServiceSetup)(AxlLoop *loop, void *user)
Setup callback — builds sources, timers, handlers against
loop.axl-service.h:
Driver-shaped lifecycle wrapper over AxlLoop. An AxlService is the typed shape for a long-running event loop: a setup callback that builds sources/timers/handlers, a teardown that releases them, and an options descriptor that auto-applies into the consumer’s option struct via offsetof.
AxlService is a DXE driver. Long-running work in UEFI lives in driver images — foreground apps die when their main returns. AxlService doesn’t try to hide that; it embraces it. There is no in-process foreground “run” path. The consumer’s main() is a launcher / supervisor, not the body of the service.
Two operational entry points:
axl_service_attach_driver(loop, &svc) Driver-tick mode. Used by the AXL_SERVICE_DRIVER macro to attach the service to the firmware notify-timer. Tick period comes from
svc->driver_tick_ms(0 = 50 ms default). Direct use is rare — the macro covers the common case.axl_service_start_embedded(deploy) Foreground-side: load an embedded driver image carrying this service via .incbin, serialize the foreground’s options into LoadOptions, pass to the driver. The driver’s DriverEntry (built with AXL_SERVICE_DRIVER) decodes them on entry. Pair with axl_service_stop.
Cross-binary ABI tripwire. When the same service descriptor is initialized in two binaries (foreground app + driver image), the option struct’s layout must match across both. Build both from the same source tree with identical compile flags (AXL_TLS, AXL_MEM_DEBUG, arch). The Makefile’s AXL_TLS state detection only catches AXL-internal struct shifts; it cannot see consumer struct shifts caused by toggling the consumer’s own conditional fields.
Called once at service startup, BEFORE the loop starts dispatching. Receives the same
loopthe service will run on, so the consumer can add accept/recv/timer sources directly.Held-protocol hazard. Any UEFI protocol the setup callback opens with
OpenProtocol(including the implicit opens thataxl_http_server_start,axl_tcp_listen, etc. do via service-binding) MUST be closed before the matching teardown returns, or the firmware’s post-callback refcount check will refusegBS->UnloadImagewithEFI_ACCESS_DENIED. This makes the service un-stoppable for the lifetime of the process; the consumer’saxl_service_stopwill fail with the SDK warning pointing at this exact case. The SDK’s wrappers (axl_http_server_free,axl_tcp_close, etc.) close the protocols they opened — if your setup usesOpenProtocoldirectly, your teardown owns the matchingCloseProtocol.Failure contract: on AXL_ERR return, the consumer is responsible for unwinding any partial state (axl_free, axl_protocol_unregister, etc.). The framework will NOT call teardown — teardown only fires after a successful setup.
- Param loop:
the event loop the service runs on
- Param user:
the descriptor’s user pointer (typically the consumer’s options struct, populated by AxlConfig auto-apply)
- Return:
AXL_OK if the service is ready to dispatch; AXL_ERR on any setup failure (consumer has already cleaned up).
-
typedef int (*AxlServiceTeardown)(void *user)
Teardown callback — releases what setup built.
Called at service shutdown ONLY if setup returned AXL_OK. Receives the same user pointer setup got. Return value is logged but doesn’t affect the framework’s teardown sequence.
Must close every protocol setup opened. See the “Held-protocol hazard” note on AxlServiceSetup. The framework cannot detect a missed
CloseProtocol; the symptom isaxl_service_stopreturning AXL_ERR with the SDK’s EFI_ACCESS_DENIED warning at axl-driver.c.
Functions
-
int axl_service_guid(const AxlService *svc, AxlGuid *out)
Resolve a service’s UEFI protocol identity GUID.
AxlService publishes its identity by deriving a deterministic GUID from
svc->namevia axl_guid_v5 with the AXL_SERVICE namespace. Consumers normally don’t need this — axl_service_is_running, axl_service_stop, and the AXL_SERVICE_DRIVER macro do the derivation internally — but if you want to callLocateProtocolfrom outside AxlService (e.g. to publish a service-specific child interface on the same handle), use this to get the same GUID those code paths use.- Returns:
AXL_OK on success (
outpopulated); AXL_ERR ifsvcis NULL,svc->nameis NULL, oroutis NULL.
-
int axl_service_attach_driver(AxlLoop *loop, const AxlService *svc)
Attach a service to firmware-tick dispatch on the supplied loop.
Calls setup(loop, svc->user) then axl_loop_attach_driver. The tick period comes from
svc->driver_tick_ms; if that field is 0, AXL_SERVICE_DEFAULT_TICK_MS (50 ms) applies. Returns immediately — the firmware notify-timer drives the loop from here. Pair with axl_service_detach_driver from DriverUnload.The caller (typically the AXL_SERVICE_DRIVER macro) owns the loop; this function does NOT free it on failure — the caller must axl_loop_free if attach fails.
- Returns:
AXL_OK if setup + attach both succeeded; AXL_ERR otherwise.
-
int axl_service_detach_driver(AxlLoop *loop, const AxlService *svc)
Detach a service’s firmware-tick dispatch.
Cancels the periodic notify-timer the matching axl_service_attach_driver installed. Returns AXL_ERR if the loop was never attached.
Does NOT call svc->teardown. That’s the caller’s responsibility — invoke axl_service_teardown after this returns AXL_OK if setup ran. (The contract changed under P1: previously detach_driver also called teardown internally, but the call was hidden inside detach_driver’s success path which made the macro unread-able and silently skipped teardown if detach_driver gained any new failure mode. Splitting makes each function’s responsibility narrow.)
The AXL_SERVICE_DRIVER macro’s unload stub does this in sequence; consumers using
axl_service_attach_driverdirectly must mirror it.Do NOT call this if axl_service_attach_driver returned AXL_ERR.
The attach-failure path already invoked teardown internally; calling detach + teardown manually would re-fire teardown. The safe pattern is “only detach what attach succeeded
for.”
- Parameters:
svc – accepted but currently only NULL-checked; reserved for future per-service detach work and kept in the signature for source-compat
-
int axl_service_teardown(const AxlService *svc)
Invoke a service’s teardown callback with consistent logging.
Single source of truth for teardown invocation across the SDK:
axl_service_attach_driver'sfailure paththe AXL_SERVICE_DRIVER macro’s unload stub after detach
Logs
teardown ENTER/teardown EXIT rc=Nat debug level, promotes a non-OK rc to a warning. Safe to call withsvc== NULL orsvc->teardown== NULL — both return AXL_OK without invoking anything.Consumers normally don’t call this directly — the three public sites above invoke it. Public so consumers rolling their own dispatch (custom firmware-tick driver, alternative deploy mode) can match the framework’s logging shape.
Idempotency is the consumer’s responsibility. Calling teardown twice on a setup that wasn’t designed for it is undefined behavior (axl_free of an already-freed handle, etc.).
- Returns:
The teardown callback’s return code (AXL_OK if no callback or
svcis NULL). The AXL_SERVICE_DRIVER macro propagates this into the EFI_STATUS gBS->UnloadImage sees, so a teardown failure surfaces to the firmware rather than getting absorbed into a “successful” unload.
-
int _axl_service_driver_init(void *image_handle, void *system_table, const AxlService *svc)
Internal driver-image entry. Implements everything the AXL_SERVICE_DRIVER macro used to inline: backend init, LoadOptions decode, protocol publish, loop creation, attach. Library-side per-image static state holds
svc, the loop, the cfg, and the published handle for the matching unload.Not user API — call AXL_SERVICE_DRIVER instead.
- Parameters:
image_handle – EFI_HANDLE for this driver image
system_table – EFI_SYSTEM_TABLE *
svc – service descriptor (non-NULL)
- Returns:
EFI_STATUS suitable to return from a firmware DriverEntry.
-
int axl_service_start_embedded(const AxlServiceDeploy *deploy)
Launch the embedded driver image carrying this service.
Serializes the foreground app’s currently-set option values via axl_config_to_string, then calls axl_driver_ensure_with_embedded(…) with that payload as LoadOptions. The driver image’s DriverEntry (built with AXL_SERVICE_DRIVER) decodes the payload back into its own AxlConfig and the framework auto-applies it into svc->user.
Callers wanting
argv -> svc->useroverlay before launch must populate*deploy->service->userthemselves. Two patterns:Standard verbs only: axl_service_main packages argv-parse + populate + start in one call (and the AXL_SERVICE convenience macro emits a one-line
main()that calls it). This is what most consumers want.Custom verb tree: copy AxlArgs values into the user struct yourself via
axl_args_get_uint/_bool/_stringfrom your verb handler, then call this. STRING pointers returned byaxl_args_get_stringlive at least until your verb handler returns — call this BEFORE return so the serialize pass reads them while still valid. Seesdk/examples/service-demo-custom.cfor the worked example.
The AxlConfig path (
axl_config_new(descs, NULL, user)+axl_config_from_stringper argv element) also works and adds default-application + STRING strdup ownership; reach for it when the consumer is already AxlConfig-shaped.If the deploy’s protocol GUID is already published when this is called (e.g. a previous launch is still active), AXL’s ensure_with_embedded short-circuits and this returns AXL_OK without re-loading. Use axl_service_is_running to detect that case explicitly if your CLI wants to report “already serving”.
- Returns:
AXL_OK if the protocol is registered (was already, or after loading); AXL_ERR on serialize overflow, deploy descriptor incomplete, or driver-load failure.
-
int axl_service_stop(const AxlServiceDeploy *deploy)
Stop a running service launched via axl_service_start_embedded.
Resolves the running driver image’s handle by looking up the GUID derived from
deploy->service->nameviaLocateHandleBuffer, then callsaxl_driver_unloadon each match. The driver image’s AXL_SERVICE_DRIVER unload stub fires synchronously inside UnloadImage and runs the framework’s teardown sequence in this order:axl_loop_detach_driver — stops the firmware notify-timer
service teardown(user) — releases what setup built
axl_protocol_unregister_guid — removes the marker protocol
axl_config_free — drops cached LoadOptions strings
axl_loop_free
Idempotent: if the protocol isn’t currently published (the service was never launched, or was already stopped), returns AXL_OK without doing anything. Use axl_service_is_running first if you want to differentiate “stopped it” from “wasn’t
running.”
Dangling-interface hazard. UEFI doesn’t ref-count protocols. If a third-party consumer obtained
service->user(or any other interface) viaLocateProtocoland held the pointer past the stop call, that pointer dangles. The same hazard applies to the shell’sunload -nand is fundamental to UEFI’s lifetime model. Callers that publish service interfaces meant for live cross-image consumption should advertise their stop semantics to those consumers (e.g. a marker GUID change or an explicit “I am about to unload” event) before calling stop.- Returns:
AXL_OK on success (or already-stopped); AXL_ERR if
deployordeploy->serviceis NULL, or any axl_driver_unload call returned an error. If multiple handles publish the GUID, all are unloaded; rc reflects whether any of them failed (the loop continues on individual failures rather than aborting on the first — partial cleanup beats leaking the rest).
-
bool axl_service_is_running(const AxlServiceDeploy *deploy)
Predicate: is this deploy’s service currently running?
Internally derives the service GUID from
deploy->service->nameand callsLocateProtocol— succeeds if any image (firmware-shipped or a previousaxl_service_start_embeddedof this deploy) has published the protocol. Useful before axl_service_start_embedded to differentiate “started fresh” from “was already running.”- Returns:
true if a previous launch is still publishing the protocol.
-
int axl_service_supervise(const AxlServiceDeploy *deploy)
Block on the default loop, then stop the service.
The standard supervise-and-stop body: blocks on
axl_loop_defaultuntilaxl_loop_runreturns (Ctrl-C,axl_loop_quit, or any source removing itself with no others left), then calls axl_service_stop. Returns process-exit-shapedint(0 on clean shutdown, 1 on stop failure or unexpected loop error).Intended for consumers that want the standard supervise body but still need to register their own loop sources (ESC key handler, a status-print timer, etc.) before blocking. Add those to
axl_loop_defaultfirst; call this once you’re ready to block:axl_service_main uses this internally; consumers writing their ownint rc = axl_service_start_embedded(&deploy); if (rc != AXL_OK) return 1; if (detach) return 0; axl_loop_add_key_press(axl_loop_default(), esc_handler, NULL); return axl_service_supervise(&deploy);
main()can compose this directly instead of duplicating the loop-run / stop / rc-translate dance.- Returns:
0 if
axl_loop_runquit cleanly (rc 0 or -1 from a Ctrl-C break) ANDaxl_service_stopsucceeded; 1 otherwise.
-
int axl_service_main(const AxlServiceDeploy *deploy, int argc, char **argv)
Standard main() body for a service consumer.
Builds a default
axl_args_runverb tree (launch [--detach],stop,status) fromdeployand dispatches argv. Thelaunchverb populatesdeploy->service->userfrom the parsed args (synthesizing anAxlArgDesc[]fromsvc->opts_descsso the consumer doesn’t repeat the descriptor in two formats), calls axl_service_start_embedded, and either:exits if
--detachwas passed (driver continues to run);blocks on
axl_loop_defaultuntil Ctrl-C, then calls axl_service_stop on its way out.
Most service consumers don’t need their own
main()— the AXL_SERVICE macro emits one for you that calls this. Direct use of axl_service_main is for consumers who want to mix the default verbs with their own (extra verbs, custom help prolog, etc.) — they wireaxl_args_runthemselves and dispatch the standard verbs into axl_service_main.- Parameters:
deploy – deploy descriptor (service + driver blob)
argc – argc from main()
argv – argv from main()
- Returns:
process-exit-shaped int: 0 on success, 1 on failure (matching
axl_args_runconventions).
-
struct AxlService
- #include <axl-service.h>
Service descriptor. Static-initializable; safe to share by reference between foreground app and driver image (same source tree, identical compile flags).
The service’s UEFI protocol identity is derived from
namevia axl_guid_v5 with a fixed AXL_SERVICE namespace — consumers don’t allocate a UUID per service. The AXL_SERVICE_DRIVER macro auto-publishes the derived GUID on the driver image’s handle when it starts, which givesaxl_driver_ensure_with_embedded(called from axl_service_start_embedded) something to verify against AND gives axl_service_is_running a way to detect a live deploy. Two binaries that share the same name string see the same derived GUID by construction.Keep all fields trivially copyable — the descriptor is read in both binaries’ startup paths and any pointer ABI shift is hard to diagnose. AxlConfig descriptors are themselves static const.
Public Members
-
const char *name
short identifier, e.g. “axl-webfs”. REQUIRED — not just a log label. AxlService derives the service’s UEFI protocol identity from this via axl_guid_v5 with a fixed AXL_SERVICE namespace, so the name uniquely picks out a service. Two binaries that share the same source tree’s name string see the same derived GUID by construction.
-
const AxlConfigDesc *opts_descs
NULL = no configurable options.
-
AxlServiceSetup setup
required (must not be NULL)
-
AxlServiceTeardown teardown
NULL = nothing to release.
-
void *user
-> caller’s options struct; AxlConfig auto-applies into here
-
uint64_t driver_tick_ms
driver-mode dispatch period in milliseconds; 0 means use the 50 ms default. Single source of truth for both axl_service_attach_driver and the AXL_SERVICE_DRIVER macro.
-
const char *name
-
struct AxlServiceDeploy
- #include <axl-service.h>
Cross-binary deployment glue. Combines a service descriptor (the part both binaries share) with the embedded driver image and disk-search filename (foreground-only fields).
The driver image links the same
servicedescriptor; the foreground app additionally fills in the blob fields. Protocol identity is derived fromservice->namevia axl_service_guid in both binaries — they see the same GUID because they share the descriptor (and therefore the name string).Public Members
-
const AxlService *service
shared descriptor
-
const unsigned char *driver_blob
embedded driver .efi bytes
-
size_t driver_blob_len
length in bytes
-
const char *driver_name
filename for disk-search fallback
-
const AxlService *service