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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:31:22 -04:00
# ifndef _AKERR_LOCK_H_
# define _AKERR_LOCK_H_
/*
* Serialization for the library ' s process - global state : the error pool
* ( AKERR_ARRAY_ERROR ) and the status registry . Private to the library - - none
* of this appears in the installed header , so the backend is not part of the
* ABI and can be changed without touching a consumer .
*
* The backend is chosen at configure time by the AKERR_THREADS build option and
* never by autodetection here . A build that quietly decided it did not need
* locking is exactly the failure this has to prevent : it would produce a
* library that reports itself thread safe and is not .
*
* AKERR_THREADS_PTHREAD POSIX threads .
* AKERR_THREADS_NONE No locking at all , for a build that has declared
* itself single threaded ( - DAKERR_THREADS = none ) .
*
* One lock covers both tables , and it is recursive . Both are deliberate :
*
* - Raising an error re - enters the library . FAIL ( ) calls
* akerr_name_for_status ( ) to render the status into the stack trace and
* ENSURE_ERROR_READY ( ) to check a context out of the pool , so a refusal
* raised from inside a locked registry operation takes the lock again on
* the same thread . A non - recursive mutex deadlocks there .
* - With a single lock there is no lock ordering to get wrong , and no way for
* a future caller to acquire the pool and the registry in the opposite
* order from this file .
*
* The cost is that error * construction * is serialized across threads . Errors
* are the exceptional path ; correctness is worth more there than throughput .
*/
/*
* PTHREAD_MUTEX_RECURSIVE is XSI , so glibc hides it under a strict - std = c99
Fix the AKERR_USE_STDLIB=OFF build (issue #12)
The freestanding build (-DAKERR_USE_STDLIB=OFF) did not compile at all:
bool, PATH_MAX and NULL were used unconditionally in the public header
but only included under the stdlib branch, and the CMake option was
pasted straight into a preprocessor definition, so a non-numeric cache
spelling (-DAKERR_USE_STDLIB=ON) silently evaluated to 0.
- Normalize AKERR_USE_STDLIB to a plain 1/0 in CMake before stamping it,
and use a consistent '#if AKERR_USE_STDLIB' everywhere it is tested.
- Include <stdbool.h>/<stddef.h> unconditionally (freestanding-safe);
keep <stdlib.h>/<string.h>/<stdio.h> behind AKERR_USE_STDLIB; move
<limits.h> out of the public header into src/error.c, its only user.
- Define the freestanding runtime contract: AKERR_RUNTIME_HEADER must
name a header providing exit, memset, snprintf, strcmp, strlen and
strncpy when AKERR_USE_STDLIB is OFF, or the header #errors naming
them. Add cmake/akerr_default_runtime.h, a libc-backed convenience
default so this repo's own OFF build and tests work out of the box.
- Route ENSURE_ERROR_READY's pool-exhaustion path through akerr_exit()
instead of a direct exit(1). Deliberate behavior change: that exit
code moves from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125), the same
sentinel every other unrepresentable status already uses. Documented
in docs/building.md and UPGRADING.md. Not an ABI break.
- Retire PATH_MAX: AKERR_MAX_ERROR_FNAME_LENGTH is now stamped by
scripts/generrno.sh from a new AKERR_MAX_ERROR_FNAME_LENGTH cache
variable, defaulting to 4096 (PATH_MAX on Linux/glibc) so
sizeof(akerr_ErrorContext) and the soname are unchanged. Rewrite the
now-stale PATH_MAX justification in src/lock.h's feature-test-macro
comment.
- Fail the configure with a FATAL_ERROR, not a warning, when
AKERR_USE_STDLIB=OFF and AKERR_THREADS would resolve to pthread,
naming -DAKERR_THREADS=none as the fix.
- Keep the generated errno table (errno.c) out of the OFF build; skip
the 'errno --list' shellout in scripts/generrno.sh under OFF and
stamp AKERR_LAST_ERRNO_VALUE from a new fallback cache variable
(default 133, Linux's EHWPOISON) instead.
- Update docs/building.md: drop the known-defect paragraph, fix
sprintf -> snprintf, add size_t, drop PATH_MAX, and document the new
options and the exit-code change.
- Guard tests/err_errno.c's registered-name assertion behind
AKERR_USE_STDLIB: akerr_init_errno() is not called when it is OFF.
- Add a CI job that configures/builds/tests AKERR_USE_STDLIB=OFF with
AKERR_THREADS=none, plus a compile-only -nostdinc -ffreestanding
check of tests/freestanding_fixture.c against the generated header.
Bump the project version to 2.0.2 (no ABI break: soname and struct
layout are unchanged).
Verified locally: OFF+none configures and builds clean with all tests
passing; ON and the plain default build both build and pass their full
test suites (37/37); OFF with the default/auto thread backend fails
configure with a message naming -DAKERR_THREADS=none; sizeof(akerr_ErrorContext)
is unchanged (37296 bytes, fname/function still 4096 each) versus the
pre-change tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:29:39 -04:00
* without _XOPEN_SOURCE . No feature - test macro is defined here : this file is
* only ever compiled with AKERR_THREADS_PTHREAD , which CMake now refuses to
* pair with AKERR_USE_STDLIB = OFF ( see the AKERR_THREADS / AKERR_USE_STDLIB
* check in CMakeLists . txt ) , so whatever default feature - test macros the host
* libc uses when building the rest of the ( hosted ) library apply here too .
* Build with - D_XOPEN_SOURCE = 700 if you need strict C99 .
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>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 08:31:22 -04:00
*/
# if defined(AKERR_THREADS_PTHREAD) && AKERR_THREADS_PTHREAD == 1
# include <pthread.h>
# include <stdlib.h>
typedef pthread_mutex_t akerr_Mutex ;
typedef pthread_once_t akerr_Once ;
# define AKERR_ONCE_INIT PTHREAD_ONCE_INIT
/*
* Terminal on failure . There is no error context to raise into : the pool one
* would come from is the thing this lock protects , and every path that could
* report the failure needs the lock to do it . A process whose error library
* silently stopped locking is worse than one that stops here .
*/
static void akerr_mutex_init ( akerr_Mutex * mutex )
{
pthread_mutexattr_t attr ;
if ( pthread_mutexattr_init ( & attr ) ! = 0 | |
pthread_mutexattr_settype ( & attr , PTHREAD_MUTEX_RECURSIVE ) ! = 0 | |
pthread_mutex_init ( mutex , & attr ) ! = 0 ) {
abort ( ) ;
}
pthread_mutexattr_destroy ( & attr ) ;
}
static void akerr_mutex_lock ( akerr_Mutex * mutex )
{
pthread_mutex_lock ( mutex ) ;
}
static void akerr_mutex_unlock ( akerr_Mutex * mutex )
{
pthread_mutex_unlock ( mutex ) ;
}
static void akerr_once ( akerr_Once * once , void ( * routine ) ( void ) )
{
pthread_once ( once , routine ) ;
}
# elif defined(AKERR_THREADS_NONE) && AKERR_THREADS_NONE == 1
typedef char akerr_Mutex ;
typedef int akerr_Once ;
# define AKERR_ONCE_INIT 0
static void akerr_mutex_init ( akerr_Mutex * mutex )
{
( void ) mutex ;
}
static void akerr_mutex_lock ( akerr_Mutex * mutex )
{
( void ) mutex ;
}
static void akerr_mutex_unlock ( akerr_Mutex * mutex )
{
( void ) mutex ;
}
/*
* The flag is raised before the routine runs , so a routine that calls back into
* akerr_init ( ) sees initialization already in progress and does not recurse - -
* the same short - circuit the pthread backend gets from akerr_initializing .
*/
static void akerr_once ( akerr_Once * once , void ( * routine ) ( void ) )
{
if ( * once = = 0 ) {
* once = 1 ;
routine ( ) ;
}
}
# else
# error "No threading backend selected. Build libakerror through its CMake, which defines AKERR_THREADS_PTHREAD or AKERR_THREADS_NONE from the AKERR_THREADS option."
# endif
# endif // _AKERR_LOCK_H_