2026-07-29 17:09:45 -04:00
|
|
|
# Repository Guidelines
|
|
|
|
|
|
|
|
|
|
## Project Structure & Module Organization
|
|
|
|
|
|
|
|
|
|
This is a small C library built with CMake. Core implementation lives in
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
`src/error.c`. `src/lock.h` is private: it selects the threading backend
|
|
|
|
|
(pthread or none) and is not installed. The public header is generated at build
|
|
|
|
|
time from `include/akerror.tmpl.h` by `scripts/generrno.sh`, which also
|
|
|
|
|
generates `src/errno.c` under the build directory and stamps in whether the
|
|
|
|
|
build is thread safe. CMake package and pkg-config templates are in `cmake/` and
|
|
|
|
|
`akerror.pc.in`. Tests are one-file C programs in `tests/`; shared test helpers
|
|
|
|
|
live beside them, such as `tests/err_capture.h` and `tests/err_threads.h`.
|
2026-07-29 17:09:45 -04:00
|
|
|
|
|
|
|
|
## Build, Test, and Development Commands
|
|
|
|
|
|
|
|
|
|
Use an out-of-tree build:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cmake -S . -B build
|
|
|
|
|
cmake --build build
|
|
|
|
|
ctest --test-dir build --output-on-failure
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`cmake -S . -B build` configures the build and generates `akerror.h`.
|
|
|
|
|
`cmake --build build` compiles `libakerror` and test executables. `ctest`
|
|
|
|
|
runs the registered unit tests. To emit CI-style results, use:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
|
|
|
|
```
|
|
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
The suite includes thread tests. The run that proves there is no data race under
|
|
|
|
|
them is ThreadSanitizer:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
scripts/thread_test.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
|
|
|
|
|
*and* the tests with it, and runs CTest with ASLR disabled (TSan aborts with an
|
|
|
|
|
"unexpected memory mapping" on kernels with `vm.mmap_rnd_bits` above 28).
|
|
|
|
|
`AKERR_SANITIZE` takes any sanitizer list, so `-DAKERR_SANITIZE=address,undefined`
|
2026-07-31 08:45:20 -04:00
|
|
|
works the same way. CTest gives each test `halt_on_error=1`, so a report fails
|
|
|
|
|
the test rather than being printed and passed over — running a sanitized test
|
|
|
|
|
binary by hand does **not** inherit that. Set it yourself
|
|
|
|
|
(`TSAN_OPTIONS=halt_on_error=1 ./build/tsan/test_err_threads_pool`) or the
|
|
|
|
|
binary can print a race and still exit 0.
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
|
2026-07-29 17:09:45 -04:00
|
|
|
Mutation testing is available through:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cmake --build build --target mutation
|
|
|
|
|
scripts/mutation_test.py --target src/error.c --threshold 65
|
|
|
|
|
```
|
|
|
|
|
|
2026-07-30 01:48:07 -04:00
|
|
|
Code coverage is available through:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cmake --build build --target coverage
|
|
|
|
|
scripts/coverage.py --threshold 90 --branch-threshold 50
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`scripts/coverage.py` configures its own instrumented build tree (default
|
|
|
|
|
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
|
|
|
|
|
reports gcov line/branch coverage per library source. Thresholds gate each
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
file as well as the total. Coverage measures the library's own sources only --
|
|
|
|
|
`src/error.c`, `src/lock.h` and the generated `src/errno.c`; the public header's
|
|
|
|
|
macros expand at their call sites, so mutation testing is what checks those.
|
|
|
|
|
|
|
|
|
|
To build without threads (no locking, no thread-local storage, thread tests not
|
|
|
|
|
registered), configure with `-DAKERR_THREADS=none`. `auto` is the default and
|
|
|
|
|
fails the configure rather than falling back.
|
2026-07-30 01:48:07 -04:00
|
|
|
|
2026-07-29 17:09:45 -04:00
|
|
|
## Coding Style & Naming Conventions
|
|
|
|
|
|
|
|
|
|
Use C99-compatible C and follow the surrounding style. Functions and types use
|
|
|
|
|
the `akerr_` prefix; macros and constants use `AKERR_` or all-caps macro names
|
|
|
|
|
such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
|
|
|
|
|
scripts, not in build outputs. Preserve concise comments for invariants,
|
|
|
|
|
macro constraints, and non-obvious error lifecycle behavior.
|
|
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
**Locking.** One recursive lock (`akerr_state_lock`, see `src/lock.h`) covers
|
|
|
|
|
both the error pool and the status registry. A function named `*_locked` is
|
|
|
|
|
called with that lock already held; a function without the suffix takes it.
|
|
|
|
|
Never take it in a body written with the `FAIL_*_RETURN` macros — those return
|
|
|
|
|
from the middle of the function and would skip the unlock. Split it instead: the
|
|
|
|
|
locked body does the work, and a thin wrapper takes the lock, calls it, and
|
|
|
|
|
releases it on the single return path. Do not call consumer code
|
|
|
|
|
(`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it.
|
|
|
|
|
|
Stop an unhandled error from exiting zero
An unhandled error could kill the process and still report success. The
default handler ended in exit(errctx->status), and an exit status is one
byte wide: the kernel keeps the low 8 bits of the argument and discards
the rest. Consumer statuses start at AKERR_FIRST_CONSUMER_STATUS (256),
so the first status any consumer can reserve exited 0 and a shell saw a
clean run. Status 300 exited 44, an unrelated error's code.
There is no wider exit() to reach for. _exit(), _Exit(), quick_exit()
and the raw exit_group syscall all truncate identically, and even
waitid(), whose si_status is a full int, reports the truncated value --
the truncation happened before the parent looked.
akerr_exit() now owns that mapping and the default handler calls it: 0
exits 0, 1 through 255 exit the status, and anything else exits
AKERR_EXIT_STATUS_UNREPRESENTABLE (125) rather than a low byte that is
either a lie or a claim of success. Only values that were already being
delivered wrong behave differently. Call it instead of exit() anywhere
you leave the process on a status; it is declared AKERR_NORETURN.
akerr_exit(0) exits 0, because 0 is this library's success status. That
is not a hole in the rule: PROCESS opens with case 0, which marks a zero
status handled, so a successful context never reaches FINISH_NORETURN's
call to the handler at all.
tests/err_exit_status.c drives one table through akerr_exit() and
through the default handler in forked children and requires identical
exit codes, so the handler cannot grow a mapping of its own. With the
clamp removed it fails with "akerr_exit(256) exited 0, want 125". The
full-width status was already reaching the log and still does, which the
same test asserts against the captured stack trace.
2.0.1. No ABI break: the soname stays libakerror.so.2 and nothing that
already existed changed shape. akerr_exit() is a new exported symbol, so
a consumer that starts calling it needs 2.0.1 at link time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:25:37 -04:00
|
|
|
**Exiting.** Never call `exit()` with a status value. Call `akerr_exit()`, which
|
|
|
|
|
owns the one mapping from an akerr status to an exit code: an exit status is a
|
|
|
|
|
byte, and every consumer status starts at 256, so passing a status through
|
|
|
|
|
`exit()` silently truncates it — status 256 exits 0 and reports success. The
|
|
|
|
|
only exits that bypass it are the two that have no status to map, both in
|
|
|
|
|
`src/error.c`: `ENSURE_ERROR_READY`'s pool-exhaustion abort and the NULL-context
|
|
|
|
|
case in `akerr_default_handler_unhandled_error()`. This rule applies to test
|
|
|
|
|
programs too, except where the test's whole point is to observe the raw
|
|
|
|
|
truncation.
|
|
|
|
|
|
2026-07-29 17:09:45 -04:00
|
|
|
## Testing Guidelines
|
|
|
|
|
|
|
|
|
|
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
|
|
|
|
`AKERR_TESTS` list in `CMakeLists.txt`; tests that intentionally abort must
|
|
|
|
|
also be listed in `AKERR_WILL_FAIL_TESTS`. Prefer focused executable tests that
|
|
|
|
|
return zero on success and use existing helpers such as `AKERR_CHECK`. Run the
|
2026-07-30 01:48:07 -04:00
|
|
|
CTest suite before submitting changes, and run mutation testing and coverage
|
|
|
|
|
when changing core control-flow, reference counting, stack-trace, or handler
|
|
|
|
|
behavior.
|
2026-07-29 17:09:45 -04:00
|
|
|
|
Make the error pool and status registry thread safe
Every entry point may now be called from any thread. akerr_init() runs
exactly once however many threads race into it, the pool hands each slot
to exactly one thread, and reservations, registrations and lookups are
serialized against each other.
One recursive lock covers both tables (src/lock.h, private). Recursive
because raising an error re-enters the library -- FAIL needs a pool slot
and a status name -- and single because two locks would mean an ordering
to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN
macros are split into *_locked functions behind wrappers that take and
release the lock on one path; consumer callbacks are never called under
it.
This is an ABI break, hence 2.0.0 and SOVERSION 2:
- akerr_next_error() now returns a context that already holds its
reference. Finding a free slot and claiming it has to be one operation,
or two threads scanning at once are handed the same slot.
ENSURE_ERROR_READY no longer increments.
- __akerr_last_ignored is thread-local, as is the last-ditch context used
to report akerr_release_error(NULL).
The threading backend is chosen at configure time by AKERR_THREADS
(auto, pthread, none). auto fails the configure when it cannot find
POSIX threads rather than quietly building a library that reports itself
thread safe and is not. generrno.sh stamps the decision into the
generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree
with the library about it.
Tests: err_threads_init, err_threads_pool and err_threads_registry
assert exclusive slot ownership, exactly one winner for a contested
range, and every registered name readable back under contention.
AKERR_SANITIZE builds the library and the tests with any sanitizer;
scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs
it. Removing the pool lock makes both the sanitizer and the plain
assertions fail, so the tests are not vacuous.
Documented in README.md and UPGRADING.md, including what this does not
cover: renaming a status while another thread looks it up, and which of
two simultaneous unhandled errors sets the exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
|
|
|
Tests that drive the library from several threads go in the `AKERR_THREAD_SAFE`
|
|
|
|
|
branch of the `AKERR_TESTS` list — an `-DAKERR_THREADS=none` build has no
|
|
|
|
|
threading to test — and use `tests/err_threads.h`, which runs a body on
|
|
|
|
|
`AKERR_TEST_THREADS` threads that meet at a barrier first.
|
|
|
|
|
Count failures per thread with `AKERR_TCHECK` rather than returning early: a
|
|
|
|
|
thread that abandons its work leaves the others holding pool slots and turns one
|
|
|
|
|
failure into a cascade. Anything the test shares between its own threads must go
|
|
|
|
|
through `__atomic` builtins — a race in the test is still a race, and
|
|
|
|
|
ThreadSanitizer cannot tell you whose it is. The capturing logger in
|
|
|
|
|
`err_capture.h` is single-threaded (shared buffer, shared length); use
|
|
|
|
|
`akerr_thread_logger` instead. Run `scripts/thread_test.sh` when changing
|
|
|
|
|
anything that touches the pool, the registry, initialization, or the lock.
|
|
|
|
|
|
2026-07-29 17:09:45 -04:00
|
|
|
## Commit & Pull Request Guidelines
|
|
|
|
|
|
|
|
|
|
Recent commits use short, imperative, sentence-case subjects, for example
|
|
|
|
|
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
|
|
|
|
|
describe the observable behavior changed. Pull requests should include a brief
|
|
|
|
|
summary, tests run, and any compatibility impact for public macros, generated
|
|
|
|
|
headers, installation paths, or CMake/pkg-config consumers.
|
|
|
|
|
|
|
|
|
|
## Agent-Specific Instructions
|
|
|
|
|
|
|
|
|
|
Do not overwrite uncommitted user changes. Avoid editing generated files in
|
|
|
|
|
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
|
|
|
|
|
or scripts instead.
|