Files
libakerror/src/lock.h

124 lines
4.1 KiB
C
Raw Normal View History

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_