Split the README reference material into docs/
The README was 794 lines: the summary, the design rationale, the whole macro reference, the threading contract, the build internals and the exit-status specification in one file. It is now 178 lines -- summary, installation, quickstart, and an index -- and the reference material lives in docs/, one file per topic: architecture, usage, status-codes, uncaught-errors, exit-status, thread-safety, building. The prose moved as written. Inbound references followed it: UPGRADING.md, TODO.md, include/akerror.tmpl.h and tests/err_threads_handoff.c now name the docs/ file that owns the text they cite, and AGENTS.md says where new documentation goes so the README does not grow back. Five factual errors fixed in the moved text: - Both NULL-pointer examples inverted their test. FAIL_ZERO_* fails when the expression is zero, so `(somePointer == NULL)` failed on a *valid* pointer. They now read `(somePointer != NULL)`. - AKERROR_NOIGNORE, four times including the #define, is AKERR_NOIGNORE. - FINISH_NORExbTURN is FINISH_NORETURN. - "functiions" is "functions". - The architecture link pointed at include/akerror.h, which is generated and not in the tree; it points at include/akerror.tmpl.h. The quickstart is new text. It compiles under -Wall -Wextra -Werror and was run through all three of its paths: handled usage error exits 0, unhandled IO error prints a trace and exits with the status, success exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
docs/exit-status.md
Normal file
76
docs/exit-status.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Exit status
|
||||
|
||||
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
||||
|
||||
```c
|
||||
void akerr_exit(int status);
|
||||
```
|
||||
|
||||
This applies everywhere you are leaving the process on account of a status, not
|
||||
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
||||
initialization routine that cannot continue, a `main()` that ends by reporting
|
||||
the status it finished with. One function owns the mapping so that a given
|
||||
status produces the same exit code no matter which of your exits it left by.
|
||||
|
||||
The mapping exists because a process exit status is one byte wide. `exit()`
|
||||
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
||||
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
||||
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
||||
truncated value because the truncation happened before the parent looked. There
|
||||
is no wider `exit()` to reach for.
|
||||
|
||||
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
||||
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
||||
status can be an exit code:
|
||||
|
||||
```
|
||||
status exit code
|
||||
0 0 (success)
|
||||
1 .. 255 the status
|
||||
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||
```
|
||||
|
||||
Passing the low byte instead would have made status 256 exit 0 and report
|
||||
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
||||
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
||||
belong to the shell.
|
||||
|
||||
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
||||
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
||||
0`, which marks a zero status handled, so a successful context cannot reach
|
||||
`FINISH_NORETURN`'s call to the handler at all.
|
||||
|
||||
Above 255, the exit code tells you the process died of an error, not which one.
|
||||
125 is inside the library's reserved band and so is also some host's `errno`, and
|
||||
every status above 255 collapses onto it. **The stack trace is what identifies
|
||||
the error** — it is printed before the handler runs, carrying the status at full
|
||||
width along with its registered name.
|
||||
|
||||
## Replacing the handler
|
||||
|
||||
After the trace is printed, `FINISH_NORETURN` calls
|
||||
`akerr_handler_unhandled_error`. The default implementation,
|
||||
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
||||
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
||||
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
||||
`akerr_exit()` so the exit code still means what it means everywhere else:
|
||||
|
||||
```c
|
||||
static void mylib_handler(akerr_ErrorContext *e)
|
||||
{
|
||||
mylib_flush_telemetry();
|
||||
if ( e == NULL ) {
|
||||
akerr_exit(AKERR_API);
|
||||
}
|
||||
akerr_exit(e->status);
|
||||
}
|
||||
|
||||
akerr_handler_unhandled_error = &mylib_handler;
|
||||
```
|
||||
|
||||
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
||||
ending in one of those calls is complete rather than falling off the end.
|
||||
|
||||
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
||||
installs one that does not exit at all, which is how the test suite asserts on
|
||||
unhandled errors without dying.
|
||||
Reference in New Issue
Block a user