Files
libakerror/AGENTS.md
Andrew Kesterson be24f80022 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

5.6 KiB

Repository Guidelines

Project Structure & Module Organization

This is a small C library built with CMake. Core implementation lives in 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.

Build, Test, and Development Commands

Use an out-of-tree build:

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:

ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"

The suite includes thread tests. The run that proves there is no data race under them is ThreadSanitizer:

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 works the same way. Under a sanitized build a report fails the test rather than being printed and passed over.

Mutation testing is available through:

cmake --build build --target mutation
scripts/mutation_test.py --target src/error.c --threshold 65

Code coverage is available through:

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 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.

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.

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.

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 CTest suite before submitting changes, and run mutation testing and coverage when changing core control-flow, reference counting, stack-trace, or handler behavior.

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.

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.