Files
libakerror/src/lock.h
Tachikoma 983ecf31c9
All checks were successful
libakerror CI Build / cmake_build_freestanding (push) Successful in 2m51s
libakerror CI Build / coverage (push) Successful in 2m51s
libakerror CI Build / cmake_build (push) Successful in 2m55s
libakerror CI Build / mutation_test (push) Successful in 47m2s
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:30:40 -04:00

124 lines
4.1 KiB
C

#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
* 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.
*/
#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_