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:
185
README.md
185
README.md
@@ -4,154 +4,13 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
||||
|
||||

|
||||
|
||||
## Upgrade notice: custom status codes (1.0.0)
|
||||
## Upgrading from a pre-1.0.0 release
|
||||
|
||||
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.
|
||||
1.0.0 replaced the consumer-sized status-name array with a private, ownership-
|
||||
enforced registry. That is a source and ABI break: see
|
||||
[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity
|
||||
limits and how to raise them, and the thread-safety rules.
|
||||
|
||||
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()`.
|
||||
|
||||
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. Treat any result other than `AKERR_STATUS_RANGE_OK` as an
|
||||
initialization failure.
|
||||
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"
|
||||
|
||||
int mylib_init(void)
|
||||
{
|
||||
if (akerr_reserve_status_range(MYLIB_ERR_BASE,
|
||||
MYLIB_ERR_LIMIT - MYLIB_ERR_BASE,
|
||||
MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||||
return MYLIB_INIT_FAILED; /* another component owns part of the range */
|
||||
}
|
||||
|
||||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_PARSE, "Parse Error");
|
||||
akerr_register_status_name(MYLIB_OWNER, MYLIB_ERR_STORAGE, "Storage Error");
|
||||
return MYLIB_INIT_OK;
|
||||
}
|
||||
```
|
||||
|
||||
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 through `akerr_log_method` and names the real owner,
|
||||
because a name that fails to register degrades that code to `"Unknown Error"` in
|
||||
every later stack trace. Registration failures are not fatal by themselves —
|
||||
check the return value during initialization if you want them to be.
|
||||
|
||||
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.
|
||||
|
||||
### Return codes
|
||||
|
||||
`akerr_reserve_status_range()`:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `AKERR_STATUS_RANGE_OK` (0) | 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 (logged, with 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()`:
|
||||
|
||||
| Code | Meaning |
|
||||
| --- | --- |
|
||||
| `AKERR_STATUS_NAME_OK` (0) | 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 |
|
||||
|
||||
Repeating an identical reservation for the same owner is a no-op. A *subset* or
|
||||
*superset* of your own range is not — it is reported as an overlap. Reserve the
|
||||
whole range in one call.
|
||||
|
||||
### 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 is reported through `akerr_log_method` and returned 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.
|
||||
|
||||
# Why?
|
||||
|
||||
@@ -220,13 +79,26 @@ names are stored sparsely, so any `int` is a legal status and no compile
|
||||
definition is needed to use large values.
|
||||
|
||||
Every library that may coexist in one process must reserve its range during
|
||||
initialization and treat any nonzero result as a startup error:
|
||||
initialization. `akerr_reserve_status_range()` and
|
||||
`akerr_register_status_name()` report failure the way everything else in this
|
||||
library does — they return `akerr_ErrorContext *`, and they are marked
|
||||
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
||||
`PASS` up out of your initialization:
|
||||
|
||||
```c
|
||||
#define MYLIB_OWNER "my-library"
|
||||
|
||||
if (akerr_reserve_status_range(256, 16, MYLIB_OWNER) != AKERR_STATUS_RANGE_OK) {
|
||||
/* Refuse to initialize: another component owns part of the range. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
/* Another component owning part of the range propagates to our caller. */
|
||||
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||
|
||||
/* Then name each code, quoting the owner you reserved with. */
|
||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
||||
"Some Error Code Description"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -234,15 +106,12 @@ Reservations are process-local, fixed-capacity, and idempotent when the same
|
||||
owner repeats the exact same range. They preserve compile-time integer constants
|
||||
so `HANDLE` still works, since `case` labels require them.
|
||||
|
||||
Then name each code, quoting the owner you reserved with:
|
||||
|
||||
```c
|
||||
akerr_register_status_name(MYLIB_OWNER, 256, "Some Error Code Description");
|
||||
```
|
||||
|
||||
Naming a status outside your reservation is refused and logged. See
|
||||
[the upgrade notice](#upgrade-notice-custom-status-codes-100) for the return
|
||||
codes, the capacity limits and how to raise them, and thread-safety rules.
|
||||
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
||||
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
||||
rules.
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
Reference in New Issue
Block a user