# Upgrade notice: custom status codes (1.0.0) Version 1.0.0 replaces the consumer-sized status-name array with a private registry, and makes status-code ownership explicit and enforced. This is a source and ABI break. The library now carries a version and an soname (`libakerror.so.1`), so a stale installed library can no longer be silently paired with a newer header — but anything built against a pre-1.0.0 header must be rebuilt. What was removed: * `AKERR_MAX_ERR_VALUE` — the registry is sparse and accepts any `int`, so consumers no longer size it. Delete every compile definition and source reference. A stale `-DAKERR_MAX_ERR_VALUE=...` is now harmless but useless. * `__AKERR_ERROR_NAMES` — the name table is private to the library. Code that touched this data symbol was using an undocumented interface; use `akerr_name_for_status()`. * `AKERR_STATUS_RANGE_OK` and `AKERR_STATUS_NAME_OK` — the registry functions no longer return an `int`. Success is a `NULL` `akerr_ErrorContext *`, like every other function in the library. See "The registry raises errors" below. To migrate: 1. Rebuild libakerror and every dependent library against the new header. 2. Move custom status codes out of the reserved `0`–`255` band. Use fixed integer constants beginning at `AKERR_FIRST_CONSUMER_STATUS` (256) rather than offsets from `AKERR_LAST_ERRNO_VALUE`, so a libc that grows an errno cannot move your codes. 3. Assign a distinct range to every library that may coexist in one process, and coordinate those ranges at the application or dependency-stack level. 4. Reserve the complete range with `akerr_reserve_status_range()` during initialization, and treat the error it returns like any other error: `CATCH` it, `PASS` it, or let it propagate out of your init function. 5. Register names with `akerr_register_status_name()`, passing the same owner string you reserved with. **You can no longer name a status you have not reserved** — see "Ownership is enforced" below. For example: ```c enum { MYLIB_ERR_BASE = AKERR_FIRST_CONSUMER_STATUS, /* 256 */ MYLIB_ERR_PARSE = MYLIB_ERR_BASE, MYLIB_ERR_STORAGE, MYLIB_ERR_LIMIT }; #define MYLIB_OWNER "mylib" akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void) { PREPARE_ERROR(errctx); /* Any collision propagates out of mylib_init() to the caller. */ PASS(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE, MYLIB_ERR_LIMIT - MYLIB_ERR_BASE, MYLIB_OWNER)); PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, "Parse Error")); PASS(errctx, akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, "Storage Error")); SUCCEED_RETURN(errctx); } ``` If your `init` cannot return an `akerr_ErrorContext *` — a C API with an `int` return, say — catch the error and convert it. Close with `FINISH_NORETURN` rather than `FINISH`: nothing can propagate out of a function that does not return an error context, and `FINISH(errctx, false)` in an `int`-returning function compiles the (dead) propagation branch anyway, which warns. ```c int mylib_init(void) { PREPARE_ERROR(errctx); int rc = MYLIB_INIT_OK; ATTEMPT { CATCH(errctx, akerr_reserve_status_range(MYLIB_ERR_BASE, MYLIB_ERR_LIMIT - MYLIB_ERR_BASE, MYLIB_OWNER)); } CLEANUP { } PROCESS(errctx) { } HANDLE_DEFAULT(errctx) { LOG_ERROR_WITH_MESSAGE(errctx, "mylib could not claim its status range"); rc = MYLIB_INIT_FAILED; /* another component owns part of the range */ } FINISH_NORETURN(errctx); return rc; } ``` You do not need to call `akerr_init()` first. Every registry entry point calls it for you, so a library that reserves its range before anything else in the process has touched libakerror keeps that reservation. (Before 1.0.0 this was silently destructive: `akerr_init()` cleared the tables, so a reservation made too early was discarded and the next component to claim the same range was told it was free.) ## Ownership is enforced Reserving a range is no longer advisory bookkeeping. A status may only be named from inside a reservation: * `akerr_register_status_name(owner, status, name)` requires that `status` fall in a range reserved by `owner`. Naming another component's status fails with `AKERR_STATUS_NAME_FOREIGN`, and naming a status nobody reserved fails with `AKERR_STATUS_NAME_UNRESERVED`. * The two-argument `akerr_name_for_status(status, name)` set path still works, but it cannot identify its caller, so it can only require that *some* reservation covers the status. Prefer the owned form: it is the one that catches a component writing into a range that is not its own. Every refusal is reported, because a name that fails to register degrades that code to `"Unknown Error"` in every later stack trace. This detects *name* collisions. It cannot detect two components compiling the same integer into a `HANDLE` label without ever registering a name, so every co-resident library should still reserve its range. ## The registry raises errors `akerr_reserve_status_range()` and `akerr_register_status_name()` return `akerr_ErrorContext *`, exactly like the rest of the library. They are marked `AKERR_NOIGNORE`, so discarding the result is a compile-time warning, and an error you catch but do not handle propagates out of your init function instead of leaving you with a range you do not actually own. ```c ATTEMPT { CATCH(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKERR_STATUS_RANGE_OVERLAP) { /* Another component owns part of it -- the message names which. */ } FINISH(errctx, true); ``` `akerr_reserve_status_range()` raises: | Status | Meaning | | --- | --- | | — (returns `NULL`) | Range reserved, or an identical range was already reserved by the same owner | | `AKERR_STATUS_RANGE_OVERLAP` | Part of the range is owned by someone else (the message names the owner) | | `AKERR_STATUS_RANGE_FULL` | No reservation slots remain | | `AKERR_STATUS_RANGE_INVALID` | `count < 1`, NULL/empty/over-long owner, or the range overflows `int` | `akerr_register_status_name()` raises: | Status | Meaning | | --- | --- | | — (returns `NULL`) | Name registered | | `AKERR_STATUS_NAME_UNRESERVED` | No reservation contains this status | | `AKERR_STATUS_NAME_FOREIGN` | The status is in a range owned by someone else | | `AKERR_STATUS_NAME_FULL` | The name registry is full | | `AKERR_STATUS_NAME_INVALID` | NULL/empty/over-long owner, or a NULL name | These are ordinary status codes inside the library's reserved band, so they can be matched with `HANDLE`, grouped with `HANDLE_GROUP`, and they print with a name and a stack trace when they go unhandled. `akerr_name_for_status(status, name)` is the one exception: it returns a name rather than an error context, so it cannot raise. A refused registration through that path is reported through `akerr_log_method` and reads back as `"Unknown Error"`. Repeating an identical reservation for the same owner is a no-op. A *subset* or *superset* of your own range is not — it raises `AKERR_STATUS_RANGE_OVERLAP`. Reserve the whole range in one call. The library holds itself to the same rule at startup: if `akerr_init()` cannot reserve its own `0`–`255` band or name one of its own codes, it logs the failure and terminates the program. That can only happen in a misconfigured build (a name table too small for the library's own entries), and the alternative is a process whose stack traces silently read `"Unknown Error"` for built-in codes. ## Capacity Both tables are fixed size, allocated in BSS, and never grow: | Limit | Default | Build-time override | | --- | --- | --- | | Status names | 3072 usable (4096 slots, 75% load) | `-DAKERR_STATUS_NAME_SLOTS=` | | Reserved ranges | 64 | `-DAKERR_MAX_RESERVED_STATUS_RANGES=` | `akerr_init()` consumes one name slot per host errno value plus one per `AKERR_*` code — around 150 on glibc, leaving roughly 2900 for all consumers in the process to share. That figure varies with the host libc, so treat it as approximate rather than a budget to fill. Both overrides are CMake cache variables and apply `PRIVATE` to the library target. The tables live entirely inside `src/error.c`, so raising them changes nothing a consumer can observe — unlike the `AKERR_MAX_ERR_VALUE` they replaced, where a mismatch between the library and its consumers corrupted memory. Set them when configuring libakerror itself: ```sh cmake -S . -B build -DAKERR_STATUS_NAME_SLOTS=16384 ``` Exhausting either table raises an error to the caller; it is never silent. ## Thread safety The registry is process-global mutable state with no locking. Reserve ranges and register names during single-threaded initialization, before spawning threads. Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently once registration has finished.