2026-01-10 10:20:35 -05:00
# ifndef _AKERR_H_
# define _AKERR_H_
2025-07-20 21:40:14 -04:00
2026-01-10 22:03:14 -05:00
# if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
2025-07-20 21:40:14 -04:00
# include <stdlib.h>
# include <stdbool.h>
# include <string.h>
# include <stdio.h>
2026-05-24 09:50:21 -04:00
# include <limits.h>
2026-01-04 22:56:31 -05:00
# endif
2025-07-20 21:40:14 -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
/*
* Threading .
*
* scripts / generrno . sh stamps this value in at build time from the AKERR_THREADS
* build option , the same way it stamps AKERR_LAST_ERRNO_VALUE . It is generated
* rather than defined by the consumer on purpose : whether the library
* serializes its global state and whether __akerr_last_ignored is a
* thread - local are the same decision , and a consumer that disagreed with the
* library about it would link against a differently shaped symbol .
*
* 1 The error pool and the status registry are mutex protected , and the
* per - thread state below is thread local . Every entry point may be called
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>
2026-08-01 16:56:07 -04:00
* from any thread . See docs / thread - safety . md for what that does and does
* not cover .
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
* 0 The library was built - DAKERR_THREADS = none for a single - threaded
* process : no locking , no thread - local storage , and calling it from more
* than one thread is undefined .
*
* Consumers can test it : # if AKERR_THREAD_SAFE .
*/
# define AKERR_THREAD_SAFE AKERR_THREAD_SAFE_SED
# if AKERR_THREAD_SAFE == 1
# if defined(__GNUC__) || defined(__clang__)
# define AKERR_THREAD_LOCAL __thread
# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
# define AKERR_THREAD_LOCAL _Thread_local
# elif defined(_MSC_VER)
# define AKERR_THREAD_LOCAL __declspec(thread)
# else
# error "libakerror was built thread safe, but this compiler has no thread-local storage specifier that akerror.h knows about. Rebuild libakerror with -DAKERR_THREADS=none, or add the spelling here."
# endif
# else
# define AKERR_THREAD_LOCAL
# endif
2026-05-24 09:50:21 -04:00
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
// related to filesystem paths, which made it grow beyond PATH_MAX, then I started
// reporting messages including 2 file paths (PATH_MAX * 2), so now to make the compiler warnings
// shut up, it's enormous (PATH_MAX*3).
# define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384
2026-01-10 10:20:35 -05:00
# define AKERR_MAX_ERROR_NAME_LENGTH 64
2026-05-24 09:50:21 -04:00
# define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
2026-01-10 10:20:35 -05:00
# define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
2026-05-24 09:50:21 -04:00
# define AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH (AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH + AKERR_MAX_ERROR_NAME_LENGTH + AKERR_MAX_ERROR_FNAME_LENGTH + AKERR_MAX_ERROR_FUNCTION_LENGTH + 16)
2026-01-10 10:20:35 -05:00
2026-01-12 08:33:31 -05:00
# define AKERR_LAST_ERRNO_VALUE AKERR_LAST_ERRNO_VALUE_SED
2026-06-22 08:15:07 -04:00
# define AKERR_NULLPOINTER (AKERR_LAST_ERRNO_VALUE + 1) /** A pointer had a NULL value where such was not permissible */
# define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2) /** Attempt to access a datastructure outside of bounds */
# define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3) /** An otherwise unspecified API contract has been violated */
# define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4) /** Relates to accessing of attributes on objects */
# define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5) /** An object had the incorrect type */
# define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6) /** A key was either invalid for or not present in a map */
# define AKERR_INDEX (AKERR_LAST_ERRNO_VALUE + 8) /** An error occurred when attempting to index an indexable datastructure (other than out of bounds) */
# define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9) /** An error occurred in the formatting of an object (usually a string) */
# define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10) /** An unspecified IO error occurred. */
# define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 11) /** A provided value was invalid */
# define AKERR_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 12) /** An error occurred in establishing, maintaining or severing a relationship between two objects */
# define AKERR_EOF (AKERR_LAST_ERRNO_VALUE + 13) /** The end of a stream or file has been encountered */
# define AKERR_CIRCULAR_REFERENCE (AKERR_LAST_ERRNO_VALUE + 14) /** Indicates that a circular reference has been found in a linked list */
# define AKERR_ITERATOR_BREAK (AKERR_LAST_ERRNO_VALUE + 15) /** Used to prematurely end an iteration cycle (such as when searching a graph and the desired node has been found) */
# define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
# define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
2026-01-10 10:20:35 -05:00
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>
2026-07-30 20:58:47 -04:00
/*
* Registry failures . These are ordinary status codes , not a private return
* enumeration : akerr_reserve_status_range ( ) and akerr_register_status_name ( )
* return akerr_ErrorContext * like everything else in this library , so a refused
* reservation can be CATCH - ed , HANDLE - d , PASS - ed , or left unhandled to produce a
* stack trace and stop the program . Success returns NULL .
*/
# define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */
# define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
# define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */
# define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */
# define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */
# define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
# define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */
/* The last status the library defines for itself. Everything from
* AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name . */
# define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
/*
* Status values 0 through 255 are reserved by libakerror at akerr_init ( ) time :
* the host ' s errno values plus the AKERR_ * codes above . Consumers allocate from
* 256 upwards . Reserving any part of this band fails with
* AKERR_STATUS_RANGE_OVERLAP naming AKERR_LIBRARY_OWNER .
*/
# define AKERR_LIBRARY_OWNER "libakerror"
# define AKERR_RESERVED_STATUS_COUNT 256
# define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
/*
* The library reserves status values 0 through 255 for itself ( see akerr_init ) ,
* which must contain every AKERR_ * code above . AKERR_LAST_ERRNO_VALUE is
* derived from the host ' s errno list at build time , so on a platform with an
* unusually large errno space these codes could escape the band and collide
* with consumer codes allocated at 256. Fail the build instead .
*/
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>
2026-07-30 20:58:47 -04:00
typedef char akerr_assert_codes_within_reserved_band [ ( AKERR_LAST_LIBRARY_STATUS < 256 ) ? 1 : - 1 ] ;
2025-07-20 21:40:14 -04:00
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
/*
* A process exit status is one byte wide . exit ( ) takes an int , but the kernel
* keeps only the low 8 bits of it and throws the rest away , so 0 through 255
* are the only statuses that can also be exit codes - - and every consumer status
* begins at AKERR_FIRST_CONSUMER_STATUS ( 256 ) . No wider variant exists to reach
* for : _exit ( ) , _Exit ( ) , quick_exit ( ) and the raw exit_group syscall all
* truncate identically , and even waitid ( ) ' s int - wide si_status reports the
* truncated value , because the truncation happened before the parent looked .
*
* akerr_exit ( ) therefore substitutes AKERR_EXIT_STATUS_UNREPRESENTABLE for any
* status it cannot deliver intact , rather than passing the low byte - - status
* 256 would exit 0 and report success . 125 is the conventional " the tool itself
* failed " code (126, 127 and 128+n belong to the shell). It is inside the
* library ' s reserved band , so it is also some host ' s errno : the exit code says
* only that the process died of an error , and the stack trace carries the real
* status .
*/
# define AKERR_EXIT_STATUS_MAX 255
# define AKERR_EXIT_STATUS_UNREPRESENTABLE 125
2026-01-10 10:20:35 -05:00
# define AKERR_MAX_ARRAY_ERROR 128
2025-07-20 21:40:14 -04:00
typedef struct
{
2026-01-10 10:20:35 -05:00
char message [ AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH ] ;
2026-01-10 09:50:17 -05:00
int arrayid ;
2025-07-20 21:40:14 -04:00
int status ;
bool handled ;
int refcount ;
2026-01-10 10:20:35 -05:00
char fname [ AKERR_MAX_ERROR_FNAME_LENGTH ] ;
char function [ AKERR_MAX_ERROR_FNAME_LENGTH ] ;
2025-07-20 21:40:14 -04:00
int lineno ;
bool reported ;
2026-01-10 10:20:35 -05:00
char stacktracebuf [ AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ] ;
2025-07-20 21:40:14 -04:00
char * stacktracebufptr ;
2026-01-10 10:20:35 -05:00
} akerr_ErrorContext ;
2025-07-20 21:40:14 -04:00
2026-01-10 10:20:35 -05:00
# define AKERR_NOIGNORE __attribute__((warn_unused_result))
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
/* akerr_exit() does not come back, and the compiler should know it: a handler
* whose last statement is a call to it is complete , not falling off the end . */
# define AKERR_NORETURN __attribute__((noreturn))
2025-07-20 21:40:14 -04:00
2026-01-10 10:20:35 -05:00
typedef void ( * akerr_ErrorUnhandledErrorHandler ) ( akerr_ErrorContext * errctx ) ;
typedef void ( * akerr_ErrorLogFunction ) ( const char * f , . . . ) ;
2025-07-20 21:40:14 -04:00
Document and test handing an error context between threads
The thread-safety section filed two different things under "does not cover,
and cannot": sharing a context between threads, and passing one to another
thread. Only the first is unsupported. Transfer already works by
construction -- the reference count is the only field the library reads
across an ownership boundary, and it is only ever touched under the pool
lock, so akerr_release_error() does not care which thread checked the slot
out. The pool is process-global, not thread-local, so a context outlives the
thread that raised it.
Calling that unsupported told readers the worker/collector shape was off the
table, which either cost them the pattern or cost them the stack trace when
they rolled their own struct instead.
Split the bullet: transfer joins the covered list and gets its own section
with the rule, the worked pattern, and the four receiving-side hazards
(PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a
void helper still parses its return, and an unhandled error now terminates
from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to
what it actually is.
err_threads_handoff.c proves it: the existing thread tests all keep every
context on the thread that raised it, so the transfer path was exercised
nowhere. Seven producers hand errors to one collector through a bounded
mutex/condvar queue -- the mutex is the thing under test, since it is what
publishes the unlocked content writes -- and the collector asserts the
context is still a live slot at refcount 1, that message and trace arrive
whole and in each producer's order, that the slot was never recycled in
flight, and that a thread which never called akerr_next_error() can release
it. A second phase reads a context whose raising thread has already exited.
Also document why copying a context by assignment is silently wrong:
stacktracebufptr is self-referential, so the copy's cursor points into the
source's buffer and the first append corrupts a slot the copier no longer
owns. TODO.md records the akerr_copy_error() shape that would fix it and the
trigger for building it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:18 -04:00
/*
* The pool . Process - global , not thread - local : a context outlives the thread that
* raised it , which is what lets one be handed to another thread and released
* there .
*/
2026-01-10 10:20:35 -05:00
extern akerr_ErrorContext AKERR_ARRAY_ERROR [ AKERR_MAX_ARRAY_ERROR ] ;
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
/*
* Set these before starting threads . They are read on every error and written
* by nothing but your own code , so changing one while other threads are raising
* errors is a data race the library cannot mediate .
*/
2026-01-10 10:20:35 -05:00
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error ;
extern akerr_ErrorLogFunction akerr_log_method ;
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 error IGNORE ( ) last swallowed , per thread : an ignored error is a fact
* about the thread that ignored it , and one shared slot would have two threads
* overwriting each other ' s . Thread local only when AKERR_THREAD_SAFE is 1.
*/
extern AKERR_THREAD_LOCAL akerr_ErrorContext * __akerr_last_ignored ;
2025-07-20 21:40:14 -04:00
Document and test handing an error context between threads
The thread-safety section filed two different things under "does not cover,
and cannot": sharing a context between threads, and passing one to another
thread. Only the first is unsupported. Transfer already works by
construction -- the reference count is the only field the library reads
across an ownership boundary, and it is only ever touched under the pool
lock, so akerr_release_error() does not care which thread checked the slot
out. The pool is process-global, not thread-local, so a context outlives the
thread that raised it.
Calling that unsupported told readers the worker/collector shape was off the
table, which either cost them the pattern or cost them the stack trace when
they rolled their own struct instead.
Split the bullet: transfer joins the covered list and gets its own section
with the rule, the worked pattern, and the four receiving-side hazards
(PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a
void helper still parses its return, and an unhandled error now terminates
from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to
what it actually is.
err_threads_handoff.c proves it: the existing thread tests all keep every
context on the thread that raised it, so the transfer path was exercised
nowhere. Seven producers hand errors to one collector through a bounded
mutex/condvar queue -- the mutex is the thing under test, since it is what
publishes the unlocked content writes -- and the collector asserts the
context is still a live slot at refcount 1, that message and trace arrive
whole and in each producer's order, that the slot was never recycled in
flight, and that a thread which never called akerr_next_error() can release
it. A second phase reads a context whose raising thread has already exited.
Also document why copying a context by assignment is silently wrong:
stacktracebufptr is self-referential, so the copy's cursor points into the
source's buffer and the first append corrupts a slot the copier no longer
owns. TODO.md records the akerr_copy_error() shape that would fix it and the
trigger for building it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:35:18 -04:00
/*
* Drop one reference , returning NULL once the last one is gone so the caller can
* null its own pointer .
*
* This need not be the thread that checked the context out . The reference count
* is the only field the library reads across threads , and it is only ever
* touched under the pool lock , so a context handed to another thread is released
* there . Exactly once , though : releasing a stale pointer takes the
* refcount - zero branch a second time and wipes a slot that by then holds
* somebody else ' s live error .
*/
2026-01-10 10:20:35 -05:00
akerr_ErrorContext AKERR_NOIGNORE * akerr_release_error ( akerr_ErrorContext * ptr ) ;
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
/*
* Check a context out of the pool . The returned context already carries one
* reference : finding a free slot and claiming it is a single operation under
* the pool lock , because two threads scanning at once would otherwise be handed
* the same slot . Release it with akerr_release_error ( ) ( or let RELEASE_ERROR ,
* SUCCEED_RETURN or FINISH do it for you ) . Returns NULL when every slot is
* checked out .
*/
2026-01-10 10:20:35 -05:00
akerr_ErrorContext AKERR_NOIGNORE * akerr_next_error ( ) ;
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
/*
* Look up ( name = = NULL ) or register ( name ! = NULL ) the display name for a
* status . Registration succeeds only if some owner has reserved a range
* containing ` status ` ; prefer akerr_register_status_name ( ) , which also checks
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>
2026-07-30 20:58:47 -04:00
* that the range belongs to you and raises an error saying why a registration
* was refused . This entry point cannot return an error context , so a refusal
* here is reported through akerr_log_method and reads back as " Unknown Error " .
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
* Never returns NULL - - an unregistered status reads back as " Unknown Error " .
*/
2026-01-10 10:20:35 -05:00
char * akerr_name_for_status ( int status , char * name ) ;
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
/*
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>
2026-07-30 20:58:47 -04:00
* Register a display name for a status you own . ` owner ` must match the owner
* string passed to akerr_reserve_status_range ( ) for the range containing
* ` status ` . Returns NULL on success , or an error context whose status is one of
* the AKERR_STATUS_NAME_ * codes above - - CATCH it , HANDLE it , or let it
* propagate .
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
*/
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>
2026-07-30 20:58:47 -04:00
akerr_ErrorContext AKERR_NOIGNORE * akerr_register_status_name ( const char * owner , int status , const char * name ) ;
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
/*
* Claim ` count ` status values starting at ` first_status ` for ` owner ` . Repeating
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>
2026-07-30 20:58:47 -04:00
* an identical reservation for the same owner is a no - op ; any other collision is
* refused . Returns NULL on success , or an error context whose status is one of
* the AKERR_STATUS_RANGE_ * codes above . Treat any error as an initialization
* failure : either handle it or let it propagate out of your init function .
Enforce status-code ownership and harden the name registry
Reservations were advisory bookkeeping: any component could name any status,
so the registry only detected declared-range overlap between components that
both opted in. Naming a status now requires a reservation.
akerr_register_status_name() checks that the range belongs to the caller, and
the legacy two-argument akerr_name_for_status() set path, which cannot
identify its caller, requires that some reservation covers the status. Every
refusal is logged and names the real owner, because a name that fails to
register degrades that code to "Unknown Error" in every later stack trace.
Fix a reservation made before the first PREPARE_ERROR being silently
discarded. akerr_init() clears the tables, so whichever component first
triggered it wiped an earlier reservation and the next component to claim the
same range was told it was free, producing exactly the undetected aliasing
the registry exists to prevent. Every registry entry point now calls
akerr_init(), which sets its guard before doing any work so those calls do
not recurse.
Replace the linear-scan name array with an open-addressed hash table, taking
lookup from O(n) to O(1) and raising usable capacity from 512 entries (366
free to consumers after errno registration) to 3072 (~2900 free). Both table
sizes are build-time overridable and applied PRIVATE: they live entirely in
src/error.c, so raising them cannot desynchronize a library from its
consumers the way AKERR_MAX_ERR_VALUE could. Exhausting either table is now
logged and returned to the caller rather than silently dropping the entry.
No dynamic allocation is introduced; both tables remain file-scope arrays,
and the library's undefined-symbol set gains only strcmp and strlen.
Register names for AKERR_EOF, AKERR_ITERATOR_BREAK and AKERR_NOT_IMPLEMENTED,
which had none and rendered as "Unknown Error" in every stack trace carrying
them. err_error_names.c now sweeps the whole AKERR_* offset span so a code
added without a name fails there instead of in production traces.
Add static assertions that the slot count is a power of two and that
AKERR_BADEXC stays inside the library's own 0-255 band, the latter guarding
against a host errno space large enough to push library codes into the range
consumers are told to allocate from.
Set a project version and soname (1.0.0 / libakerror.so.1) so a stale
installed library can no longer be silently paired with newer headers, and so
akerror.pc ships a real Version field instead of an empty one.
Mutation testing surfaced an out-of-bounds probe in the new table that the
suite did not catch: masking with SLOTS rather than SLOTS-1 indexes past the
array, and err_maxval.c asserted only that some names registered before the
table filled, which a collapsed probe sequence still satisfies. It now
requires a substantial entry count and reads every entry back by its own
distinct name.
Tests: 28/28 pass. Coverage 99.4% line / 86.8% branch. Mutation score for
src/error.c 74% -> 77.3%.
Compatibility: source and ABI break. AKERR_MAX_ERR_VALUE and the
__AKERR_ERROR_NAMES data symbol are gone, custom codes must move out of
0-255, and names must be registered against a reserved range. README.md
carries the migration steps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:19:25 -04:00
*/
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>
2026-07-30 20:58:47 -04:00
akerr_ErrorContext AKERR_NOIGNORE * akerr_reserve_status_range ( int first_status , int count , const char * owner ) ;
2026-01-10 10:20:35 -05:00
void akerr_init ( ) ;
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
/*
* Terminate the process , reporting ` status ` . Use this instead of exit ( )
* anywhere you are leaving on account of an akerr status - - an unhandled - error
* handler of your own , a CLI ' s top - level HANDLE block , an init routine that
* cannot continue - - so that every exit out of the library ' s status space maps
* the same way .
*
* Exits with ` status ` when 0 < = status < = AKERR_EXIT_STATUS_MAX , and with
* AKERR_EXIT_STATUS_UNREPRESENTABLE otherwise ( see above ) . Status 0 exits 0 :
* zero is this library ' s success status , and passing it here says the program
* finished , not that it failed with a code that got lost .
*/
void AKERR_NORETURN akerr_exit ( int status ) ;
/*
* The default akerr_handler_unhandled_error : logs nothing further - - the stack
* trace has already been printed by the time it runs - - and hands ` ptr - > status `
* to akerr_exit ( ) , or exits 1 when ` ptr ` is NULL . Replace it if you need a
* different mapping , and call akerr_exit ( ) from your replacement .
*/
2026-01-10 10:20:35 -05:00
void akerr_default_handler_unhandled_error ( akerr_ErrorContext * ptr ) ;
void akerr_default_logger ( const char * f , . . . ) ;
2026-05-21 21:44:52 -04:00
int akerr_valid_error_address ( akerr_ErrorContext * ptr ) ;
2026-01-12 08:33:31 -05:00
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
void akerr_init_errno ( void ) ;
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>
2026-07-30 20:58:47 -04:00
/*
* Internal . Names a status in the library ' s own reserved band on behalf of
* akerr_init ( ) and the generated errno table , which have no caller to raise
* into : a failure here is logged and terminates the program . Not part of the
* consumer API - - use akerr_register_status_name ( ) , which raises instead .
*/
void __akerr_name_library_status ( int status , const char * name ) ;
2025-07-20 21:40:14 -04:00
Use the library's own error idioms inside the library
Four things in src/error.c did by hand what the macros already do, or
skipped checks the library would have caught for a consumer.
akerr_copy_string() returned void and validated only its capacity, while
writing through a caller-supplied pointer for a caller-supplied length.
It is now __akerr_copy_string() and raises: AKERR_NULLPOINTER for a NULL
destination or source, AKERR_VALUE for a capacity with no room for a
terminator. Both call sites PASS it, and the owner copy in
akerr_reserve_status_range() now gates the commit, so a failed copy
cannot leave a range claimed under an empty owner. It is exported under
the internal prefix rather than static so tests/err_copy_string.c can
drive those guards; nothing else can reach them.
__akerr_name_library_status() and the band reservation in akerr_init()
hand-rolled the log/handler/release sequence. Both now use
ATTEMPT/CATCH/PROCESS/FINISH_NORETURN. PASS does not fit: both sites are
void and have no caller to propagate to, so the terminal form of the same
idiom is the right one -- an unhandled failure prints its stack trace and
goes to akerr_handler_unhandled_error, which terminates, exactly as
before but without the bespoke plumbing. The legacy set path in
akerr_name_for_status() had the same shape and now handles its refusal
with HANDLE_DEFAULT, converting it to the "Unknown Error" sentinel.
Every remaining `if (x) { FAIL_RETURN }` in the registry is now
FAIL_ZERO_RETURN or FAIL_NONZERO_RETURN, and akerr_register_status_name()
checks both owner and name before passing either down --
akerr_store_status_name() reads a NULL owner as "caller did not identify
itself" for the legacy path, so a NULL arriving through the owned entry
point would have skipped the ownership check entirely.
New tests: err_copy_string (the guards above), err_library_status_fatal
(WILL_FAIL -- proves a refused library-status registration terminates).
Tests: ctest 31/31, mutation 80.7% (was 77.5%), line coverage 98.9%.
Branch coverage on src/error.c drops 64.5% -> 50.4%, just over its gate:
each FAIL_* site carries ~6 branch outcomes of error-construction
machinery that only run when that failure fires, and each PASS around a
call that cannot fail carries ~25, so added validation lowers the ratio
by construction. Recorded in TODO.md item 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 21:53:33 -04:00
/*
* Internal . Bounded string copy into a fixed buffer , always NUL - terminated .
* Raises AKERR_NULLPOINTER for a NULL destination or source and AKERR_VALUE for
* a capacity that leaves no room for a terminator . Exported so the library ' s
* own tests can drive those guards ; not part of the consumer API .
*/
akerr_ErrorContext AKERR_NOIGNORE * __akerr_copy_string ( char * destination , int capacity ,
const char * source ) ;
2025-07-20 21:40:14 -04:00
# define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
2026-01-10 10:20:35 -05:00
akerr_log_method ( " %s%s:%s:%d: %s %d (%s): %s " , ( char * ) & __err_context - > stacktracebuf , ( char * ) __FILE__ , ( char * ) __func__ , __LINE__ , __err_message , __err_context - > status , akerr_name_for_status ( __err_context - > status , NULL ) , __err_context - > message ) ; \
2025-07-20 21:40:14 -04:00
# define LOG_ERROR(__err_context) \
LOG_ERROR_WITH_MESSAGE ( __err_context , " " ) ;
# define RELEASE_ERROR(__err_context) \
if ( __err_context ! = NULL ) { \
2026-01-10 10:20:35 -05:00
__err_context = akerr_release_error ( __err_context ) ; \
2025-07-20 21:40:14 -04:00
}
# define PREPARE_ERROR(__err_context) \
2026-01-10 10:20:35 -05:00
akerr_init ( ) ; \
akerr_ErrorContext __attribute__ ( ( unused ) ) * __err_context = NULL ;
2025-07-20 21:40:14 -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
/*
* akerr_next_error ( ) hands back a context that already holds one reference - -
* it has to , or a second thread could be given the same slot between the scan
* and the increment . There is nothing to increment here .
*/
2025-07-20 21:40:14 -04:00
# define ENSURE_ERROR_READY(__err_context) \
if ( __err_context = = NULL ) { \
2026-01-10 10:20:35 -05:00
__err_context = akerr_next_error ( ) ; \
2025-07-20 21:40:14 -04:00
if ( __err_context = = NULL ) { \
2026-01-10 10:20:35 -05:00
akerr_log_method ( " %s:%s:%d: Unable to pull an error context from the array! " , __FILE__ , ( char * ) __func__ , __LINE__ ) ; \
2025-07-20 21:40:14 -04:00
exit ( 1 ) ; \
} \
2026-07-28 09:22:19 -04:00
}
/*
* Append a formatted line to the error ' s stack - trace buffer , bounded by the
* space that remains so a deep propagation chain cannot write past the end of
* stacktracebuf . snprintf reports the length it * would * have written , which on
* truncation exceeds what it actually wrote , so the cursor advance is clamped
* to the remaining space .
*/
# define AKERR_STACKTRACE_APPEND(__err_context, ...) \
do { \
char * __akerr_stb = ( char * ) __err_context - > stacktracebuf ; \
size_t __akerr_used = ( size_t ) ( __err_context - > stacktracebufptr - __akerr_stb ) ; \
if ( __akerr_used < AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ) { \
size_t __akerr_rem = AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - __akerr_used ; \
int __akerr_n = snprintf ( __err_context - > stacktracebufptr , __akerr_rem , __VA_ARGS__ ) ; \
if ( __akerr_n < 0 ) { \
__akerr_n = 0 ; \
} \
__err_context - > stacktracebufptr + = ( ( size_t ) __akerr_n < __akerr_rem ) \
? ( size_t ) __akerr_n : ( __akerr_rem - 1 ) ; \
} \
} while ( 0 )
2025-07-20 21:40:14 -04:00
2026-07-28 09:22:19 -04:00
/*
2026-01-10 10:20:35 -05:00
* Failure and success methods for functions that return akerr_ErrorContext *
2025-07-20 21:40:14 -04:00
*/
# define FAIL_ZERO_RETURN(__err_context, __x, __err, __message, ...) \
if ( __x = = 0 ) { \
FAIL ( __err_context , __err , __message , # # __VA_ARGS__ ) ; \
return __err_context ; \
}
# define FAIL_NONZERO_RETURN(__err_context, __x, __err, __message, ...) \
if ( __x ! = 0 ) { \
FAIL ( __err_context , __err , __message , # # __VA_ARGS__ ) ; \
return __err_context ; \
}
# define FAIL_RETURN(__err_context, __err, __message, ...) \
FAIL ( __err_context , __err , __message , # # __VA_ARGS__ ) ; \
return __err_context ;
# define SUCCEED_RETURN(__err_context) \
RELEASE_ERROR ( __err_context ) ; \
return NULL ;
/*
* Failure and success methods for use inside of ATTEMPT ( ) blocks
*/
# define FAIL_ZERO_BREAK(__err_context, __x, __err, __message, ...) \
if ( __x = = 0 ) { \
FAIL ( __err_context , __err , __message , # # __VA_ARGS__ ) ; \
break ; \
}
# define FAIL_NONZERO_BREAK(__err_context, __x, __err, __message, ...) \
if ( __x ! = 0 ) { \
FAIL ( __err_context , __err , __message , # # __VA_ARGS__ ) ; \
break ; \
}
# define FAIL_BREAK(__err_context, __err_, __message, ...) \
FAIL ( __err_context , __err_ , __message , # # __VA_ARGS__ ) ; \
break ;
# define SUCCEED_BREAK(__err_context) \
SUCCEED ( __err_context ) ; \
break ;
/*
* General failure and success methods
*/
# define FAIL(__err_context, __err, __message, ...) \
ENSURE_ERROR_READY ( __err_context ) ; \
__err_context - > status = __err ; \
2026-07-28 10:52:29 -04:00
snprintf ( ( char * ) __err_context - > fname , AKERR_MAX_ERROR_FNAME_LENGTH , " %s " , __FILE__ ) ; \
snprintf ( ( char * ) __err_context - > function , AKERR_MAX_ERROR_FUNCTION_LENGTH , " %s " , __func__ ) ; \
2025-07-20 21:40:14 -04:00
__err_context - > lineno = __LINE__ ; \
2026-01-10 10:20:35 -05:00
snprintf ( ( char * ) __err_context - > message , AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH , __message , # # __VA_ARGS__ ) ; \
2026-07-28 09:22:19 -04:00
AKERR_STACKTRACE_APPEND ( __err_context , " %s:%s:%d: %d (%s) : %s \n " , ( char * ) __err_context - > fname , ( char * ) __err_context - > function , __err_context - > lineno , __err_context - > status , akerr_name_for_status ( __err_context - > status , NULL ) , ( __err_context - > message = = NULL ? " " : __err_context - > message ) ) ;
2025-07-20 21:40:14 -04:00
# define SUCCEED(__err_context) \
ENSURE_ERROR_READY ( __err_context ) ; \
__err_context - > status = 0 ;
/*
* Defines for the ATTEMPT / CATCH / CLEANUP / PROCESS / HANDLE / FINISH process
*/
# define ATTEMPT \
switch ( 0 ) { \
case 0 : \
2026-05-15 19:41:22 -04:00
# define VALID(__err_context, __stmt) \
__stmt ; \
2026-05-24 19:14:35 -04:00
if ( akerr_valid_error_address ( __err_context ) = = 0 ) { \
2026-05-24 19:41:32 -04:00
__err_context = NULL ; \
2026-06-22 08:15:07 -04:00
FAIL ( __err_context , AKERR_BADEXC , " Received (akerr_ErrorContext *) from an invalid memory region. (Did the method finish without calling SUCCEED_RETURN?) " ) ; \
2026-05-15 19:41:22 -04:00
}
2025-07-20 21:40:14 -04:00
# define DETECT(__err_context, __stmt) \
2026-05-15 19:41:22 -04:00
VALID ( __err_context , __stmt ) ; \
2025-07-20 21:40:14 -04:00
if ( __err_context ! = NULL ) { \
if ( __err_context - > status ! = 0 ) { \
2026-07-28 09:22:19 -04:00
AKERR_STACKTRACE_APPEND ( __err_context , " %s:%s:%d \n " , ( char * ) __FILE__ , ( char * ) __func__ , __LINE__ ) ; \
2025-07-20 21:40:14 -04:00
break ; \
} \
}
# define CATCH(__err_context, __stmt) \
DETECT ( __err_context , __err_context = __stmt ) ;
2026-05-15 19:41:22 -04:00
# define PASS(__err_context, __stmt) \
switch ( 0 ) { \
case 0 : \
DETECT ( __err_context , __err_context = __stmt ) ; \
} \
FINISH_LOGIC ( __err_context , true ) ;
2025-07-20 21:40:14 -04:00
# define IGNORE(__stmt) \
2026-01-10 10:20:35 -05:00
__akerr_last_ignored = __stmt ; \
if ( __akerr_last_ignored ! = NULL ) { \
LOG_ERROR_WITH_MESSAGE ( __akerr_last_ignored , " ** IGNORED ERROR ** " ) ; \
2025-07-20 21:40:14 -04:00
}
# define CLEANUP \
} ;
# define PROCESS(__err_context) \
if ( __err_context ! = NULL ) { \
switch ( __err_context - > status ) { \
case 0 : \
__err_context - > handled = true ;
# define HANDLE(__err_context, __err_status) \
break ; \
case __err_status : \
__err_context - > stacktracebufptr = ( char * ) & __err_context - > stacktracebuf ; \
__err_context - > handled = true ;
# define HANDLE_GROUP(__err_context, __err_status) \
case __err_status : \
__err_context - > stacktracebufptr = ( char * ) & __err_context - > stacktracebuf ; \
__err_context - > handled = true ;
# define HANDLE_DEFAULT(__err_context) \
break ; \
default : \
__err_context - > stacktracebufptr = ( char * ) & __err_context - > stacktracebuf ; \
__err_context - > handled = true ;
2026-05-15 19:41:22 -04:00
# define FINISH_LOGIC(__err_context, __pass_up) \
2025-07-20 21:40:14 -04:00
if ( __err_context ! = NULL ) { \
if ( __err_context - > handled = = false & & __pass_up = = true ) { \
return __err_context ; \
} \
} \
2026-05-15 19:41:22 -04:00
# define FINISH(__err_context, __pass_up) \
} ; \
} ; \
FINISH_LOGIC ( __err_context , __pass_up ) \
2025-07-20 21:40:14 -04:00
RELEASE_ERROR ( __err_context ) ;
# define FINISH_NORETURN(__err_context) \
} ; \
} ; \
if ( __err_context ! = NULL ) { \
if ( __err_context - > handled = = false ) { \
LOG_ERROR_WITH_MESSAGE ( __err_context , " Unhandled Error " ) ; \
2026-01-10 10:20:35 -05:00
akerr_handler_unhandled_error ( __err_context ) ; \
2025-07-20 21:40:14 -04:00
} \
} \
RELEASE_ERROR ( __err_context ) ;
2026-01-10 10:20:35 -05:00
# endif // _AKERR_H_