AxlSys — System Utilities

System operations, environment variables, time, NVRAM storage, boot-option management, x86 I/O port access, driver lifecycle, hex dump, configuration framework (including command-line parsing), and path manipulation.

Headers:

  • <axl/axl-sys.h> — System operations (reset, GUID, device map refresh)

  • <axl/axl-env.h> — Environment variables and working directory

  • <axl/axl-time.h> — Wall-clock time and monotonic timestamps

  • <axl/axl-nvstore.h> — Portable NVRAM key-value storage

  • <axl/axl-boot.h> — Boot-option management (Boot####/BootOrder/BootNext/BootCurrent)

  • <axl/axl-port.h> — x86 I/O port access (in/out)

  • <axl/axl-driver.h> — Driver binding and lifecycle

  • <axl/axl-image.h> — Executable-image lifecycle (load/start/unload)

  • <axl/axl-mem-phys.h> — Physical-memory map/unmap + one-shot read/write

  • <axl/axl-watchdog.h> — Boot-services watchdog control

  • <axl/axl-rng.h> — Cryptographic random bytes

  • <axl/axl-diag.h> — Tool diagnostic helpers (-v output)

  • <axl/axl-hexdump.h> — Hex/ASCII dump formatting

  • <axl/axl-config.h> — Unified configuration + command-line parsing

  • <axl/axl-path.h> — Path manipulation

The event/cancellable/wait primitives previously listed here now live in src/event/ — see src/event/README.md.

System Utilities

GUIDs

UEFI identifies protocols, variables, and services by 128-bit GUIDs. AXL provides AxlGuid (standard C types, no UEFI headers needed) and the AXL_GUID macro for initialization:

AxlGuid my_guid = AXL_GUID(0x12345678, 0xabcd, 0xef01,
    0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01);

if (axl_guid_cmp(&a, &b) == 0) {
    // GUIDs are equal
}

Firmware Globals

After AXL_APP or axl_driver_init, these globals are available (typed when <uefi/axl-uefi.h> is included):

  • gST — System Table (EFI_SYSTEM_TABLE *)

  • gBS — Boot Services (EFI_BOOT_SERVICES *)

  • gRT — Runtime Services (EFI_RUNTIME_SERVICES *)

  • gImageHandle — handle of the running application or driver

NVRAM Variables

Portable key-value storage backed by firmware variables, organized by namespace. Built-in namespaces are "global" (spec UEFI Global Variable GUID, e.g. SecureBoot, BootOrder, Boot####) and "app" (per-app GUID for application settings).

// Read
uint8_t secure_boot;
size_t sz = sizeof(secure_boot);
if (axl_nvstore_get("global", "SecureBoot", &secure_boot, &sz) == 0) {
    axl_printf("SecureBoot: %s\n", secure_boot ? "on" : "off");
}

// Write
axl_nvstore_set("app", "last-run", timestamp, timestamp_len,
                AXL_NV_PERSISTENT | AXL_NV_BOOT);

For variable-length values (NV strings, OEM blobs of unknown size), axl_nvstore_get_alloc does the probe-allocate-read dance for you and hands back a heap buffer the caller frees with axl_free. The buffer is allocated needed + 1 bytes with the trailing byte zeroed, so a string-shaped variable can be dereferenced as NUL-terminated even if the wire payload omitted the NUL:

void   *buf;
size_t  sz;
if (axl_nvstore_get_alloc("global", "PlatformLang", &buf, &sz) == 0) {
    axl_printf("PlatformLang = '%s' (%zu bytes)\n", (char *)buf, sz);
    axl_free(buf);
}

get_alloc returns -1 for missing variables (and for backends that allow 0-byte values, succeeds with sz == 0 and a 1-byte NUL allocation; UEFI’s SetVariable(size=0) means “delete” so the empty path doesn’t surface there).

Vendor Namespaces

Vendor variables (Dell/HPE/Lenovo OEM keys) plug in via namespace registration so consumer call sites stay UEFI-free — they reference namespaces by name only. The backend token is opaque (a const AxlGuid * on UEFI; on a future Linux backend it could be a path prefix):

extern const AxlGuid AXL_OEM_VENDOR_GUID;  // declared per-vendor
axl_nvstore_register_namespace("oem", &AXL_OEM_VENDOR_GUID);
axl_nvstore_get("oem", "AssetTag", buf, &sz);

Other operations: axl_nvstore_delete, axl_nvstore_iter (walk all keys in a namespace), axl_nvstore_get_attrs (read AXL_NV_* flags without reading the value).

Boot Options

Typed wrappers over the Boot####/BootOrder/BootNext/BootCurrent firmware-variable family. The EFI_LOAD_OPTION wire codec stays internal to AxlBoot — consumers operate on AxlBootOption structs:

AxlBootOption opt;
if (axl_boot_option_get(0x0001, &opt) == 0) {
    axl_printf("Boot0001: %s\n  path: %s\n",
               opt.description, opt.device_path ?: "(unknown)");
    axl_boot_option_free(&opt);
}

uint16_t *order;
size_t    n;
if (axl_boot_order_get(&order, &n) == 0) {
    for (size_t i = 0; i < n; i++) {
        axl_printf("  %zu: Boot%04X\n", i, order[i]);
    }
    axl_free(order);
}

Set/delete options (_option_set, _option_delete), reorder boot sequence (_order_set), or arm a one-shot (_next_set / _next_clear). Encoding device paths to/from text uses the firmware’s EFI_DEVICE_PATH_TO_TEXT_PROTOCOL / _FROM_TEXT_PROTOCOL_set returns -1 if the from-text protocol isn’t published.

x86 I/O Ports

Public wrappers around in/out for legacy hardware that hasn’t moved to MMIO (CMOS, SuperIO, IPMI KCS, port-based ACPI PM blocks):

#if defined(__x86_64__) || defined(__i386__)
uint8_t v = axl_io_port_read8(0x70);
axl_io_port_write8(0x71, v | 0x80);
#endif

Build-gated to x86 — calls compile out on AArch64, so wrong-arch usage surfaces as a link error rather than a silent runtime no-op. 8/16/32-bit variants for read and write.

Physical-Memory Access

For tools that scan ROM regions, peek at MMIO control registers, or search firmware tables. The _map/_unmap pair is the held abstraction; one-shot _read{8,16,32,64} / _write{8,16,32,64} helpers cover the typical “I just want one byte” case without boilerplate. UEFI is identity-mapped so map is effectively a no-op; the abstraction exists for portability — a future Linux backend would mmap("/dev/mem") on the way in.

// Held mapping over multiple accesses.
void *va;
if (axl_mem_phys_map(0xFEE00000, 4096, &va) == 0) {
    uint32_t apic_id = *(volatile uint32_t *)((uint8_t *)va + 0x20);
    axl_mem_phys_unmap(va, 4096);
}

// One-shot read.
uint32_t signature;
axl_mem_phys_read32(0xE0000, &signature);

axl_mem_phys_search does a byte-by-byte scan for a needle within a mapped region — useful for finding signatures inside firmware blobs.

Watchdog

UEFI starts every loaded image with a 5-minute boot-services watchdog (UEFI 2.11 §7.5). Long-running diagnostics get killed without warning unless they take action:

// Disable entirely (typical for diagnostics that exceed 5 min).
axl_watchdog_disarm();

// Or extend without disabling protection.
axl_watchdog_set(900);  // 15 minutes
// ... long-running work ...
axl_watchdog_pet();     // re-arm to the same window

Random Bytes

Thin wrapper over EFI_RNG_PROTOCOL (UEFI 2.11 §37.5). The protocol is published by most modern firmware on platforms with an entropy source (RDRAND on x86, an SBSA TRNG on aa64). Returns -1 if the protocol isn’t installed — consumers that need a deterministic fallback layer their own.

uint8_t nonce[16];
if (axl_rng_bytes(nonce, sizeof(nonce)) != 0) {
    // RNG not available — bail or fall back
}

Driver Lifecycle

Build DXE drivers with axl-cc --type driver. The driver entry point is DriverEntry (not main). Call axl_driver_init to set up the AXL runtime:

EFI_STATUS EFIAPI DriverEntry(EFI_HANDLE ImageHandle,
                               EFI_SYSTEM_TABLE *SystemTable) {
    axl_driver_init(ImageHandle, SystemTable);
    axl_printf("Driver loaded\n");
    // ...
}

See sdk/examples/driver.c for a complete example.

For long-running services (cross-binary marshalling, structured setup/teardown, foreground or driver-tick deployment), see AxlService — the lifecycle wrapper over AxlLoop that composes axl-driver + axl-config + axl-loop.

Image Lifecycle

For loading and running arbitrary EFI images (not DXE drivers), use axl_image_*:

AxlImage *img;
if (axl_image_load("fs0:\\boot\\hello.efi", &img) == 0) {
    int exit_code = 0;
    axl_image_start(img, &exit_code);
    axl_image_unload(img);
}

The handle is opaque — EFI_HANDLE and EFI_LOADED_IMAGE_PROTOCOL never cross the public API. axl_image_* is a thin wrapper over axl_driver_* (which already handles path-to-device-path construction and the device-path / buffer load fallback); the only distinct piece is axl_image_start, which captures the image’s exit status (axl_driver_start discards it because drivers aren’t expected to exit cleanly). Forward slashes in the path are normalized to backslashes.

Image Signature Inspection

For “is this PE file signed and does its signature validate?” checks without committing to launching the image, use <axl/axl-image-verify.h>:

AxlImageSignatureInfo info = {0};
if (axl_image_verify_signature("fs0:\\boot.efi",
                               /* consult_db = */ true,
                               &info) == 0) {
    if (!info.has_signature) {
        axl_print("UNSIGNED\n");
    } else if (info.consulted_db && !info.signature_valid) {
        axl_print("SIGNATURE INVALID against current Secure Boot db\n");
    } else {
        axl_print("SIGNED%s by '%s' (issued by '%s')\n",
                  info.consulted_db ? " (db-validated)" : " (presence only)",
                  info.subject_cn != NULL ? info.subject_cn : "(unknown)",
                  info.issuer_cn  != NULL ? info.issuer_cn  : "(unknown)");
    }
    axl_image_signature_info_free(&info);
}

The presence axis (has_signature) is a pure file-bytes parse of the PE Certificate Table — no firmware dependencies. The validity axis (signature_valid + consulted_db) opts into a firmware dry-run via LoadImage(SourceBuffer) + immediate UnloadImage, which fires EFI_SECURITY2_ARCH_PROTOCOL callbacks (audit logs, PCR measurement, dbx notifications) as a side effect — pass consult_db = false when those side effects matter. The subject_cn / issuer_cn fields populate from the first certificate in the PKCS#7 SignedData bundle via an in-tree DER walker; they’re best-effort diagnostic strings (the formal way to identify the Authenticode signer is via SignerInfo’s IssuerAndSerial — out of scope for diagnostic CN output).

Protocol Registry

What UEFI Means by “Protocol”

A UEFI protocol is not a wire protocol or a network spec — it’s the closest thing UEFI has to an object or a vtable. Concretely, a protocol is:

  • a C struct of function pointers (and sometimes inline state),

  • identified by a 128-bit GUID,

  • installed on a handle (a firmware-allocated opaque token that represents some logical entity — a disk, a network port, a driver image, a service endpoint, etc.).

Consumers find a protocol by GUID via the LocateProtocol or LocateHandleBuffer Boot Services calls; the firmware returns the struct pointer; the consumer calls the function pointers it carries.

Mental-model translation if you come from elsewhere:

  • Java / C# / Swift: a UEFI protocol is roughly an interface bound to a specific instance — except identity is a GUID instead of a class type, and instances are handles instead of objects.

  • COM: very similar to a COM interface — IID-keyed vtable on a handle. UEFI’s design lineage is COM-via-IntelBIOS.

  • POSIX: there’s no clean parallel. The closest analog is “a device-driver struct file_operations registered in a kobject hierarchy keyed by a UUID instead of a path.”

The naming is awkward and we’re stuck with it because that’s what the UEFI spec writes everywhere. AXL’s protocol registry is a name-keyed wrapper over this UEFI-native concept: instead of shipping a GUID literal at every call site, consumers pass a string name and the registry resolves it. Internally it still calls InstallProtocolInterface / LocateProtocol — there is no extra runtime cost, just less boilerplate and fewer GUIDs to copy-paste.

Using the Registry

Built-in well-known names cover the spec-defined protocols a portable consumer typically reaches for: "smbios", "shell", "simple-network", "simple-fs", "device-path", "loaded-image", "ram-disk", the IPv4 networking family ("tcp4", "tcp4-sb", "ip4", "ip4-config2", "dhcp4", "dhcp4-sb", "dns4", "dns4-sb"), and "tcg2".

// Find a protocol (consumer side)
EFI_SMBIOS_PROTOCOL *smbios;
if (axl_protocol_find("smbios", (void **)&smbios) == AXL_OK) {
    // ...
}

// Enumerate all handles publishing a protocol
void   **handles;
size_t   count;
if (axl_protocol_enumerate("simple-fs", &handles, &count) == AXL_OK) {
    for (size_t i = 0; i < count; i++) { /* ... */ }
    axl_free(handles);
}

Custom Protocol Names

Drivers can publish their own protocols under a project-defined name. By default axl_protocol_register("my-protocol", &iface, &handle) synthesizes a deterministic GUID from the name string (FNV-1a). That works for single-image use, but the GUID is unstable across typos and not directly usable for cross-image discovery via raw LocateProtocol. Pin a published vendor GUID once at startup:

static const AxlGuid kMySvcGuid =
    AXL_GUID(0xdead0001, 0xbeef, 0xcafe,
             0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0);

// In DriverEntry
axl_protocol_register_name("my-protocol", &kMySvcGuid);
axl_protocol_register("my-protocol", &mInterface, &mHandle);

External consumers can either use axl_protocol_find("my-protocol", …) (after a matching register_name of their own — names are per-image) or LocateProtocol(&kMySvcGuid, …) directly against the published GUID. register_name is idempotent for the same (name, guid) pair, refuses to shadow built-in well-known names, and returns AXL_ERR if the name is already pinned to a different GUID.

Driver-Image Lifecycle

Registered protocols are NOT auto-released when a driver image is unloaded — the AXL protocol registry never owned the install. Walk your protocols in axl_driver_set_unload’s callback:

static EFI_STATUS EFIAPI MyUnload(EFI_HANDLE image) {
    if (mHandle != NULL) {
        axl_protocol_unregister(mHandle, "my-protocol", &mInterface);
    }
    return EFI_SUCCESS;
}

sdk/examples/driver.c is the canonical reference. Forgetting to unregister leaves dangling handle entries pointing at freed driver memory; subsequent LocateProtocol calls hand consumers a stale vtable and the next dispatch faults.

TPL Contract

All entry points (_register, _register_name, _register_multiple, _find, _enumerate, _unregister) bottom out in UEFI Boot Services calls (InstallProtocolInterface, LocateProtocol, LocateHandleBuffer, UninstallProtocolInterface) plus axl_malloc, all of which require TPL ≤ TPL_NOTIFY per UEFI 2.11 §7.3. Callers running from a timer handler at TPL_NOTIFY are fine; callers at TPL_HIGH_LEVEL must lower first. Callbacks dispatched through AxlLoop (defer-drain, pubsub delivery, source handlers) all run at TPL_APPLICATION — the loop calls WaitForEvent, which mandates that level — so consumers writing protocol-event-driven code don’t need to worry about TPL inside handlers.

Protocol-Event Subscriptions

For protocols that need to publish events (“client connected”, “upload complete”), reuse axl_pubsub_* from <axl/axl-pubsub.h> rather than rolling a protocol-internal callback list. Topics are string-keyed; multiple consumers can subscribe; delivery is deferred via the loop’s defer queue, so handlers always fire at TPL_APPLICATION. Pubsub is AxlLoop-scoped: subscribers must run on the same loop instance as the publisher to receive events.

Auto-Loading Driver Dependencies

Tools that need a protocol provided by a DXE driver (e.g. a RAM-disk manager that needs EFI_RAM_DISK_PROTOCOL from RamDiskDxe.efi) can call axl_driver_ensure to short-circuit when the protocol is already registered, or to find and load the driver themselves otherwise:

if (axl_driver_ensure(&EfiRamDiskProtocolGuid,
                      "RamDiskDxe.efi") != 0) {
    axl_printf("RamDiskDxe.efi not available\n");
    return 1;
}
/* Protocol is now usable. */

The search walks drivers/<arch>/<name> on the running image’s own volume first, then the image’s own directory, then the volume root, and finally every other mounted FAT volume. The first match is loaded and started; if it doesn’t end up registering the requested protocol, the image is unloaded and the search continues. This lets tools work whether they’re invoked from a bare UEFI shell, a boot menu, or a startup.nsh that has already eager-loaded the driver.

Tool Diagnostics

When investigating “why doesn’t my tool work on this firmware?”, call axl_diag_startup(argc, argv) from your -v / --verbose handler. It prints six labelled sections in one block:

POSIX argc = 3
POSIX argv[0] = "mkrd.efi"
POSIX argv[1] = "-v"
POSIX argv[2] = "testrd"
LOADOPT: size = 38 bytes
LOADOPT: utf8 = "mkrd.efi -v testrd"
SHELL: protocol OK, Argc = 3
SHELL: Argv[0] = "FS0:\mkrd.efi"
...
IMG: path = \mkrd.efi
VOLUMES: 1 mounted
  fs0

POSIX argv shows what reached main after axl-app.c parsed EFI_LOADED_IMAGE_PROTOCOL.LoadOptions. LOADOPT shows the raw UCS-2 buffer the firmware passed in. SHELL is the optional EFI_SHELL_PARAMETERS_PROTOCOL probe — some OEM firmware sometimes doesn’t publish it for cross-volume invocations, which was the original “argc=1” bug. IMG and VOLUMES are the search anchors axl_driver_ensure / axl_driver_locate use.

For protocol-registration questions specifically, pair it with axl_diag_probe_protocol:

if (verbose) {
    axl_diag_startup(argc, argv);
    axl_diag_probe_protocol(
        (const AxlGuid *)&EFI_RAM_DISK_PROTOCOL_GUID,
        "EFI_RAM_DISK_PROTOCOL");
}
/* ... call axl_driver_ensure ... */
if (verbose) {
    axl_diag_probe_protocol(
        (const AxlGuid *)&EFI_RAM_DISK_PROTOCOL_GUID,
        "EFI_RAM_DISK_PROTOCOL (post-ensure)");
}

The two probes around axl_driver_ensure show whether the firmware already had the driver baked in (both ALREADY REGISTERED) or whether ensure had to load it from disk (NOT registeredREGISTERED).

Configuration (and Command-Line Parsing)

Unified configuration framework. One descriptor table drives defaults, typed getters, auto-apply to caller structs via offsetof, callbacks for custom logic, parent inheritance for cascading defaults, and command-line argument parsing (short flags, long flags, repeatable multi-values, positional args, and --).

AxlConfig replaces ad-hoc key-value parsing with a declarative system. You define a table of option descriptors (name, type, default value, optional short flag, help text), then populate from any source: defaults, programmatic set, command-line argv, or a parent config. Type validation happens automatically.

Defining Options

#include <axl.h>

typedef struct {
    size_t  port;
    bool    verbose;
    size_t  max_connections;
} ServerConfig;

static const AxlConfigDesc opts[] = {
    { "port",     AXL_CFG_UINT, "8080", 0, "Listen port",
      offsetof(ServerConfig, port), sizeof(size_t) },
    { "verbose",  AXL_CFG_BOOL, "false", 0, "Verbose output",
      offsetof(ServerConfig, verbose), sizeof(bool) },
    { "max.conn", AXL_CFG_UINT, "16", 0, "Max connections",
      offsetof(ServerConfig, max_connections), sizeof(size_t) },
    { 0 }
};

Creating and Querying

ServerConfig sc;
AXL_AUTOPTR(AxlConfig) cfg = axl_config_new(opts);

// Set the auto-apply target -- values are written directly
// into the struct fields via offsetof
axl_config_set_target(cfg, &sc);

// Set values (type-validated)
axl_config_set(cfg, "port", "9090");       // sc.port = 9090
axl_config_set(cfg, "verbose", "true");    // sc.verbose = true

// Query values
size_t port = axl_config_get_uint(cfg, "port");
const char *port_str = axl_config_get(cfg, "port");  // "9090"

Command-Line Parsing

CLI parsing moved to AxlArgs (<axl/axl-args.h>) — see the Command-Line Parsing (AxlArgs) section below. AxlConfig stays focused on the live property-bag use case (HTTP client/server settings, future modules with tunable runtime properties).

Multi-Value Options

For options that can be specified multiple times (e.g., -H "Name: Value"):

size_t count = axl_config_get_multi_count(cfg, "headers");
for (size_t i = 0; i < count; i++) {
    const char *hdr = axl_config_get_multi(cfg, "headers", i);
    axl_printf("  header: %s\n", hdr);
}

Standard option groups (group injection)

Networked tools repeat the same NIC / local-IP / port descriptors over and over. axl_config_descs_net emits the canonical entries into a consumer-owned accumulator, with descriptor offsets shifted by the consumer’s embedded AxlNetOpts sub-struct offset:

typedef struct {
    AxlNetOpts net;       // the standard sub-struct
    const char *url;
    bool        read_only;
} MountOpts;

static const AxlConfigDesc mount_consumer_descs[] = {
    { "url",       AXL_CFG_STRING, "",      "Server URL",
      offsetof(MountOpts, url),       sizeof(((MountOpts*)0)->url) },
    { "read-only", AXL_CFG_BOOL,   "false", "Mount read-only",
      offsetof(MountOpts, read_only), sizeof(bool), 'r' },
    { 0 }
};

static AxlConfigDesc mount_descs[16];
void mount_descs_init(void) {
    size_t n = axl_config_descs_net(mount_descs, ARRAY_SIZE(mount_descs),
                                    AXL_NET_OPT_SERVER,
                                    offsetof(MountOpts, net));
    n += axl_config_descs_append(mount_descs + n,
                                 ARRAY_SIZE(mount_descs) - n - 1,
                                 mount_consumer_descs);
    mount_descs[n] = (AxlConfigDesc){ 0 };
}

AXL_NET_OPT_CLIENT / _SERVER presets cover the common cases; finer-grained bitmasks (AXL_NET_OPT_NIC | AXL_NET_OPT_PORT) also work. AXL_NET_OPT_SOURCE_IP and AXL_NET_OPT_LISTEN_IP both target the same local_ip field (same bind(2) syscall); they differ only in CLI vocabulary — pick whichever matches your tool’s role. The emitted descriptors preserve short_name / choices and route through AxlConfig’s auto-apply machinery exactly like the consumer’s own table. See <axl/axl-net-opts.h> for the option-bag types and the matching axl_net_init_from_opts bring-up helper.

The companion axl_config_descs_append copies a consumer-owned descriptor fragment (terminated by {0}) onto the accumulator; the caller writes the final {0} terminator once, after all fragments have been appended.

Parent Inheritance

Create a child config that inherits defaults from a parent:

AxlConfig *defaults = axl_config_new(opts);
axl_config_set(defaults, "port", "8080");

AxlConfig *override = axl_config_new_with_parent(opts, defaults);
// override inherits "port"="8080" until explicitly set

Command-Line Parsing (AxlArgs)

Declarative CLI parser — the tool declares a static AxlArgsNode tree, calls axl_args_run from main, and the framework parses argv, validates types and bounds, generates --help, and dispatches to the matching leaf handler.

Header: <axl/axl-args.h>.

One node type, three shapes

A single recursive node type (AxlArgsNode) describes the program root, every inner branch (“category”), and every leaf verb. A node is exactly one of:

  • Leafhandler set, verbs NULL. Optionally has positionals. Handler runs once parsing completes at this level.

  • Branchverbs set (NULL-terminated array of child nodes), handler NULL. Positionals MUST be NULL (the first non-flag argument is the verb name).

  • Single-handler app — root happens to be a leaf (no verbs). The whole tool is one shape.

A node with both, or neither, is a configuration error and the parser exits non-zero before invoking anything.

Single-handler tool

static int do_run(AxlArgs *a) {
    const char *path = axl_args_get_string(a, "path");
    return process(path);
}

int main(int argc, char **argv) {
    return axl_args_run(argc, argv, &(AxlArgsNode){
        .name = "mytool", .help = "Process a file",
        .positionals = (AxlArgDesc[]){
            { .name = "path", .type = AXL_ARG_STRING, .required = true,
              .help = "Input file" },
            {0}
        },
        .handler = do_run,
    });
}

Multi-verb tool

static const AxlArgsNode verbs[] = {
    { .name = "show", .handler = do_show, .positionals = slot_pos,
      .help = "Decoded fields for one slot" },
    { .name = "list", .handler = do_list,
      .help = "List populated slots" },
    {0}
};

int main(int argc, char **argv) {
    return axl_args_run(argc, argv, &(AxlArgsNode){
        .name = "memspd", .help = "Read JEDEC SPD content",
        .flags = flags, .verbs = verbs,
    });
}

Argument types

AxlArgDesc.type selects the parser. Numeric types (AXL_ARG_U8..AXL_ARG_S64) accept optional min / max bounds and a base (0 = auto-detect, 10, or 16). String types get one more knob:

  • AXL_ARG_BOOL — presence flag, no value

  • AXL_ARG_STRING — unconstrained string

  • AXL_ARG_MULTI — repeatable string (variadic positional, or repeatable flag); accumulates into axl_args_get_multi

  • AXL_ARG_U8 / AXL_ARG_U16 / AXL_ARG_U32 / AXL_ARG_U64 / AXL_ARG_S64 — typed integers with bounds

  • AXL_ARG_CHOICE — string restricted to a caller-supplied set:

static const char *const fields[] = {
    "noHdds", "riserCfg", "delRiser", NULL
};
static const AxlArgDesc field_pos[] = {
    { .name = "field", .type = AXL_ARG_CHOICE, .required = false,
      .choices = fields,
      .default_value = "noHdds",
      .help = "field selector" },
    {0}
};

The framework rejects values not in choices with a breadcrumb-prefixed error matching the out-of-range numeric format, and lists the accepted values as <noHdds|riserCfg|delRiser> in --help output. Comparison is case-sensitive by default. Setting choices to NULL or an empty array degrades to AXL_ARG_STRING (unconstrained); useful when the caller wants <a|b|c> help text but custom validation in the handler.

For case-insensitive match — useful when migrating CLIs that already accept mixed-case variants — set .choices_case_insensitive = true:

{ .name = "field", .type = AXL_ARG_CHOICE,
  .choices = fields,
  .choices_case_insensitive = true,
  .help = "field selector" }

--help then renders <noHdds|riserCfg|delRiser> (case-insensitive) so users know dd_cfg and DD_CFG both work. The value the handler sees retains the user’s original casing — only validation folds case. ASCII-only fold (per axl_strcasecmp); non-ASCII bytes compare byte-equal.

Nested verbs (<top> <category> <verb>)

static const AxlArgsNode bios_verbs[] = {
    { .name = "test", .handler = bios_test, .help = "Run BIOS self-test" },
    { .name = "pci",  .handler = bios_pci,  .help = "List BIOS-PCI map" },
    {0}
};

static const AxlArgsNode top_verbs[] = {
    { .name = "bios", .verbs = bios_verbs,
      .help = "BIOS / SMBIOS subcommands" },
    { .name = "load", .handler = do_load, .positionals = load_args,
      .help = "Load and run a UEFI image" },
    {0}
};

int main(int argc, char **argv) {
    return axl_args_run(argc, argv, &(AxlArgsNode){
        .name = "do", .help = "Hardware diagnostic CLI",
        .flags = root_flags,
        .verbs = top_verbs,
    });
}

do bios test invokes the leaf with the breadcrumb in scope; if the user types do bios flarble, the error reads do bios: unknown verb 'flarble'. do bios --help recurses into the bios subtree’s auto-generated help.

Branch with a default handler

A node can set BOTH verbs and handler — the handler runs only when no sub-verb is supplied. Useful for the do bios → “print summary” pattern where a category has subverbs but also wants a default action:

static const AxlArgsNode bios_verbs[] = {
    { .name = "info", .handler = bios_info, .help = "Type 0 summary" },
    { .name = "test", .handler = bios_test, .help = "Walk every record" },
    {0}
};

static const AxlArgsNode bios_node = {
    .name    = "bios",
    .help    = "BIOS / SMBIOS subcommands",
    .verbs   = bios_verbs,
    .handler = bios_info,    // fires on 'do bios' with no sub-verb
};

Dispatch is unambiguous: a verb argument that matches a child recurses into it; a verb argument that matches none errors as do bios: unknown verb 'flarble' (the handler is not a catch-all); no verb argument at all invokes the handler with the branch’s parsed flags. Branch+handler nodes still cannot have positionals — the first non-flag is structurally the verb name.

In do bios --help output, the verb whose handler matches the default is annotated (default) so users see which sub-verb the no-arg form is equivalent to.

Parent-flag visibility

Flags declared on a parent node are visible to descendant handlers via the same accessors. A --verbose declared on the root is readable from a leaf two levels deep:

static int bios_test(AxlArgs *a) {
    bool verbose = axl_args_get_bool(a, "verbose");   // root flag
    uint8_t slot = (uint8_t)axl_args_get_uint(a, "slot");  // leaf positional
    /* ... */
}

Same for axl_args_user_data — descendants inherit the nearest non-NULL value walking up the chain.

Error attribution

Errors are prefixed with the full breadcrumb path so users know exactly which level rejected their input:

do bios test: 'foo' for --slot is not a valid integer
do pci: unknown verb 'flarble'
do: unknown flag --verbosee

Lifetime

AxlArgs and accessor return values live until the leaf handler returns. String values point into argv (program-lifetime); copy numeric values, copy variadic-positional pointers if you need them past handler return. Never call axl_args_get_* from a loop callback that fires after the handler returns — extract everything into local state inside the handler first.

Path Manipulation

Path manipulation: basename, dirname, extension, join, resolve. Handles both / (Unix) and \ (UEFI) path separators. All allocated results are freed with axl_free().

UEFI uses backslash (\) as the path separator, while most developers are familiar with forward slash (/). AXL accepts both and normalizes internally. Paths typically start with a volume name: fs0:/path/to/file.

AXL_AUTO_FREE char *base = axl_path_get_basename("fs0:/logs/app.log");
// base = "app.log"

AXL_AUTO_FREE char *dir = axl_path_get_dirname("fs0:/logs/app.log");
// dir = "fs0:/logs"

AXL_AUTO_FREE char *ext = axl_path_get_extension("app.log");
// ext = "log"

AXL_AUTO_FREE char *full = axl_path_join("fs0:/data", "output.json");
// full = "fs0:/data/output.json"

// Resolve relative paths
char resolved[256];
axl_path_resolve("fs0:/app", "../config/app.cfg",
                 resolved, sizeof(resolved));
// resolved = "fs0:/config/app.cfg"

Synchronization primitives

AxlCompletion / AxlCancellable / axl_wait_* and the foundational AxlEvent moved out of AxlUtil into the dedicated src/event/ module. See src/event/README.md for the current documentation.

API Reference

AxlSys

Defines

AXL_GUID(d1, d2, d3, d4_0, d4_1, d4_2, d4_3, d4_4, d4_5, d4_6, d4_7)

Initialize an AxlGuid from literal values.

Usage: AxlGuid

g = AXL_GUID(0x12345678, 0xABCD, 0xEF01,

0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01);

AXL_RESET_COLD

cold reset (full power cycle)

AXL_RESET_WARM

warm reset (CPU reset, memory preserved)

AXL_RESET_SHUTDOWN

power off

Typedefs

typedef void *AxlHandle

Opaque handle for any UEFI-tracked entity.

axl-sys.h:

System operations — reset, device mapping refresh. UEFI-specific, no GLib equivalent.

Binary-compatible with EFI_HANDLE (both void *). Use in public API and consumer code wherever a UEFI handle would appear — image handles from DriverEntry, protocol handles returned by axl_protocol_register, controller handles passed to axl_driver_connect_handle, handles returned by axl_protocol_enumerate. AxlHandle is an opaque token; never dereference it. The EFI_* spelling stays available via <uefi/axl-uefi.h> for the rare consumer that needs to call a gBS->...(EFI_HANDLE) directly.

typedef struct AxlSystemTable AxlSystemTable

Opaque firmware system-table pointer.

Forward-decl for EFI_SYSTEM_TABLE. Drivers receive an AxlSystemTable * from the AXL_DRIVER adapter and pass it straight to axl_driver_init; consumers never dereference it. Reach for <uefi/axl-uefi.h>’s typed EFI_SYSTEM_TABLE only when you need to poke at firmware internals the AXL surface doesn’t cover.

typedef int (*AxlDevicePathFn)(uint8_t type, uint8_t subtype, const void *node, void *user)

Per-node callback for axl_device_path_for_each.

Return 0 to continue iteration, any non-zero value to stop — the return value is propagated back from axl_device_path_for_each so callbacks can use it as a found-flag, error code, or count.

node points at the full device-path node (4-byte header followed by payload); cast it to the corresponding spec struct (e.g. VENDOR_DEVICE_PATH *) once type and subtype have confirmed the shape.

Functions

static inline bool axl_guid_cmp(const AxlGuid *a, const AxlGuid *b)

Compare two GUIDs for equality.

Returns:

true if equal.

int axl_guid_v5(const AxlGuid *namespace_uuid, const char *name, AxlGuid *out)

Derive a deterministic GUID from a (namespace, name) pair.

Name-based UUID generation in the shape of RFC 4122 §4.3 (UUIDv5): the SHA-1 of namespace_bytes || name_bytes is truncated to 16 bytes, with the version field set to 5 and the RFC-4122 variant bits set on the result. Same (namespace, name) always yields the same GUID, across binaries / arches / runs.

Used by AxlService to derive each service’s identity GUID from AxlService.name so consumers don’t have to hand-allocate a UUID per service. Other AXL modules that want stable GUIDs from string keys can use the same primitive.

Namespace bytes are fed verbatimAxlGuid’s storage layout (data1/data2/data3 in host byte order) is what hashes, NOT the RFC-4122 network-byte-order serialization. The 16-byte result is likewise stored as opaque AxlGuid bytes; AXL never reads its data1/data2/data3 fields as host-order ints once derived. This is an internal AXL convention, not strict RFC 4122 — GUIDs derived here won’t match what a UUIDv5 generator on another platform would produce given “the same” namespace UUID written in canonical text form. That’s fine for AXL’s use case (derivation lives entirely inside AXL) and avoids a host-vs-network endian conversion that would otherwise creep into every caller.

Parameters:
  • namespace_uuid – namespace UUID (e.g. an AXL module’s identity)

  • name – NUL-terminated name string

  • out – [out] derived GUID

Returns:

AXL_OK on success (out populated); AXL_ERR if namespace or name is NULL or out is NULL.

bool axl_device_path_has_vendor(void *device_path, const AxlGuid *guid)

Check if a device path contains a vendor node with the given GUID.

Walks the device path node chain looking for a hardware vendor node (type 0x01, subtype 0x04) whose GUID matches guid.

Parameters:
  • device_path – device path (from “device-path” protocol)

  • guid – vendor GUID to match

Returns:

true if a matching vendor node is found.

int axl_device_path_for_each(const void *device_path, AxlDevicePathFn fn, void *user)

Walk a device-path node chain with bounded-step safety.

Iterates from device_path through the END node, calling fn on each node with its (type, subtype, node) triple. Stops early when fn returns non-zero (and propagates that value), or when a malformed node is hit (length < 4 or the chain doesn’t terminate within an internal step cap).

Replaces hand-rolled while (!EFI_DP_IS_END(node)) ... loops — those used to differ on whether they bounded the walk, leaving malformed firmware data able to runaway.

Parameters:
  • device_path – device path (from “device-path” protocol)

  • fn – per-node callback

  • user – opaque user pointer for the callback

Returns:

0 on a clean traversal to END, the callback’s non-zero return value if it stopped early, or -1 on malformed input.

const void *axl_device_path_find(const void *device_path, uint8_t type, uint8_t subtype)

Find the first device-path node matching (type, subtype).

Parameters:
  • device_path – device path (from “device-path” protocol)

  • type – node type to match

  • subtype – node subtype to match

Returns:

pointer to the node (cast to the corresponding spec struct by the caller), or NULL if no match.

size_t axl_device_path_size(const void *device_path)

Compute the total byte length of a device path INCLUDING the END node.

Useful when copying / appending device paths, e.g. when building a LoadImage argument out of an existing volume DP plus a file suffix. Bounded by the same step cap as the iterator.

Parameters:
  • device_path – device path (from “device-path” protocol)

Returns:

size in bytes, or 0 on malformed input.

char *axl_device_path_to_text(const void *device_path)

Render a device path as the firmware’s canonical text form.

Wraps the EFI_DEVICE_PATH_TO_TEXT_PROTOCOL the firmware exposes (ConvertDevicePathToText) and converts the resulting UCS-2 string to UTF-8. The output is the same format dh -d and bcfg boot dump produce — e.g. PciRoot(0x0)/Pci(0x3,0x0)/MAC(525400123456,0x1).

Returns NULL when the firmware doesn’t expose EFI_DEVICE_PATH_TO_TEXT_PROTOCOL (some vintage UEFI 2.0 builds omit it) or when device_path is NULL.

Parameters:
  • device_path – device path (from “device-path” protocol)

Returns:

UTF-8 string allocated with axl_malloc, or NULL on failure. Caller frees with axl_free.

void axl_reset(int type)

Reset or shut down the system.

Does not return on success.

Parameters:
  • type – AXL_RESET_COLD, AXL_RESET_WARM, or AXL_RESET_SHUTDOWN

int axl_map_refresh(void)

Rescan device-to-filesystem mappings.

Equivalent to the Shell “map -r” command. Call after hot-plugging a USB drive or after a driver installs a new filesystem.

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_sys_get_firmware_info(AxlFirmwareInfo *info)

Get firmware information (vendor, revision, spec version).

Parameters:
  • info – [out] receives firmware info

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_sys_get_memory_size(uint64_t *total_bytes)

Get total usable memory size in bytes.

Queries the firmware memory map and sums all usable regions.

Parameters:
  • total_bytes – [out] receives total usable RAM

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_handle_get_protocol(void *handle, const char *name, void **interface)

Get a protocol interface from a specific handle.

Parameters:
  • handle – handle from axl_protocol_enumerate

  • name – protocol name (e.g., “device-path”, “simple-fs”)

  • interface – [out] protocol interface pointer

Returns:

AXL_OK on success, AXL_ERR if not found.

int axl_protocol_register_name(const char *name, const AxlGuid *guid)

Pin a stable vendor GUID to a custom protocol name.

By default axl_protocol_register("custom-name", ...) synthesizes a deterministic GUID from the name string via FNV-1a. That works for single-image use, but the GUID is unstable across name spelling (a typo gives a different GUID) and isn’t usable for cross-image discovery via raw LocateProtocol because external consumers can’t reproduce it without the same name string.

Calling axl_protocol_register_name(name, guid) once at startup pins name to guid in the per-process registry, so subsequent axl_protocol_register / _find / _enumerate / _unregister calls for that name install or look up against guid instead. Other consumers can publish the GUID in their own headers and LocateProtocol against it without going through the AXL protocol-registry layer at all.

Idempotent: re-registering the same (name, guid) pair returns AXL_OK. Re-registering a name with a different GUID, or registering a name already in the built-in well-known table (e.g. “smbios”, “simple-fs”), returns AXL_ERR. Names are copied internally; name does not need to outlive the call.

Process-lifetime: the registration persists for the lifetime of the running image. There is no axl_protocol_unregister_name; unregistering a handle with axl_protocol_unregister does not remove the name pinning, since other consumers may still want to reuse the same name → GUID mapping. The custom-name table is fixed-capacity (16 entries per image) and statically linked into each image — consumers loading and unloading drivers on a tight loop should pin once at first init, not on every iteration.

Cross-image discovery: each image has its own copy of the AXL protocol-registry layer (via static linkage of libaxl.a), so a consumer in image B that wants to find a protocol published by image A must either (a) call axl_protocol_register_name itself with the same (name, guid) pair before calling axl_protocol_find, or (b) call LocateProtocol directly against the published GUID without going through the AXL protocol-registry layer.

Parameters:
  • name – protocol name (copied internally)

  • guid – vendor GUID to bind to name

Returns:

AXL_OK on success or idempotent re-register; AXL_ERR if name is NULL/empty, guid is NULL, the name shadows a built-in well-known name, the name is already pinned to a different GUID, or the registry is full.

int axl_protocol_find(const char *name, void **interface)

Find a system protocol by name.

Looks up a named protocol in the platform protocol registry. Well-known names: “smbios”, “shell”, “simple-network”, “simple-fs”. Custom names work too — names registered via axl_protocol_register_name resolve to their pinned GUID; unregistered custom names fall back to a deterministic FNV-1a GUID derived from the name string.

Cross-image gotcha: the name → GUID table is per-image. A consumer that wants to find a custom-named protocol published by a different image must first call axl_protocol_register_name with the same (name, guid) pair, or skip the name layer and LocateProtocol against the published GUID directly.

Parameters:
  • name – protocol name

  • interface – [out] service interface pointer

Returns:

AXL_OK on success, AXL_ERR if not found.

int axl_protocol_enumerate(const char *name, void ***handles, size_t *count)

Enumerate all handles providing a named protocol.

Caller frees the returned handles array with axl_free().

Parameters:
  • name – protocol name

  • handles – [out] array of handles

  • count – [out] number of handles

Returns:

AXL_OK on success (count may be 0), AXL_ERR on error.

int axl_protocol_register(const char *name, void *interface, void **handle)

Register a protocol on a handle.

Creates a new handle if *handle is NULL.

Parameters:
  • name – protocol name

  • interface – protocol interface to install

  • handle – [in/out] handle (NULL to create new)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_protocol_unregister(void *handle, const char *name, void *interface)

Unregister a protocol from a handle.

Parameters:
  • handle – handle from axl_protocol_register

  • name – protocol name

  • interface – interface to remove

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_protocol_find_guid(const AxlGuid *guid, void **interface)

Locate a protocol interface by GUID directly.

GUID-keyed counterpart to axl_protocol_find. Skips the name-registry name → GUID lookup; useful for consumers that already hold a GUID (notably AxlService, whose identity is the name-derived GUID from axl_service_guid). Returns the first interface registered for the GUID via LocateProtocol semantics.

Parameters:
  • guid – protocol GUID to look up (must be non-NULL)

  • interface – [out] service interface pointer

Returns:

AXL_OK on success (interface populated); AXL_ERR if no handle publishes the GUID or arguments are NULL.

int axl_protocol_enumerate_guid(const AxlGuid *guid, void ***handles, size_t *count)

Enumerate all handles publishing a protocol by GUID directly.

GUID-keyed counterpart to axl_protocol_enumerate. Returns an axl_malloc'd array of handles publishing guid; caller frees with axl_free. Used by axl_service_stop to discover every driver image that registered the service’s identity GUID (typically one, but the contract handles N for symmetry with the underlying LocateHandleBuffer).

Empty result (no handle publishes the GUID) is success: handles is set to NULL and count to 0.

Parameters:
  • guid – protocol GUID to look up (must be non-NULL)

  • handles – [out] axl_malloc’d handle array (may be NULL on empty)

  • count – [out] number of handles

Returns:

AXL_OK on success (count may be 0); AXL_ERR on bad arguments or firmware allocation failure.

int axl_protocol_register_guid(const AxlGuid *guid, void *interface, void **handle)

Register a protocol on a handle by GUID directly.

Skips the name-registry name → GUID lookup. Used by AxlService’s AXL_SERVICE_DRIVER macro to publish a sentinel handle for the service’s identity GUID without going through axl_protocol_register_name (which would pollute the per-image name table). Also useful for consumers that already have a GUID and don’t need the name layer.

Creates a new handle if *handle is NULL.

Parameters:
  • guid – protocol GUID

  • interface – protocol interface to install

  • handle – [in/out] handle (NULL to create new)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_protocol_unregister_guid(void *handle, const AxlGuid *guid, void *interface)

Unregister a protocol by GUID directly.

GUID-based counterpart to axl_protocol_unregister.

Parameters:
  • handle – handle from axl_protocol_register_guid

  • guid – protocol GUID

  • interface – interface to remove

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_protocol_register_multiple(void **handle, ...)

Register multiple protocols on a handle atomically.

Installs one or more protocols on the same handle in one operation. If any fails, none are installed. Creates a new handle if *handle is NULL. Pass name/interface pairs followed by NULL:

void *h = NULL;
axl_protocol_register_multiple(&h,
    "simple-fs", &my_fs,
    "device-path", &my_dp,
    NULL);
Parameters:
  • handle – [in/out] handle (NULL to create new)

Param :

name, interface pairs, terminated by NULL

Returns:

AXL_OK on success, AXL_ERR on error.

struct AxlGuid
#include <axl-sys.h>

UEFI-compatible GUID in standard C types.

Binary-compatible with EFI_GUID. Use in public API so consumer apps don’t need <uefi/axl-uefi.h> for GUID operations.

Public Members

uint32_t data1
uint16_t data2
uint16_t data3
uint8_t data4[8]
struct AxlFirmwareInfo
#include <axl-sys.h>

Firmware information.

Public Members

char vendor[64]

firmware vendor name (UTF-8)

uint32_t firmware_revision

vendor firmware revision

uint16_t spec_major

UEFI spec major version.

uint16_t spec_minor

UEFI spec minor version.

AxlEnv

Functions

char *axl_getenv(const char *name)

Get a shell environment variable.

axl-env.h:

Shell environment variable access.

char *home = axl_getenv("path");
if (home != NULL) {
    axl_printf("path=%s\n", home);
    axl_free(home);
}
axl_setenv("myvar", "hello", true);

Returns a UTF-8 copy of the variable’s value. Caller frees with axl_free().

Parameters:
  • name – variable name (UTF-8)

Returns:

value string, or NULL if not found.

int axl_setenv(const char *name, const char *value, bool overwrite)

Set a shell environment variable.

Parameters:
  • name – variable name (UTF-8)

  • value – value (UTF-8)

  • overwrite – if false, don’t replace existing value

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_unsetenv(const char *name)

Remove a shell environment variable.

Parameters:
  • name – variable name (UTF-8)

Returns:

AXL_OK on success, AXL_ERR on error.

AxlTime

Defines

AXL_TIME_TZ_UNSPECIFIED

Sentinel for AxlRealtime.timezone_minutes when the firmware does not report a timezone (corresponds to UEFI’s EFI_UNSPECIFIED_TIMEZONE = 2047).

AXL_TIME_FLAG_DAYLIGHT

Bit set in AxlRealtime.flags if the time is currently in DST.

Functions

size_t axl_time_format(char *buf, size_t buf_size)

Format current time as ISO 8601 with microseconds.

axl-time.h:

Time utilities: ISO 8601 formatting and a monotonic counter.

Sleep and wait primitives live in <axl/axl-wait.h> — that’s where to find axl_sleep / axl_msleep / axl_usleep (ergonomic void-return sleep) and the axl_wait_* family (condvar-style blocking with cancel + timeout).

Example: “2026-03-27T14:05:32.123456”

Parameters:
  • buf – destination buffer (at least 28 bytes)

  • buf_size – size of buffer

Returns:

characters written (excluding NUL), 0 on error.

uint64_t axl_time_get_ms(void)

Get a monotonic millisecond counter.

Based on firmware time — not wall-clock accurate but monotonically increasing within a boot session. Useful for measuring elapsed time.

Returns:

milliseconds since an arbitrary epoch (typically boot).

uint64_t axl_time_get_us(void)

Get a high-resolution monotonic microsecond counter.

Reads the architecture’s cycle counter (x86 TSC / aarch64 CNTPCT_EL0). The first call calibrates against a brief firmware stall and returns 0; subsequent calls are cheap (one counter read + a multiply) and return microseconds since the calibration call. No defined relationship to wallclock time — pair with axl_time_get_ms when you need both elapsed-microsecond resolution and a wallclock anchor.

Use this instead of axl_time_get_ms when the measurement window is on the order of milliseconds or shorter (firmware stall calibration, network round-trips, parser benchmarking).

Returns:

microseconds since the implicit calibration epoch. Returns 0 on the very first call (calibration tick) and on architectures with no usable cycle counter.

int axl_time_realtime(AxlRealtime *out)

Read the firmware real-time clock.

Backend-neutral wrap of EFI_RUNTIME_SERVICES.GetTime. Allocation- free; safe to call from CPU exception context. The returned AxlRealtime carries the full GetTime payload (year/month/day/ hour/minute/second/nanosecond/timezone/dst) — consumers that only need a packed timestamp pack the fields themselves.

Returns:

AXL_OK on success, AXL_ERR if the firmware reports a failure or out is NULL. On failure out is unmodified.

struct AxlRealtime
#include <axl-time.h>

Wallclock time snapshot as exposed by the firmware real-time clock.

Layout-stable; the firmware-side EFI_TIME struct is private to the SDK. Consumers see UTF-8-friendly snake_case field names and a sentinel-based timezone encoding rather than UEFI’s EFI_UNSPECIFIED_TIMEZONE magic value.

Public Members

uint16_t year

full year (e.g. 2026)

uint8_t month

1-12

uint8_t day

1-31

uint8_t hour

0-23

uint8_t minute

0-59

uint8_t second

0-60 (UEFI allows leap-second 60)

uint8_t flags

AXL_TIME_FLAG_* bits.

uint32_t nanosecond

0-999,999,999

int16_t timezone_minutes

UTC offset, or AXL_TIME_TZ_UNSPECIFIED.

AxlNvStore

Defines

AXL_NV_VOLATILE

lost on reboot

axl-nvstore.h:

Platform-agnostic non-volatile key-value storage.

Provides persistent storage that survives reboots. On UEFI, this maps to firmware variables (GetVariable/SetVariable). Variables are organized by namespace.

Built-in namespaces: “global” — standard firmware variables (e.g., SecureBoot, BootOrder) “app” — application-specific persistent settings

Vendor namespaces (Dell, HPE, Lenovo OEM variables) plug in via axl_nvstore_register_namespace() with a backend-specific token. On UEFI the token is a const AxlGuid * (vendor-GUID pointer); on a Linux backend it might be a path prefix. Access sites stay UEFI-free — they reference namespaces by name only.

uint8_t secure_boot;
size_t sz = sizeof(secure_boot);
if (axl_nvstore_get("global", "SecureBoot", &secure_boot, &sz) == 0) {
    axl_printf("SecureBoot: %s\n", secure_boot ? "enabled" : "disabled");
}

extern const AxlGuid AXL_OEM_VENDOR_GUID;
axl_nvstore_register_namespace("oem", &AXL_OEM_VENDOR_GUID);
axl_nvstore_get("oem", "AssetTag", buf, &sz);
AXL_NV_PERSISTENT

survives reboot (non-volatile)

AXL_NV_BOOT

accessible during boot services

AXL_NV_RUNTIME

accessible at runtime

Typedefs

typedef int (*AxlNvstoreIterFn)(const char *key, void *ctx)

Iterator callback for axl_nvstore_iter.

Return:

0 to continue iteration, non-zero to stop. The non-zero value is returned to the iter() caller.

Functions

int axl_nvstore_register_namespace(const char *name, const void *backend_token)

Register a namespace name and bind it to a backend token.

The backend token is opaque to consumers. On UEFI it is a const AxlGuid * (vendor-GUID pointer); on other backends it may be a path prefix or other identifier. The pointer must remain valid for the lifetime of the program — the table stores the pointer, not a copy.

Built-in namespaces “global” and “app” are pre-registered and do not need to be registered explicitly.

Parameters:
  • name – namespace name (UTF-8, copied)

  • backend_token – opaque per-backend token

Returns:

AXL_OK on success, AXL_ERR if the namespace table is full or the name is already registered with a different token.

int axl_nvstore_get(const char *ns, const char *key, void *buf, size_t *size)

Read a value from non-volatile storage.

Parameters:
  • ns – namespace (e.g., “global”, “app”)

  • key – variable name (UTF-8)

  • buf – output buffer

  • size – [in/out] buffer size / bytes read

Returns:

AXL_OK on success, AXL_ERR on error (variable not found, buffer too small, namespace not registered, etc.). On buffer-too-small, size is updated to the required size.

int axl_nvstore_get_alloc(const char *ns, const char *key, void **out_buf, size_t *out_size)

Read a value from non-volatile storage into a heap buffer.

Like axl_nvstore_get, but the buffer is allocated for you. Useful when reading variable-length blobs (NV strings, OEM settings) where the caller doesn’t know the size up front and picking a fixed stack buffer either over-allocates or risks truncation. On success, *out_buf is a heap pointer of *out_size bytes that the caller frees with axl_free.

On failure, *out_buf is set to NULL and *out_size is set to 0. The buffer is allocated with one extra byte beyond *out_size and zeroed there, so callers that read string variables can dereference (char *)*out_buf as a NUL-terminated C string when the variable’s payload doesn’t already include a trailing NUL.

Parameters:
  • ns – namespace (e.g., “global”, “app”)

  • key – variable name (UTF-8)

  • out_buf – [out] heap buffer, caller frees with axl_free

  • out_size – [out] payload size in bytes (excluding the trailing NUL)

Returns:

AXL_OK on success, AXL_ERR on any error (variable not found, allocation failed, namespace not registered, etc.).

int axl_nvstore_set(const char *ns, const char *key, const void *buf, size_t size, uint32_t flags)

Write a value to non-volatile storage.

Passing flags == 0 (or AXL_NV_VOLATILE alone) defaults to AXL_NV_BOOT — UEFI rejects SetVariable with attribute mask 0 for non-delete writes, so the implementation substitutes boot-services access as the minimal sensible default.

Parameters:
  • ns – namespace

  • key – variable name (UTF-8)

  • buf – data to write

  • size – data size in bytes

  • flags – AXL_NV_* flags (0 → AXL_NV_BOOT)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_nvstore_set_str(const char *ns, const char *key, const char *str, uint32_t flags)

Write a NUL-terminated string to non-volatile storage.

Convenience over axl_nvstore_set: the payload size is axl_strlen(str) + 1, including the trailing NUL, so callers don’t need to repeat the +1 ritual at every site that stores a string. The empty string "" writes a single NUL byte.

To delete a string variable, use axl_nvstore_delete — this function does not double as a deleter; str == NULL is an error so the two operations stay distinct.

Parameters:
  • ns – namespace

  • key – variable name (UTF-8)

  • str – NUL-terminated string to write (must be non-NULL)

  • flags – AXL_NV_* flags (0 → AXL_NV_BOOT)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_nvstore_get_str(const char *ns, const char *key, char **out_str)

Read a string-valued variable into a heap buffer.

Convenience over axl_nvstore_get_alloc for string variables. On success, *out_str is a NUL-terminated heap-allocated C string the caller frees with axl_free. The trailing NUL is guaranteed even if the firmware payload omitted one (the alloc path zero-extends by one byte).

On failure, *out_str is set to NULL.

No content validation is performed — if the variable holds binary data with embedded NULs, the returned string ends at the first NUL.

Parameters:
  • ns – namespace

  • key – variable name (UTF-8)

  • out_str – [out] heap C string, caller frees with axl_free

Returns:

AXL_OK on success, AXL_ERR on error (variable not found, allocation failed, namespace not registered, etc.).

int axl_nvstore_delete(const char *ns, const char *key)

Delete a variable from non-volatile storage.

Parameters:
  • ns – namespace

  • key – variable name (UTF-8)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_nvstore_get_attrs(const char *ns, const char *key, uint32_t *attrs)

Get a variable’s attribute flags.

Parameters:
  • ns – namespace

  • key – variable name (UTF-8)

  • attrs – [out] AXL_NV_* flags

Returns:

AXL_OK on success, AXL_ERR on error (variable not found, namespace not registered, etc.).

int axl_nvstore_iter(const char *ns, AxlNvstoreIterFn cb, void *ctx)

Iterate all keys in a namespace.

Walks all variables whose backend token matches the registered namespace’s token, invoking cb for each. Stops early if cb returns non-zero.

Parameters:
  • ns – namespace

  • cb – callback, called once per key

  • ctx – passed unchanged to cb

Returns:

0 if the walk completed, the callback’s non-zero value if it stopped early, or -1 if the namespace is not registered or the iterator failed.

AxlDriver

Typedefs

typedef void *AxlDriverHandle

Opaque handle to a loaded driver image.

axl-driver.h:

UEFI driver lifecycle — load, start, connect, disconnect, unload. No GLib equivalent (UEFI-specific).

AxlDriverHandle drv;
if (axl_driver_load("fs0:\\MyDriver.efi", &drv) == 0) {
    axl_driver_start(drv);
    axl_driver_connect(drv);
    // ... driver is active ...
    axl_driver_disconnect(drv);
    axl_driver_unload(drv);
}

Functions

int axl_driver_load(const char *path, AxlDriverHandle *handle)

Load a driver image from a file path.

Loads the .efi file into memory but does not start it.

Parameters:
  • path – path to .efi driver (UTF-8)

  • handle – [out] receives driver handle

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_load_buffer(const unsigned char *buf, size_t len, AxlDriverHandle *out_handle)

Load a driver image from a memory buffer.

Buffer-source counterpart to axl_driver_load. Calls gBS->LoadImage with SourceBuffer/SourceSize and no DevicePath, returning the resulting handle for use with axl_driver_set_load_options, axl_driver_start, and axl_driver_unload.

Used by tools that .incbin a companion driver into the app to ship as a single binary. For the higher-level AxlService case use axl_service_start_embedded — this primitive is for non-AxlService drivers that still need per-call LoadOptions or explicit handle tracking.

The driver is loaded but NOT started. The image’s LoadedImage->FilePath is left NULL; drivers that read FilePath at startup (some Driver-Binding-style drivers do, notably iPXE) will not work via this entry point — load them by path instead.

Parameters:
  • buf – driver image bytes (must be non-NULL)

  • len – length in bytes (must be > 0)

  • out_handle – [out] driver handle for set_load_options/start/unload

Returns:

AXL_OK on success (*out_handle is set); AXL_ERR on argument validation failure or LoadImage failure (*out_handle is set to NULL).

int axl_driver_start(AxlDriverHandle handle)

Start a loaded driver image.

Calls the driver’s entry point. The driver registers its binding protocol(s) but does not yet bind to devices.

Parameters:
  • handle – driver handle from axl_driver_load

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_connect(AxlDriverHandle handle)

Connect a driver to all matching device handles.

Triggers the driver’s Supported/Start sequence for each compatible device. Call after axl_driver_start.

Parameters:
  • handle – driver handle

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_disconnect(AxlDriverHandle handle)

Disconnect a driver from all devices.

Triggers the driver’s Stop sequence for each bound device.

Parameters:
  • handle – driver handle

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_unload(AxlDriverHandle handle)

Unload a driver image from memory.

The driver must be disconnected first. Also frees any load-options copy installed via axl_driver_set_load_options() on this handle — the firmware retains the LoadOptions pointer for the loaded-image lifetime, so the AXL-side copy must be released here. Release runs BEFORE gBS->UnloadImage so a UnloadImage failure still doesn’t leak the copy.

Parameters:
  • handle – driver handle

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_set_load_options(AxlDriverHandle handle, const void *data, size_t size)

Set load options on a loaded driver image.

Provides configuration data (e.g., a URL) that the driver reads from EFI_LOADED_IMAGE_PROTOCOL.LoadOptions during startup. The data is copied internally — caller’s buffer can be freed after. The copy is owned by AXL and freed by axl_driver_unload() (or by a subsequent set on the same handle, which replaces the previous copy). Pass NULL data to clear load options and free any previous copy. Call between axl_driver_load and axl_driver_start.

AXL tracks at most 16 outstanding driver handles with load options — a 17th call returns AXL_ERR and frees the would-be copy without touching firmware state. Sequential load/unload is the realistic case; if you legitimately need more concurrent driver instances with load options, bump LOAD_OPTIONS_TABLE_SIZE in src/util/axl-driver.c.

Parameters:
  • handle – driver handle from axl_driver_load

  • data – option data (copied; NULL to clear)

  • size – option data size in bytes

Returns:

AXL_OK on success, AXL_ERR on bad arguments, alloc failure, HandleProtocol failure, or tracking-table-full.

void axl_driver_init(AxlHandle image_handle, AxlSystemTable *system_table)

Initialize the AXL runtime for a DXE driver.

Drivers don’t use AXL_APP / int main(). Call this from DriverEntry to set up firmware table pointers (gST/gBS/gRT) and I/O streams so axl_printf, axl_malloc, etc. work.

Most drivers don’t need to call this directly — the AXL_DRIVER(entry, unload) macro in <axl.h> emits the DriverEntry stub and wires axl_driver_init automatically. Use this manual path only when your driver publishes spec-defined UEFI protocols (EFI_SIMPLE_FILE_SYSTEM_PROTOCOL, EFI_BLOCK_IO_PROTOCOL, etc.) and you’ve opted into <uefi/axl-uefi.h> for the spec types — in that case cast the firmware-supplied EFI_HANDLE / EFI_SYSTEM_TABLE * to the AXL parameter types at the call site (the underlying pointers are bit-identical; the cast is a typing-only formality).

// AXL-only driver:
static int my_main(AxlHandle image, AxlSystemTable *st);
static int my_unload(AxlHandle image);
AXL_DRIVER(my_main, my_unload)

// Spec-protocol publisher (tier 2):
EFI_STATUS EFIAPI DriverEntry(EFI_HANDLE h, EFI_SYSTEM_TABLE *st) {
    axl_driver_init((AxlHandle)h, (AxlSystemTable *)st);
    ...
}
Parameters:
  • image_handle – image handle from DriverEntry

  • system_table – system table from DriverEntry

int axl_driver_set_unload(void *unload_fn)

Set the unload callback for the current driver image.

Call from DriverEntry to register a cleanup function that runs when the driver is unloaded. The callback has EFIAPI calling convention — declare it as: EFI_STATUS EFIAPI MyUnload(EFI_HANDLE ImageHandle)

Cleanup contract — what the firmware does NOT do for you:

  • Services registered via axl_protocol_register / axl_protocol_register_multiple are NOT auto-released. The AXL protocol registry never owned the install; it issued a gBS->InstallProtocolInterface and forgot. The unload callback must walk every protocol the driver published and call axl_protocol_unregister for each. Forgetting leaves dangling handle entries that point at freed driver memory — subsequent LocateProtocol calls hand consumers a stale vtable and the next dispatch faults.

  • Heap allocations made via axl_malloc are not auto-freed. axl_mem_dump_leaks (DEBUG builds) prints what was missed.

  • Events / timers created via the AxlLoop or backend layer stay live; close them with the matching _close calls.

sdk/examples/driver.c shows the canonical shape.

Parameters:
  • unload_fn – EFIAPI unload function pointer

Returns:

AXL_OK on success, AXL_ERR on error.

char *axl_driver_get_load_options(void)

Get the load options that were passed to the current image.

Returns a UTF-8 copy of the load options string. Caller frees with axl_free(). Useful for drivers that receive configuration (e.g., a URL) via LoadOptions.

Returns:

options string, or NULL if no options or on error.

int axl_driver_get_load_options_raw(const void **out_buf, size_t *out_size)

Get the LoadOptions buffer as raw bytes (no encoding conversion).

UEFI shell launches pass LoadOptions as a UCS-2 string — axl_driver_get_load_options is the right entry point for that. Programmatic loaders (axl_driver_set_load_options) pass arbitrary bytes; this entry point hands them back unchanged.

Used by AxlService to read its UTF-8 axl_config_to_string payload without misinterpreting it as UCS-2.

Parameters:
  • out_buf – [out] borrowed pointer into LoadedImage

  • out_size – [out] LoadOptionsSize in bytes

Returns:

AXL_OK on success (out params populated with a borrowed pointer into the firmware’s LoadedImage struct — do NOT free), AXL_ERR if the image has no LoadOptions or HandleProtocol failed.

char *axl_driver_get_image_path(void)

Get the filesystem path the current image was loaded from.

Returns a UTF-8 path like “fs0:\drivers\MyDriver.efi”. Useful for finding companion files next to the driver. Caller frees with axl_free().

Returns:

path string, or NULL if unavailable.

int axl_driver_connect_handle(void *handle)

Connect controllers on a specific handle.

Triggers driver binding for one handle (e.g., after installing a filesystem protocol on a new handle). More targeted than axl_driver_connect which reconnects all handles.

Parameters:
  • handle – handle to connect (from axl_protocol_register, etc.)

Returns:

AXL_OK on success, AXL_ERR on error.

int axl_driver_locate(const char *driver_name, char *out, size_t out_size)

Find a driver file on disk without loading it.

Walks the same search order axl_driver_ensure() uses (image’s drivers/<arch>/, image’s own directory, image’s drivers/, other volumes’ drivers/<arch>/) and writes the first matching existing path to out.

Useful when the caller needs to control the LoadImage / StartImage lifecycle directly — for example, to set load options between the two for a driver that takes per-invocation configuration:

char path[256];
if (axl_driver_locate("axl-webfs-dxe.efi", path, sizeof(path)) != 0) {
    axl_printf("axl-webfs-dxe.efi not found\n");
    return 1;
}
AxlDriverHandle h;
axl_driver_load(path, &h);
axl_driver_set_load_options(h, url_w, url_size);
axl_driver_start(h);

Same trust caveat as axl_driver_ensure: searches every mounted FAT volume. Don’t pass attacker-controlled driver_name.

Parameters:
  • driver_name – driver filename (e.g. “axl-webfs-dxe.efi”)

  • out – [out] receives full path on success

  • out_size – capacity of out in bytes

Returns:

AXL_OK on success (path written to out), AXL_ERR if the driver wasn’t found or out is too small to hold the result.

int axl_driver_ensure(const AxlGuid *protocol_guid, const char *driver_name)

Ensure a protocol-providing driver is loaded.

If protocol_guid is already registered (LocateProtocol succeeds), returns 0 immediately. Otherwise searches for driver_name and loads + starts the first match found, in this order:

  1. drivers/<arch>/<driver_name> on the volume the running image booted from

  2. <image_dir>/<driver_name> in the running image’s own directory

  3. drivers/<driver_name> at the running image’s volume root

  4. drivers/<arch>/<driver_name> on every other mounted FAT volume

The arch suffix is “x64” or “aa64”, matching the running image’s architecture. After load+start, LocateProtocol is re-checked; if the protocol still isn’t registered, the driver is unloaded and the function returns -1.

Safe to call multiple times — repeats short-circuit at step 1. EFI_ALREADY_STARTED on StartImage is treated as success.

Typical use, before touching a driver-provided protocol. Note the cast: EFI_RAM_DISK_PROTOCOL_GUID is an EFI_GUID from the generated UEFI headers; AxlGuid is layout-compatible, so a const cast lets callers pass it through without including any UEFI headers in their own public surface:

if (axl_driver_ensure((const AxlGuid *)&EFI_RAM_DISK_PROTOCOL_GUID,
                      "RamDiskDxe.efi") != 0) {
    axl_printf("RamDiskDxe.efi not available\n");
    return 1;
}

Trust model. This function will load the first matching .efi file off any mounted FAT volume — including a USB stick the user just plugged in. UEFI executes loaded drivers with full firmware privileges. Only call this with driver names you trust, and don’t call it with attacker-controlled driver_name values.

Parameters:
  • protocol_guid – protocol GUID to look up (must be non-NULL)

  • driver_name – driver filename (e.g. “RamDiskDxe.efi”)

Returns:

AXL_OK if the protocol is registered (was already, or after loading the driver); AXL_ERR if the driver wasn’t found, failed to load/start, or didn’t register the protocol after starting.

int axl_driver_ensure_with_embedded(const AxlGuid *protocol_guid, const char *driver_name, const unsigned char *embedded_buf, size_t embedded_len, const char *override_name, const void *load_options, size_t load_options_size)

Ensure a protocol-providing driver is loaded, with embedded fallback and optional caller override.

Generalizes axl_driver_ensure() for tools that ship a driver blob baked into the .efi binary itself, so they work as self-contained binaries on minimal firmware that omits the corresponding optional UEFI 2.6+ DXE module.

Resolution order:

  1. LocateProtocol(protocol_guid) — short-circuit if firmware already provides the protocol. Most OEM firmware (Dell, HP, Supermicro) ships RamDiskDxe and similar in their firmware volume; this is the common path.

  2. If override_name is non-NULL, search disk for that name only using axl_driver_ensure()’s 4-path search. The embedded blob is NOT used as a fallback — caller explicitly opted into a specific external driver.

  3. Otherwise, search disk for driver_name using the same 4-path search. If found, load + start it.

  4. If still not registered and embedded_buf is non-NULL, call LoadImage(SourceBuffer=embedded_buf, SourceSize=embedded_len) followed by StartImage. No filesystem access.

The embedded path is the safety net — it lets the tool work on firmware that ships neither the protocol nor a user-staged copy.

axl_driver_ensure(g, n) is exactly

axl_driver_ensure_with_embedded(

g, n, NULL, 0, NULL, NULL, 0)

.

LoadOptions (load_options / load_options_size): when non-NULL, AXL installs the bytes into the loaded image’s EFI_LOADED_IMAGE_PROTOCOL.LoadOptions BEFORE StartImage is called, via the same axl_driver_set_load_options path (so unload-time release is automatic). Applied on BOTH the disk-load path (steps 2/3) and the embedded path (step 4). Skipped on the step-1 short-circuit — the firmware-provided protocol implies a driver instance the consumer doesn’t own. Used by AxlService to ship a foreground process’s options through to the driver image (typically via axl_config_to_string).

Trust caveat (same as axl_driver_ensure): step 3 will load the first matching .efi off any mounted FAT volume. Don’t pass attacker- controlled driver_name or override_name. The embedded buffer is whatever the build system baked in — caller’s responsibility to verify provenance.

Parameters:
  • protocol_guid – protocol GUID to look up (must be non-NULL)

  • driver_name – canonical driver filename, e.g. “RamDiskDxe.efi”

  • embedded_buf – embedded .efi bytes (may be NULL)

  • embedded_len – length of embedded_buf in bytes (0 if NULL)

  • override_name – user-provided override name (may be NULL)

  • load_options – LoadOptions to install pre-Start (may be NULL)

  • load_options_size – size of load_options in bytes (0 if NULL)

Returns:

AXL_OK if the protocol is registered (was already, or after loading); AXL_ERR if all four steps failed.

int axl_driver_load_dir(const char *dir_path, const char *pattern, size_t *loaded_count)

Load, start, and connect all .efi drivers in a directory.

Scans dir_path for files matching pattern (glob, e.g. “*.efi”). Each matching file is loaded, started, and connected. On return, loaded_count receives the number of drivers successfully started. Pass NULL for pattern to match all .efi files.

Parameters:
  • dir_path – directory to scan (UTF-8)

  • pattern – glob pattern (NULL = “*.efi”)

  • loaded_count – [out] number of drivers loaded (may be NULL)

Returns:

AXL_OK on success (even if no drivers found), AXL_ERR on error.

AxlEmbed

Defines

AXL_EMBED_DECLARE(name)

Declare extern symbols for a link-time embedded blob.

axl-embed.h:

Helper macros for declaring and using arbitrary binary blobs embedded into a UEFI image at link time. The framework is content-agnostic — driver .efi images are the canonical use case (paired with axl_service_start_embedded or axl_driver_load_buffer) but anything works:

  • Trust material (CA bundles, public keys) parsed at startup

  • Static config files (JSON5, key=value) parsed at startup

  • HTML / CSS / JS for an embedded HTTP server

  • Lookup tables, license text, calibration data — anything

The blob is supplied at link time by axl-cc --embed PATH[=symbol] — axl-cc generates a .s file with .incbin and links it for you. (AXL’s own build system uses an internal EMBED_BLOB Makefile function for the same purpose; the symbol convention is shared.)

Either way, in C the consumer writes:

AXL_EMBED_DECLARE(greeting);

axl_printf("%.*s",
           (int)AXL_EMBED_SIZE(greeting),
           (const char *)AXL_EMBED_DATA(greeting));

For binary blobs (e.g. an embedded driver image), pass AXL_EMBED_DATA / AXL_EMBED_SIZE directly to AxlServiceDeploy.driver_blob / axl_driver_load_buffer — no cast needed.

The bare name argument names the blob in C terms (e.g. greeting); the macros prepend the axl_embedded_ prefix that the linker symbols actually use, so the prefix appears in exactly one place (here).

See sdk/examples/embed-asset.c for a non-driver worked example and sdk/examples/service-demo/launch.c for the driver case.

Emits the axl_embedded_<name> and axl_embedded_<name>_end extern declarations the linker resolves against the .incbin sidecar (or the .S that axl-cc --embed generates).

Use at file scope. Pair with AXL_EMBED_DATA and AXL_EMBED_SIZE for read access.

AXL_EMBED_DATA(name)

Pointer to the first byte of an embedded blob.

Result type is const unsigned char *. Pair with AXL_EMBED_DECLARE.

AXL_EMBED_SIZE(name)

Size of an embedded blob in bytes.

Result type is size_t. Pair with AXL_EMBED_DECLARE.

AxlDiag

Functions

void axl_diag_startup(int argc, char **argv)

Dump image-launch state to axl_printf if AXL_DIAG is set.

axl-diag.h:

Diagnostic helpers for tools — dump image-launch state, probe protocol registration. Intended to be called from a tool’s -v / &#8212;

verbose code path so users can answer “what does my tool

see at startup on this firmware?” without having to recompile.

if (verbose) {
    axl_diag_startup(argc, argv);
    axl_diag_probe_protocol(&MY_PROTOCOL_GUID, "MY_PROTOCOL");
}

Prints six labelled sections covering everything a tool typically wants to know on first boot of unfamiliar firmware:

  • POSIX argc/argv — what reached main after axl-app parsed EFI_LOADED_IMAGE_PROTOCOL.LoadOptions.

  • LOADOPT — the raw LoadOptions UCS-2 buffer, decoded as UTF-8. Mismatch with POSIX argv would mean axl-app’s tokenizer got confused by quoting or unusual whitespace.

  • SHELLEFI_SHELL_PARAMETERS_PROTOCOL probe + its argv if available. Optional protocol; some firmwares (some OEM platforms before fix) don’t publish it for cross-volume invocations.

  • IMG — image path (where the running .efi was loaded from).

  • VOLUMES — mounted FAT volumes with their fsN names. These are the search anchors for axl_driver_ensure / axl_driver_locate.

Activation: gated on the AXL_DIAG shell environment variable. Set to any non-empty value (e.g. set AXL_DIAG 1) to enable; unset or empty silences the dump entirely. This frees the -v short flag in tools to carry their counterpart’s Linux semantics (e.g. grep -v = invert match) instead of being hijacked for cross-tool framework diagnostics.

Tools should call this unconditionally near the top of their main handler — the env-var check happens internally. No-op when unset.

Output is plain text via axl_printf; no allocation beyond the UTF-8 conversion buffers (auto-freed). Safe to call from any application context; not reentrant — don’t call from a log handler.

Parameters:
  • argc – POSIX argc as received by main

  • argv – POSIX argv as received by main

int axl_diag_probe_protocol(const AxlGuid *protocol_guid, const char *display_name)

Probe whether a protocol is currently registered.

Calls LocateProtocol(@p protocol_guid) and prints a one-line status to axl_printf:

PROBE: <display_name> ALREADY REGISTERED (LocateProtocol=0x0)
PROBE: <display_name> NOT registered (LocateProtocol=0xE0...0E)

Useful around axl_driver_ensure calls to show the before/after state of a driver-provided protocol — tells you whether the firmware had it baked in (short-circuit) or whether ensure actually loaded a driver from disk.

Parameters:
  • protocol_guid – protocol GUID to probe

  • display_name – short tag for the printed line

Returns:

AXL_OK if registered, AXL_ERR otherwise.

AxlHexdump

Defines

AXL_HEX_GROUP_BYTE

axl-hexdump.h:

Formatted hex+ASCII dump with configurable grouping. Supports direct console output and log integration.

AXL_HEX_GROUP_WORD
AXL_HEX_GROUP_DWORD
AXL_HEX_GROUP_QWORD
AXL_HEXDUMP_MAX_SIZE
AxlHexDumpLog(Level, Name, Data, Size, BytesPerLine, GroupSize)

Convenience macro that injects _AxlLogDomain, func, LINE.

Requires AXL_LOG_DOMAIN() in the source file.

Functions

void axl_hexdump(const char *name, const void *data, size_t size, size_t bytes_per_line, size_t group_size)

Print a hex+ASCII dump to stdout via axl_print().

Parameters:
  • name – label printed above the dump (may be NULL)

  • data – buffer to dump

  • size – number of bytes

  • bytes_per_line – columns (0 = default 16, max 64)

  • group_size – grouping width (AXL_HEX_GROUP_*)

void axl_hexdump_to_log(int level, const char *domain, const char *func, int line, const char *name, const void *data, size_t size, size_t bytes_per_line, size_t group_size)

Emit a hex+ASCII dump through axl_log_full().

Parameters:
  • level – log level (AXL_LOG_ERROR..AXL_LOG_TRACE)

  • domain – log domain

  • funcfunc

  • lineLINE

  • name – label printed above the dump (may be NULL)

  • data – buffer to dump

  • size – number of bytes

  • bytes_per_line – columns (0 = default 16, max 64)

  • group_size – grouping width (AXL_HEX_GROUP_*)