akerr_reserve_status_range() and akerr_register_status_name() returned private int enumerations, which was the one place in the library where a failure was not an akerr_ErrorContext *. They now return one like everything else: NULL on success, and on refusal an error whose status is a real code in the library's reserved band, so it can be CATCH-ed, HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are marked AKERR_NOIGNORE, so discarding the result warns at compile time. AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining seven codes move into the AKERR_* offset span and get registered names. AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span in the reserved-band static assert and the exhaustiveness sweep. The refusal detail that used to go straight to akerr_log_method now travels in the error message, so a caller that handles the error decides whether it is reported. The two-argument akerr_name_for_status() set path is the exception: it returns a name and cannot raise, so it logs and releases. akerr_init() likewise has no caller to raise into, so failing to reserve its own band or name its own codes is logged and fatal -- that can only happen on a misconfigured build, and continuing would degrade every later stack trace to "Unknown Error". Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and rewrite its return-code tables in terms of the statuses now raised. Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5% (was 77.6%; the new survivors are the fatal init path, which needs a library built with an undersized name table -- TODO item 7). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.0 KiB
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 anyint, 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; useakerr_name_for_status().AKERR_STATUS_RANGE_OKandAKERR_STATUS_NAME_OK— the registry functions no longer return anint. Success is aNULLakerr_ErrorContext *, like every other function in the library. See "The registry raises errors" below.
To migrate:
- Rebuild libakerror and every dependent library against the new header.
- Move custom status codes out of the reserved
0–255band. Use fixed integer constants beginning atAKERR_FIRST_CONSUMER_STATUS(256) rather than offsets fromAKERR_LAST_ERRNO_VALUE, so a libc that grows an errno cannot move your codes. - Assign a distinct range to every library that may coexist in one process, and coordinate those ranges at the application or dependency-stack level.
- Reserve the complete range with
akerr_reserve_status_range()during initialization, and treat the error it returns like any other error:CATCHit,PASSit, or let it propagate out of your init function. - 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:
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.
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 thatstatusfall in a range reserved byowner. Naming another component's status fails withAKERR_STATUS_NAME_FOREIGN, and naming a status nobody reserved fails withAKERR_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.
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=<power of two> |
| Reserved ranges | 64 | -DAKERR_MAX_RESERVED_STATUS_RANGES=<n> |
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:
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.