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>
209 lines
9.0 KiB
Markdown
209 lines
9.0 KiB
Markdown
# 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=<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:
|
||
|
||
```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.
|