Raise errors from the status registry instead of returning codes
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>
This commit is contained in:
208
UPGRADING.md
Normal file
208
UPGRADING.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user