1 Commits

Author SHA1 Message Date
eec5726d91 Release ignored error contexts
All checks were successful
libakerror CI Build / cmake_build (push) Successful in 2m52s
libakerror CI Build / coverage (push) Successful in 2m48s
libakerror CI Build / mutation_test (push) Successful in 44m14s
2026-08-03 12:55:42 -04:00
19 changed files with 87 additions and 553 deletions

View File

@@ -37,50 +37,6 @@ jobs:
fail_on_failure: 'true' fail_on_failure: 'true'
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
# Builds and tests the AKERR_USE_STDLIB=OFF configuration end to end (issue
# #12), and separately proves that the generated header compiles under a
# genuinely freestanding toolchain (-nostdinc -ffreestanding, no libc at
# all) via tests/freestanding_fixture.c, which is never linked or run.
cmake_build_freestanding:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc
- name: configure, build and test (AKERR_USE_STDLIB=OFF)
run: |
cmake -S . -B build-off -DAKERR_USE_STDLIB=OFF -DAKERR_THREADS=none
cmake --build build-off
ctest --test-dir build-off --output-on-failure
- name: freestanding consumer fixture (compile-only, no libc)
run: |
gcc -c -std=c11 \
-nostdinc -ffreestanding \
-isystem "$(gcc -print-file-name=include)" \
-I build-off/generated/include \
-I tests \
tests/freestanding_fixture.c -o /tmp/freestanding_fixture.o
- run: echo "🍏 This job's status is ${{ job.status }}."
sanitizer:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y cmake gcc moreutils
- name: build with AddressSanitizer and UBSan
run: |
cmake -S . -B build/asan -DAKERR_SANITIZE=address,undefined
cmake --build build/asan
- name: test with AddressSanitizer and UBSan
run: ctest --test-dir build/asan --output-on-failure
- run: echo "🍏 This job's status is ${{ job.status }}."
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.10) cmake_minimum_required(VERSION 3.10)
# 1.0.0 replaced the consumer-sized __AKERR_ERROR_NAMES array with private # 1.0.0 replaced the consumer-sized __AKERR_ERROR_NAMES array with private
# storage. 2.0.0 makes the library thread safe, which is a second ABI break in # storage. 2.0.0 makes the library thread safe, which is a second ABI break in
# the same places: akerr_last_ignored became thread-local storage, and # the same places: __akerr_last_ignored became thread-local storage, and
# ENSURE_ERROR_READY no longer takes the pool reference that akerr_next_error() # ENSURE_ERROR_READY no longer takes the pool reference that akerr_next_error()
# now takes for it. Consumer code compiled against a 1.x header would # now takes for it. Consumer code compiled against a 1.x header would
# double-count every reference. Hence the major bump and the SOVERSION, so a # double-count every reference. Hence the major bump and the SOVERSION, so a
@@ -9,35 +9,13 @@ cmake_minimum_required(VERSION 3.10)
# 2.0.1 fixes the unhandled-error exit code, which reported success for any # 2.0.1 fixes the unhandled-error exit code, which reported success for any
# status whose low byte was zero. It adds akerr_exit() but breaks nothing: the # status whose low byte was zero. It adds akerr_exit() but breaks nothing: the
# soname is unchanged and no existing entry point changed shape. # soname is unchanged and no existing entry point changed shape.
# 2.0.2 fixes the AKERR_USE_STDLIB=OFF build, which did not compile at all project(akerror VERSION 2.0.1 LANGUAGES C)
# (issue #12): untangles the header's includes, defines the AKERR_RUNTIME_HEADER
# freestanding contract, retires PATH_MAX in favor of the AKERR_MAX_ERROR_FNAME_LENGTH
# build option (default unchanged, so this is not an ABI break), and fails the
# configure instead of the build when AKERR_THREADS would resolve to pthread
# under AKERR_USE_STDLIB=OFF. ENSURE_ERROR_READY's pool-exhaustion path now
# calls akerr_exit() instead of exit(1) directly, which changes that exit code
# from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125) -- a deliberate behavior
# change, not an ABI break: no soname move, no entry point changed shape.
project(akerror VERSION 2.0.2 LANGUAGES C)
include(GNUInstallDirs) include(GNUInstallDirs)
include(CMakePackageConfigHelpers) include(CMakePackageConfigHelpers)
include(CTest) include(CTest)
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library") set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
set(AKERR_MAX_ERROR_FNAME_LENGTH 4096 CACHE STRING
"Bytes reserved for the fname/function fields of akerr_ErrorContext. Defaults to PATH_MAX on Linux/glibc, which keeps sizeof(akerr_ErrorContext) and the soname unchanged; changing it is an ABI break.")
set(AKERR_LAST_ERRNO_VALUE_FALLBACK 133 CACHE STRING
"AKERR_LAST_ERRNO_VALUE to stamp when AKERR_USE_STDLIB is OFF, since the freestanding build cannot shell out to 'errno --list'. Defaults to 133 (Linux's EHWPOISON).")
# Mandatory under AKERR_USE_STDLIB=OFF: the generated header #errors at
# compile time if it is unset when included (see include/akerror.tmpl.h). Left
# empty here so an explicit -DAKERR_RUNTIME_HEADER=... is honored; if still
# empty once AKERR_USE_STDLIB=OFF is known (below), it defaults to a
# convenience header that is merely a thin, libc-backed stand-in, so this
# repository's own OFF build and tests work without a real freestanding
# runtime. A genuinely freestanding consumer should override this.
set(AKERR_RUNTIME_HEADER "" CACHE STRING
"Header providing exit, memset, snprintf, strcmp, strlen and strncpy, required when AKERR_USE_STDLIB is OFF")
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters") set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
set(AKERR_SANITIZE "" CACHE STRING set(AKERR_SANITIZE "" CACHE STRING
"Sanitizers to build the library and tests with, e.g. thread or address,undefined") "Sanitizers to build the library and tests with, e.g. thread or address,undefined")
@@ -73,34 +51,6 @@ else()
"AKERR_THREADS must be auto, pthread or none, not '${AKERR_THREADS}'") "AKERR_THREADS must be auto, pthread or none, not '${AKERR_THREADS}'")
endif() endif()
# Normalize the stdlib option. CMake cache booleans may be spelled ON/OFF,
# TRUE/FALSE, 1/0, YES/NO and more; pasting AKERR_USE_STDLIB straight into a
# preprocessor definition (as this used to) left non-numeric spellings such as
# -DAKERR_USE_STDLIB=ON expanding to "#if ON == 1", where ON reads as an
# undefined identifier -- silently 0. Reduce it to a plain 1 or 0 once, here.
if(AKERR_USE_STDLIB)
set(AKERR_USE_STDLIB_DEFINE 1)
else()
set(AKERR_USE_STDLIB_DEFINE 0)
if(AKERR_RUNTIME_HEADER STREQUAL "")
set(AKERR_RUNTIME_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/cmake/akerr_default_runtime.h")
endif()
endif()
# A freestanding build cannot be thread safe through pthreads: src/lock.h's
# pthread backend calls into libc (pthread_mutex_init, abort) unconditionally,
# and the freestanding runtime contract (AKERR_RUNTIME_HEADER) does not cover
# it. Fail the configure rather than produce a library that silently links
# libc anyway.
if(NOT AKERR_USE_STDLIB AND AKERR_THREAD_SAFE)
message(FATAL_ERROR
"AKERR_USE_STDLIB=OFF is incompatible with a pthread threading "
"backend: libakerror serializes its global state with a recursive "
"pthread mutex, and pthreads pull in the C standard library. "
"Configure with -DAKERR_THREADS=none to build a freestanding "
"library instead.")
endif()
# Size of the private status-name hash table. Must be a power of two; usable # Size of the private status-name hash table. Must be a power of two; usable
# capacity is 75% of it (src/error.c asserts both). The host's errno list # capacity is 75% of it (src/error.c asserts both). The host's errno list
# consumes part of that at akerr_init() time, so the remainder is what all # consumes part of that at akerr_init() time, so the remainder is what all
@@ -204,36 +154,14 @@ add_custom_command(
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
${GENERATED_DIR} ${GENERATED_DIR}
${AKERR_THREAD_SAFE} ${AKERR_THREAD_SAFE}
${AKERR_USE_STDLIB_DEFINE}
${AKERR_LAST_ERRNO_VALUE_FALLBACK}
${AKERR_MAX_ERROR_FNAME_LENGTH}
DEPENDS ${SCRIPT} ${INFILE} ${GENERATED_THREAD_STAMP} DEPENDS ${SCRIPT} ${INFILE} ${GENERATED_THREAD_STAMP}
VERBATIM VERBATIM
) )
# More than one library target consumes the generated sources below. Route
# them through one explicit prerequisite so parallel Make builds cannot invoke
# the same generator twice and interleave writes to errno.c.
add_custom_target(akerror_generated
DEPENDS ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
)
# The generated errno table is produced by shelling out to `errno --list`
# (moreutils) and #include <errno.h>, neither of which is freestanding-safe.
# It is also useless there: akerr_init_errno() is only ever called under
# AKERR_USE_STDLIB (see src/error.c), so a freestanding build does not compile
# or link it into the library at all.
if(AKERR_USE_STDLIB)
set(AKERR_ERRNO_SOURCES ${GENERATED_ERRNO_C})
else()
set(AKERR_ERRNO_SOURCES)
endif()
add_library(akerror SHARED add_library(akerror SHARED
src/error.c src/error.c
${AKERR_ERRNO_SOURCES} ${GENERATED_ERRNO_C}
) )
add_dependencies(akerror akerror_generated)
target_include_directories(akerror PUBLIC target_include_directories(akerror PUBLIC
$<BUILD_INTERFACE:${GENERATED_DIR}/include> $<BUILD_INTERFACE:${GENERATED_DIR}/include>
@@ -253,25 +181,12 @@ else()
set(AKERR_THREADS_DEFINE AKERR_THREADS_NONE=1) set(AKERR_THREADS_DEFINE AKERR_THREADS_NONE=1)
endif() endif()
# PUBLIC and unconditional: the generated header #errors at compile time if
# AKERR_USE_STDLIB is OFF and this is not defined, and that check runs for any
# translation unit that includes the header -- the library's own sources as
# much as a consumer's. Harmless (and unused) when AKERR_USE_STDLIB is ON.
if(NOT AKERR_USE_STDLIB)
set(AKERR_RUNTIME_HEADER_DEFINE "AKERR_RUNTIME_HEADER=\"${AKERR_RUNTIME_HEADER}\"")
else()
set(AKERR_RUNTIME_HEADER_DEFINE "")
endif()
target_compile_definitions(akerror target_compile_definitions(akerror
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB_DEFINE} PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS} PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES} PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
PRIVATE ${AKERR_THREADS_DEFINE} PRIVATE ${AKERR_THREADS_DEFINE}
) )
if(AKERR_RUNTIME_HEADER_DEFINE)
target_compile_definitions(akerror PUBLIC ${AKERR_RUNTIME_HEADER_DEFINE})
endif()
if(AKERR_THREAD_SAFE) if(AKERR_THREAD_SAFE)
target_link_libraries(akerror PRIVATE Threads::Threads) target_link_libraries(akerror PRIVATE Threads::Threads)
@@ -285,35 +200,6 @@ set_target_properties(akerror PROPERTIES
akerr_instrument_for_coverage(akerror) akerr_instrument_for_coverage(akerror)
akerr_instrument_for_sanitizers(akerror) akerr_instrument_for_sanitizers(akerror)
# akerr_init() must terminate if it cannot reserve the library-owned status
# band. The production table sizes are deliberately PRIVATE, so exercise that
# otherwise unreachable startup failure with a second library target whose
# private registries cannot accept even the first reservation. Keeping this a
# distinct target is the test: no compile definition leaks into consumers or
# weakens the production library.
add_library(akerror_init_failure SHARED
src/error.c
${AKERR_ERRNO_SOURCES}
)
add_dependencies(akerror_init_failure akerror_generated)
target_include_directories(akerror_init_failure PUBLIC
${GENERATED_DIR}/include
)
target_compile_definitions(akerror_init_failure
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB_DEFINE}
PRIVATE AKERR_STATUS_NAME_SLOTS=8
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=0
PRIVATE ${AKERR_THREADS_DEFINE}
)
if(AKERR_RUNTIME_HEADER_DEFINE)
target_compile_definitions(akerror_init_failure PUBLIC ${AKERR_RUNTIME_HEADER_DEFINE})
endif()
if(AKERR_THREAD_SAFE)
target_link_libraries(akerror_init_failure PRIVATE Threads::Threads)
endif()
akerr_instrument_for_coverage(akerror_init_failure)
akerr_instrument_for_sanitizers(akerror_init_failure)
# Each test is one source file in tests/ built into test_<name> and registered # Each test is one source file in tests/ built into test_<name> and registered
# as CTest <name>. Tests expected to abort (unhandled error / contract # as CTest <name>. Tests expected to abort (unhandled error / contract
# violation) go in AKERR_WILL_FAIL_TESTS; all others must exit 0. # violation) go in AKERR_WILL_FAIL_TESTS; all others must exit 0.
@@ -341,7 +227,6 @@ set(AKERR_TESTS
err_registry_init_order err_registry_init_order
err_status_exception err_status_exception
err_copy_string err_copy_string
err_init_reservation_fatal
err_library_status_fatal err_library_status_fatal
err_refcount_double_fail err_refcount_double_fail
err_stacktrace_bounds err_stacktrace_bounds
@@ -369,18 +254,13 @@ endif()
set(AKERR_WILL_FAIL_TESTS set(AKERR_WILL_FAIL_TESTS
err_trace err_trace
err_improper_closure err_improper_closure
err_init_reservation_fatal
err_library_status_fatal err_library_status_fatal
) )
foreach(_test IN LISTS AKERR_TESTS) foreach(_test IN LISTS AKERR_TESTS)
add_executable(test_${_test} tests/${_test}.c) add_executable(test_${_test} tests/${_test}.c)
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
if(_test STREQUAL "err_init_reservation_fatal") target_link_libraries(test_${_test} PRIVATE akerror)
target_link_libraries(test_${_test} PRIVATE akerror_init_failure)
else()
target_link_libraries(test_${_test} PRIVATE akerror)
endif()
if(AKERR_THREAD_SAFE) if(AKERR_THREAD_SAFE)
target_link_libraries(test_${_test} PRIVATE Threads::Threads) target_link_libraries(test_${_test} PRIVATE Threads::Threads)
endif() endif()
@@ -425,14 +305,7 @@ if(Python3_FOUND)
# The script configures and drives its own instrumented build tree (under # The script configures and drives its own instrumented build tree (under
# ${CMAKE_BINARY_DIR}/coverage) so this build's binaries and its coverage # ${CMAKE_BINARY_DIR}/coverage) so this build's binaries and its coverage
# counters can never be stale or half-instrumented. Reports via gcov. # counters can never be stale or half-instrumented. Reports via gcov.
# Keep the convenient generic name at the top level, but namespace it when add_custom_target(coverage
# embedded so a parent project can provide its own coverage target.
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(AKERR_COVERAGE_TARGET coverage)
else()
set(AKERR_COVERAGE_TARGET akerror_coverage)
endif()
add_custom_target(${AKERR_COVERAGE_TARGET}
COMMAND ${Python3_EXECUTABLE} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py ${CMAKE_CURRENT_SOURCE_DIR}/scripts/coverage.py
--source-root ${CMAKE_CURRENT_SOURCE_DIR} --source-root ${CMAKE_CURRENT_SOURCE_DIR}
@@ -464,6 +337,7 @@ if(Python3_FOUND)
) )
endif() endif()
set(main_lib_dest "lib/my_library-${MY_LIBRARY_VERSION}")
install(TARGETS akerror install(TARGETS akerror
EXPORT akerrorTargets EXPORT akerrorTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
@@ -487,17 +361,8 @@ configure_package_config_file(
INSTALL_DESTINATION ${akerror_install_cmakedir} INSTALL_DESTINATION ${akerror_install_cmakedir}
) )
# The SOVERSION is the project major version, so packages with the same major
# are ABI-compatible and a different major must be rejected.
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/akerrorConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/akerrorConfigVersion.cmake"
DESTINATION ${akerror_install_cmakedir} DESTINATION ${akerror_install_cmakedir}
) )

View File

@@ -170,7 +170,7 @@ the exit code was the status truncated to a byte, and every consumer status
starts at 256. Use `akerr_exit()` instead of `exit()` — see starts at 256. Use `akerr_exit()` instead of `exit()` — see
[docs/exit-status.md](docs/exit-status.md). No ABI break. [docs/exit-status.md](docs/exit-status.md). No ABI break.
2.0.0 makes the library thread safe. That is an ABI break — `akerr_last_ignored` 2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
became thread-local storage and the pool now takes its own reference — so became thread-local storage and the pool now takes its own reference — so
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
consumer-sized status-name array with a private, ownership-enforced registry. consumer-sized status-name array with a private, ownership-enforced registry.

View File

@@ -1,38 +1,3 @@
# Bug fix: the AKERR_USE_STDLIB=OFF build, and a pool-exhaustion exit code (2.0.2)
`-DAKERR_USE_STDLIB=OFF` did not compile at all ([issue #12](https://source.starfort.tech/andrew/libakerror/issues/12)):
`bool`, `PATH_MAX` and `NULL` were used unconditionally in the public header
but only included under the stdlib branch, and the CMake option itself was
pasted straight into a preprocessor definition, so a boolean spelling like
`ON` silently evaluated to 0 in `#if AKERR_USE_STDLIB == 1`. Both are fixed:
`<stdbool.h>` and `<stddef.h>` are now included unconditionally (they are
freestanding-safe), and the CMake option is normalized to a plain `1`/`0`
before being stamped into the header.
`AKERR_USE_STDLIB=OFF` now has a mandatory, explicit contract:
`AKERR_RUNTIME_HEADER` must name a header providing `exit`, `memset`,
`snprintf`, `strcmp`, `strlen` and `strncpy` — the header `#error`s at compile
time naming those six symbols if it is unset. `AKERR_THREADS` must resolve to
`none`; the configure now fails outright (not a warning) if it would resolve
to `pthread`, since the pthread backend calls into libc. See
[docs/building.md](docs/building.md#dependencies).
`PATH_MAX`, used to size the `fname`/`function` fields of `akerr_ErrorContext`,
is retired in favor of a new `AKERR_MAX_ERROR_FNAME_LENGTH` build option. Its
default (4096) matches `PATH_MAX` on Linux/glibc, so `sizeof(akerr_ErrorContext)`
and the soname are unchanged unless you deliberately override it — **no ABI
break**.
**Behavior change, not an ABI break:** `ENSURE_ERROR_READY`'s pool-exhaustion
path — reached when every slot in `AKERR_ARRAY_ERROR` is checked out and
something still tries to raise — used to call `exit(1)` directly. It now calls
`akerr_exit(AKERR_EXIT_STATUS_UNREPRESENTABLE)`, the same terminal path every
other exit out of the library's status space goes through, so **the exit code
for that case changes from 1 to 125**. If you were checking `$?` for exactly
`1` to detect pool exhaustion specifically, check for 125 instead (and note
125 is shared with every other status an exit code cannot carry — see the note
on `AKERR_EXIT_STATUS_UNREPRESENTABLE` in the header).
# Bug fix: unhandled-error exit status (2.0.1) # Bug fix: unhandled-error exit status (2.0.1)
An unhandled error could kill the process and still report success. An unhandled error could kill the process and still report success.
@@ -87,7 +52,7 @@ accident.
What moved at the ABI: What moved at the ABI:
* `akerr_last_ignored` is thread-local storage. An ignored error is a fact * `__akerr_last_ignored` is thread-local storage. An ignored error is a fact
about the thread that ignored it, and one shared slot had two threads about the thread that ignored it, and one shared slot had two threads
overwriting each other's. The `IGNORE` macro expands at *your* call site, so overwriting each other's. The `IGNORE` macro expands at *your* call site, so
your objects reference the symbol under whichever storage model your header your objects reference the symbol under whichever storage model your header
@@ -167,11 +132,6 @@ One recursive lock covers both the pool and the registry, so error
correctness there is worth more than throughput, but a program that raises correctness there is worth more than throughput, but a program that raises
errors in a hot loop will feel it. errors in a hot loop will feel it.
Releasing the last reference to a context remains serialized under that same
pool lock, but it now resets only the handled/status/reported state, the string
heads, and the stack-trace cursor. It no longer wipes the whole context buffer,
so release is a fixed handful of stores rather than a tens-of-kilobytes write.
The per-thread last-ditch context is a whole `akerr_ErrorContext` (tens of The per-thread last-ditch context is a whole `akerr_ErrorContext` (tens of
kilobytes) in thread-local storage, allocated per thread on first use of the kilobytes) in thread-local storage, allocated per thread on first use of the
library from that thread. library from that thread.

View File

@@ -1,22 +0,0 @@
#ifndef AKERR_DEFAULT_RUNTIME_H_
#define AKERR_DEFAULT_RUNTIME_H_
/*
* Convenience default for AKERR_RUNTIME_HEADER, used when libakerror is
* configured -DAKERR_USE_STDLIB=OFF without also setting
* -DAKERR_RUNTIME_HEADER. It re-exposes the six symbols the freestanding
* build needs (exit, memset, snprintf, strcmp, strlen, strncpy) from the
* host's own C library, so building and testing the OFF configuration on an
* ordinary hosted machine does not require standing up a real freestanding
* runtime first.
*
* A genuinely freestanding consumer -- the whole point of
* AKERR_USE_STDLIB=OFF -- supplies their own header providing those six
* symbols and overrides the AKERR_RUNTIME_HEADER cache variable; this file is
* not meant for that use.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#endif /* AKERR_DEFAULT_RUNTIME_H_ */

View File

@@ -9,22 +9,13 @@ either of those.
| Option | Default | What it does | | Option | Default | What it does |
| ------ | ------- | ------------ | | ------ | ------- | ------------ |
| `AKERR_THREADS` | `auto` | Threading backend: `auto`, `pthread`, or `none`. `auto` takes POSIX threads and **fails the configure** if it cannot find them. See [Building single threaded](thread-safety.md#building-single-threaded). **Must be `none` when `AKERR_USE_STDLIB` is `OFF`** — the pthread backend calls into libc, and the configure fails otherwise. | | `AKERR_THREADS` | `auto` | Threading backend: `auto`, `pthread`, or `none`. `auto` takes POSIX threads and **fails the configure** if it cannot find them. See [Building single threaded](thread-safety.md#building-single-threaded). |
| `AKERR_USE_STDLIB` | `ON` | Link against the C standard library. See [Dependencies](#dependencies) for what you must supply instead when this is `OFF`. | | `AKERR_USE_STDLIB` | `ON` | Link against the C standard library. See [Dependencies](#dependencies) for what you must supply instead when this is `OFF`. |
| `AKERR_RUNTIME_HEADER` | *(empty)* | **Mandatory when `AKERR_USE_STDLIB` is `OFF`.** Header providing `exit`, `memset`, `snprintf`, `strcmp`, `strlen` and `strncpy`; the generated header `#error`s at compile time if it is unset. Defaults to a thin, libc-backed convenience header (`cmake/akerr_default_runtime.h`) so this repository's own `OFF` build and test suite work without a real freestanding runtime — a genuinely freestanding consumer should override it with their own header. |
| `AKERR_MAX_ERROR_FNAME_LENGTH` | `4096` | Bytes reserved for the `fname`/`function` fields of `akerr_ErrorContext`. Defaults to `PATH_MAX` on Linux/glibc, which keeps `sizeof(akerr_ErrorContext)` and the soname unchanged; changing it is an ABI break. |
| `AKERR_LAST_ERRNO_VALUE_FALLBACK` | `133` | `AKERR_LAST_ERRNO_VALUE` to stamp when `AKERR_USE_STDLIB` is `OFF`, since that configuration cannot shell out to `errno --list`. Defaults to 133 (Linux's `EHWPOISON`). |
| `AKERR_STATUS_NAME_SLOTS` | `4096` | Slots in the status-name table; 75% of it is usable. | | `AKERR_STATUS_NAME_SLOTS` | `4096` | Slots in the status-name table; 75% of it is usable. |
| `AKERR_MAX_RESERVED_STATUS_RANGES` | `64` | How many status ranges may be reserved in one process. | | `AKERR_MAX_RESERVED_STATUS_RANGES` | `64` | How many status ranges may be reserved in one process. |
| `AKERR_SANITIZE` | *(empty)* | Sanitizer list applied to the library and the tests, e.g. `thread` or `address,undefined`. | | `AKERR_SANITIZE` | *(empty)* | Sanitizer list applied to the library and the tests, e.g. `thread` or `address,undefined`. |
| `AKERR_COVERAGE` | `OFF` | Instrument the library with gcov counters. | | `AKERR_COVERAGE` | `OFF` | Instrument the library with gcov counters. |
**Behavior change (2.0.2):** pool exhaustion inside `ENSURE_ERROR_READY` (every
context-producing macro goes through it) used to call `exit(1)` directly. It
now calls `akerr_exit(AKERR_EXIT_STATUS_UNREPRESENTABLE)`, so a process that
runs out of pool slots exits **125** instead of **1**. See
[UPGRADING.md](../UPGRADING.md).
The two capacity options are applied `PRIVATE`: the tables live entirely in The two capacity options are applied `PRIVATE`: the tables live entirely in
`src/error.c`, so raising them never changes anything a consumer can see. See `src/error.c`, so raising them never changes anything a consumer can see. See
[UPGRADING.md](../UPGRADING.md) for what happens when you exhaust them. [UPGRADING.md](../UPGRADING.md) for what happens when you exhaust them.
@@ -33,59 +24,40 @@ The two capacity options are applied `PRIVATE`: the tables live entirely in
The build process relies upon `scripts/generrno.sh` which performs the following: The build process relies upon `scripts/generrno.sh` which performs the following:
1. When `AKERR_USE_STDLIB` is `ON`: executes `errno --list` and gathers up the 1. Executes `errno --list` and gathers up the output
output. When it is `OFF`, this is skipped entirely (`errno --list` needs 1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
moreutils and `<errno.h>`, neither freestanding-safe) and 2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
`AKERR_LAST_ERRNO_VALUE` is taken from the `AKERR_LAST_ERRNO_VALUE_FALLBACK`
cache variable instead.
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set
`AKERR_LAST_ERRNO_VALUE`, `AKERR_THREAD_SAFE`, and
`AKERR_MAX_ERROR_FNAME_LENGTH`.
2. Generates `src/errno.c`, which contains a function called by `akerr_init`
that initializes all of the status names for the previously defined values
of `errno`. Under `AKERR_USE_STDLIB=OFF` this file is a stub, and CMake does
not compile it into the library at all — `akerr_init_errno()` is never
called in that configuration.
Neither generated output is meant to be edited. Change the template or the generator. Neither output is meant to be edited. Change the template or the generator.
## Dependencies ## Dependencies
This library depends upon `stdlib`, and upon POSIX threads unless it is built This library depends upon `stdlib`, and upon POSIX threads unless it is built
with `-DAKERR_THREADS=none` (see [Thread safety](thread-safety.md)). If you with `-DAKERR_THREADS=none` (see [Thread safety](thread-safety.md)). If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
don't want to link against stdlib, build with `-DAKERR_USE_STDLIB=OFF` (which
requires `-DAKERR_THREADS=none` — see the options table above) and supply a
header, via the `AKERR_RUNTIME_HEADER` cache variable, that provides:
- `memset` function - `memset` function
- `strncpy` function - `strncpy` function
- `strlen` function - `strlen` function
- `strcmp` function - `strcmp` function
- `snprintf` function - `sprintf` function
- `exit` function - `exit` function
- `bool` type - `bool` type
- `NULL` type - `NULL` type
- `size_t` type
- `INT_MAX` constant - `INT_MAX` constant
- `PATH_MAX` constant
`<stdbool.h>` and `<stddef.h>` — which give you `bool`/`NULL`/`size_t` — are
included unconditionally by the public header regardless of
`AKERR_USE_STDLIB`, since both are freestanding-safe. `AKERR_RUNTIME_HEADER` is
mandatory in this configuration: the generated header `#error`s at compile
time if it is unset.
... then you can compile it thusly: ... then you can compile it thusly:
``` ```
cmake -S . -B build -DAKERR_USE_STDLIB=OFF -DAKERR_THREADS=none \ cmake -S . -B build -DAKERR_USE_STDLIB=OFF
-DAKERR_RUNTIME_HEADER=/path/to/your/runtime.h
cmake --build build cmake --build build
cmake --install build cmake --install build
``` ```
If you omit `-DAKERR_RUNTIME_HEADER`, the build falls back to a convenience **Known defect:** that configuration does not currently compile. `bool`,
header backed by the host's own libc (see the options table above), so the `PATH_MAX` and `NULL` are used unconditionally but only included under the
`OFF` configuration still builds and its test suite still runs on an ordinary stdlib branch, so the header's includes need untangling before
hosted machine — useful for exercising the freestanding code paths without a `-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement
real freestanding runtime, but not what an actually freestanding consumer must provide. That configuration does not currently compile -- `bool`, `PATH_MAX` and `NULL`
wants. Supply your own header to get the real thing. are used unconditionally but included only under the stdlib branch. See
[issue #12](https://source.starfort.tech/andrew/libakerror/issues/12).

View File

@@ -14,12 +14,11 @@ What that covers:
against each other and against lookups. Two threads reserving the same range against each other and against lookups. Two threads reserving the same range
cannot both win — exactly one gets `NULL` and the other gets cannot both win — exactly one gets `NULL` and the other gets
`AKERR_STATUS_RANGE_OVERLAP` naming the winner. `AKERR_STATUS_RANGE_OVERLAP` naming the winner.
* **Per-thread state.** `IGNORE` copies the swallowed context into its * **Per-thread state.** `IGNORE` uses `__akerr_last_ignored` as a scratch pointer
thread-local `akerr_last_ignored` snapshot before releasing the pool slot. while it logs an error, then releases the context and clears the pointer.
The snapshot remains valid until that thread ignores another error, so a That scratch pointer and the last-ditch context used to report
later pool checkout cannot overwrite it. The snapshot and the last-ditch `akerr_release_error(NULL)` are thread-local, so concurrent calls cannot
context used to report `akerr_release_error(NULL)` are thread-local, so overwrite each other's state.
concurrent calls cannot overwrite each other's state.
* **Handing a context from one thread to another.** A context is not thread * **Handing a context from one thread to another.** A context is not thread
state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it
outlives the thread that raised it. The reference count is the only field the outlives the thread that raised it. The reference count is the only field the

View File

@@ -1,42 +1,12 @@
#ifndef _AKERR_H_ #ifndef _AKERR_H_
#define _AKERR_H_ #define _AKERR_H_
/* #if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
* A consumer compiling this header directly (not through the CMake package,
* which always stamps a numeric AKERR_USE_STDLIB=0/1 -- see CMakeLists.txt)
* gets the hosted default.
*/
#ifndef AKERR_USE_STDLIB
#define AKERR_USE_STDLIB 1
#endif
/*
* <stdbool.h> and <stddef.h> are freestanding-safe (C99/C11 4p6): they define
* only bool/true/false and NULL/size_t, nothing that requires an operating
* system underneath. Include them unconditionally so both configurations get
* those types. Everything that actually talks to a hosted environment --
* stdlib.h, string.h, stdio.h -- stays behind AKERR_USE_STDLIB.
*/
#include <stdbool.h>
#include <stddef.h>
#if AKERR_USE_STDLIB
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#else #include <limits.h>
/*
* Freestanding runtime contract. AKERR_USE_STDLIB=OFF still needs exit,
* memset, snprintf, strcmp, strlen and strncpy (see the FAIL/ENSURE_ERROR_READY
* macros and src/error.c below) -- this library does not implement its own
* copies of them. Define AKERR_RUNTIME_HEADER to a header that provides all
* six before including this one.
*/
#ifdef AKERR_RUNTIME_HEADER
#include AKERR_RUNTIME_HEADER
#else
#error "AKERR_USE_STDLIB is OFF: define AKERR_RUNTIME_HEADER to a header providing exit, memset, snprintf, strcmp, strlen and strncpy"
#endif
#endif #endif
/* /*
@@ -45,7 +15,7 @@
* scripts/generrno.sh stamps this value in at build time from the AKERR_THREADS * 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 * 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 * rather than defined by the consumer on purpose: whether the library
* serializes its global state and whether akerr_last_ignored is a * serializes its global state and whether __akerr_last_ignored is a
* thread-local are the same decision, and a consumer that disagreed with the * thread-local are the same decision, and a consumer that disagreed with the
* library about it would link against a differently shaped symbol. * library about it would link against a differently shaped symbol.
* *
@@ -82,15 +52,7 @@
#define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384 #define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384
#define AKERR_MAX_ERROR_NAME_LENGTH 64 #define AKERR_MAX_ERROR_NAME_LENGTH 64
/* #define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
* scripts/generrno.sh stamps this in from the AKERR_MAX_ERROR_FNAME_LENGTH
* CMake cache variable, the same way it stamps AKERR_THREAD_SAFE and
* AKERR_LAST_ERRNO_VALUE. It used to be PATH_MAX, which is not available in a
* freestanding build; the default here (4096) matches PATH_MAX on Linux/glibc
* so sizeof(akerr_ErrorContext) and the soname are unchanged unless you
* deliberately override it.
*/
#define AKERR_MAX_ERROR_FNAME_LENGTH 4096
#define AKERR_MAX_ERROR_FUNCTION_LENGTH 128 #define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
#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) #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)
@@ -211,12 +173,12 @@ extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
extern akerr_ErrorLogFunction akerr_log_method; extern akerr_ErrorLogFunction akerr_log_method;
/* /*
* IGNORE()'s public per-thread snapshot. IGNORE() copies the swallowed error here * IGNORE()'s per-thread scratch pointer. It is non-NULL only while IGNORE()
* before releasing its pool context, so this remains a useful debugging aid * logs the swallowed error; IGNORE() releases the context and clears this
* after the pool slot is reused. The snapshot is read-only and is replaced by * pointer before returning to its caller. Thread local only when
* the next ignored error. Thread local only when AKERR_THREAD_SAFE is 1. * AKERR_THREAD_SAFE is 1.
*/ */
static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored; extern AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
/* /*
* Drop one reference, returning NULL once the last one is gone so the caller can * Drop one reference, returning NULL once the last one is gone so the caller can
@@ -334,7 +296,7 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
__err_context = akerr_next_error(); \ __err_context = akerr_next_error(); \
if ( __err_context == NULL ) { \ if ( __err_context == NULL ) { \
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \ akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
akerr_exit(AKERR_EXIT_STATUS_UNREPRESENTABLE); \ exit(1); \
} \ } \
} }
@@ -473,20 +435,11 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
FINISH_LOGIC(__err_context, true); FINISH_LOGIC(__err_context, true);
#define IGNORE(__stmt) \ #define IGNORE(__stmt) \
do { \ __akerr_last_ignored = __stmt; \
akerr_ErrorContext *__akerr_ignored = __stmt; \ if ( __akerr_last_ignored != NULL ) { \
if ( __akerr_ignored != NULL ) { \ LOG_ERROR_WITH_MESSAGE(__akerr_last_ignored, "** IGNORED ERROR **"); \
memcpy(&akerr_last_ignored, __akerr_ignored, \ RELEASE_ERROR(__akerr_last_ignored); \
sizeof(akerr_last_ignored)); \ }
akerr_last_ignored.stacktracebufptr = \
(char *)&akerr_last_ignored.stacktracebuf; \
akerr_ErrorContext *__akerr_ignored_snapshot = \
&akerr_last_ignored; \
LOG_ERROR_WITH_MESSAGE(__akerr_ignored_snapshot, \
"** IGNORED ERROR **"); \
RELEASE_ERROR(__akerr_ignored); \
} \
} while ( 0 )
#define CLEANUP \ #define CLEANUP \
}; };

View File

@@ -7,39 +7,18 @@ outdir=$2
# with the library about whether it locks and whether its per-thread state is # with the library about whether it locks and whether its per-thread state is
# thread local. Defaults to 1 for a hand-run of this script. # thread local. Defaults to 1 for a hand-run of this script.
thread_safe=${3:-1} thread_safe=${3:-1}
# 1 for a normal (libc-linked) build, 0 for -DAKERR_USE_STDLIB=OFF. The
# freestanding build cannot shell out to `errno --list` (moreutils) or
# #include <errno.h>, so it skips the errno scrape entirely and stamps
# AKERR_LAST_ERRNO_VALUE from a fixed fallback instead. Defaults to 1 for a
# hand-run of this script.
use_stdlib=${4:-1}
# AKERR_LAST_ERRNO_VALUE to stamp when use_stdlib is 0. Defaults to 133
# (Linux's EHWPOISON) so the reserved band assertion in akerror.tmpl.h still
# holds.
last_errno_fallback=${5:-133}
# Bytes reserved for the fname/function fields of akerr_ErrorContext. Defaults
# to 4096 (PATH_MAX on Linux/glibc) so sizeof(akerr_ErrorContext) and the
# soname stay unchanged from before this became a build option.
max_error_fname_length=${6:-4096}
if [ "${thread_safe}" != "0" ] && [ "${thread_safe}" != "1" ]; then if [ "${thread_safe}" != "0" ] && [ "${thread_safe}" != "1" ]; then
echo "$0: thread-safe argument must be 0 or 1, got '${thread_safe}'" >&2 echo "$0: thread-safe argument must be 0 or 1, got '${thread_safe}'" >&2
exit 1 exit 1
fi fi
if [ "${use_stdlib}" != "0" ] && [ "${use_stdlib}" != "1" ]; then
echo "$0: use-stdlib argument must be 0 or 1, got '${use_stdlib}'" >&2
exit 1
fi
mkdir -p ${outdir}/src mkdir -p ${outdir}/src
mkdir -p ${outdir}/include mkdir -p ${outdir}/include
rm -f ${outdir}/src/errno.c rm -f ${outdir}/src/errno.c
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
if [ "${use_stdlib}" = "1" ]; then echo "#include <errno.h>" >> ${outdir}/src/errno.c
echo "#include <akerror.h>" >> ${outdir}/src/errno.c cat >> ${outdir}/src/errno.c <<'EOF'
echo "#include <errno.h>" >> ${outdir}/src/errno.c
cat >> ${outdir}/src/errno.c <<'EOF'
/* /*
* These names belong to the library's own reserved band, and this runs from * These names belong to the library's own reserved band, and this runs from
@@ -50,25 +29,15 @@ if [ "${use_stdlib}" = "1" ]; then
* control flow no test can reach. * control flow no test can reach.
*/ */
EOF EOF
echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c
maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1) maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1)
errno --list | while read LINE; do errno --list | while read LINE; do
define=$(echo "$LINE" | cut -d ' ' -f 1); define=$(echo "$LINE" | cut -d ' ' -f 1);
value=$(echo "$LINE" | cut -d ' ' -f 2); value=$(echo "$LINE" | cut -d ' ' -f 2);
desc=$(echo "$LINE" | cut -d ' ' -f 3-); desc=$(echo "$LINE" | cut -d ' ' -f 3-);
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ; echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
done; done;
echo "}" >> ${outdir}/src/errno.c echo "}" >> ${outdir}/src/errno.c
else
# Freestanding: no `errno --list` shellout (requires moreutils and
# <errno.h>, neither freestanding-safe), and no errno.c at all -- CMake
# does not compile it into the library under AKERR_USE_STDLIB=OFF. Still
# write a stub so the OUTPUT this rule promises always exists.
echo "/* AKERR_USE_STDLIB=OFF: no errno table generated. */" >> ${outdir}/src/errno.c
maxval=${last_errno_fallback}
fi
sed -e "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" \ sed -e "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" \
-e "s/#define AKERR_THREAD_SAFE .*/#define AKERR_THREAD_SAFE ${thread_safe}/" \ -e "s/#define AKERR_THREAD_SAFE .*/#define AKERR_THREAD_SAFE ${thread_safe}/" \
-e "s/#define AKERR_MAX_ERROR_FNAME_LENGTH .*/#define AKERR_MAX_ERROR_FNAME_LENGTH ${max_error_fname_length}/" \
${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h

View File

@@ -1,10 +1,6 @@
#include "akerror.h" #include "akerror.h"
#include "lock.h" #include "lock.h"
/* INT_MAX (used below in akerr_reserve_status_range_locked) only, not in the #if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
* public header: <limits.h> is freestanding-safe, but nothing else in this
* file's freestanding build needs it, so it stays out of the shared header. */
#include <limits.h>
#if AKERR_USE_STDLIB
#include <stdlib.h> #include <stdlib.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
@@ -24,10 +20,10 @@
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage * It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage
* is allocated per thread only when that thread first touches the library's * is allocated per thread only when that thread first touches the library's
* thread-local block, and the alternative is a shared buffer that two threads * thread-local block, and the alternative is a shared buffer that two threads
* can be writing at once. The per-thread IGNORE() snapshot lives in the public * can be writing at once.
* template header because the macro copies into it at the call site.
*/ */
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch; static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error; akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
akerr_ErrorLogFunction akerr_log_method = NULL; akerr_ErrorLogFunction akerr_log_method = NULL;
@@ -92,10 +88,7 @@ typedef struct
static akerr_StatusName akerr_status_names[AKERR_STATUS_NAME_SLOTS]; static akerr_StatusName akerr_status_names[AKERR_STATUS_NAME_SLOTS];
static int akerr_status_name_count; static int akerr_status_name_count;
/* C has no portable zero-length arrays. Keep one unused physical slot when a static akerr_StatusRange akerr_status_ranges[AKERR_MAX_RESERVED_STATUS_RANGES];
* test build sets the logical capacity to zero to drive init's fatal path. */
static akerr_StatusRange akerr_status_ranges[
AKERR_MAX_RESERVED_STATUS_RANGES > 0 ? AKERR_MAX_RESERVED_STATUS_RANGES : 1];
static int akerr_status_range_count; static int akerr_status_range_count;
akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR]; akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
@@ -158,7 +151,7 @@ int akerr_valid_error_address(akerr_ErrorContext *ptr)
void akerr_default_logger(const char *fmt, ...) void akerr_default_logger(const char *fmt, ...)
{ {
#if AKERR_USE_STDLIB #if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
va_list ap; va_list ap;
va_start(ap, fmt); va_start(ap, fmt);
@@ -240,6 +233,7 @@ static void akerr_init_state(void)
AKERR_ARRAY_ERROR[i].arrayid = i; AKERR_ARRAY_ERROR[i].arrayid = i;
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf; AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
} }
__akerr_last_ignored = NULL;
(void)akerr_last_ditch_context(); (void)akerr_last_ditch_context();
if ( akerr_log_method == NULL ) { if ( akerr_log_method == NULL ) {
akerr_log_method = &akerr_default_logger; akerr_log_method = &akerr_default_logger;
@@ -289,7 +283,7 @@ static void akerr_init_state(void)
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name"); __akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full"); __akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name"); __akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
#if AKERR_USE_STDLIB #if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
akerr_init_errno(); akerr_init_errno();
#endif #endif
@@ -383,10 +377,10 @@ akerr_ErrorContext *akerr_next_error()
} }
/* /*
* The reset returns the slot to the pool, so it and the decrement that triggers * The wipe returns the slot to the pool, so it and the decrement that triggers
* it are one operation under the lock. Otherwise a thread that saw the count * it are one operation under the lock. Otherwise a thread that saw the count
* reach zero could be handed the slot by akerr_next_error() and start writing * reach zero could be handed the slot by akerr_next_error() and start writing
* its error into it while the releasing thread was still resetting it. * its error into it while the releasing thread was still memsetting it.
*/ */
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err) akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
{ {
@@ -404,13 +398,7 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
} }
if ( err->refcount == 0 ) { if ( err->refcount == 0 ) {
oldid = err->arrayid; oldid = err->arrayid;
err->handled = false; memset(err, 0x00, sizeof(akerr_ErrorContext));
err->status = 0;
err->reported = false;
err->message[0] = '\0';
err->fname[0] = '\0';
err->function[0] = '\0';
err->stacktracebuf[0] = '\0';
err->stacktracebufptr = (char *)&err->stacktracebuf; err->stacktracebufptr = (char *)&err->stacktracebuf;
err->arrayid = oldid; err->arrayid = oldid;
remaining = NULL; remaining = NULL;
@@ -676,7 +664,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range_locked(int
"must not overflow int)", "must not overflow int)",
count, first_status, owner == NULL ? "(null)" : owner, count, first_status, owner == NULL ? "(null)" : owner,
AKERR_MAX_STATUS_RANGE_OWNER_LENGTH); AKERR_MAX_STATUS_RANGE_OWNER_LENGTH);
last_status = first_status + (count - 1); last_status = first_status + count - 1;
for ( int i = 0; i < akerr_status_range_count; i++ ) { for ( int i = 0; i < akerr_status_range_count; i++ ) {
if ( first_status <= akerr_status_ranges[i].last && if ( first_status <= akerr_status_ranges[i].last &&

View File

@@ -33,12 +33,10 @@
/* /*
* PTHREAD_MUTEX_RECURSIVE is XSI, so glibc hides it under a strict -std=c99 * 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 * without _XOPEN_SOURCE. No feature-test macro is defined here, because the
* only ever compiled with AKERR_THREADS_PTHREAD, which CMake now refuses to * public header already needs the same one for PATH_MAX: a build strict enough
* pair with AKERR_USE_STDLIB=OFF (see the AKERR_THREADS/AKERR_USE_STDLIB * to lose one has already lost the other. Build with -D_XOPEN_SOURCE=700 if you
* check in CMakeLists.txt), so whatever default feature-test macros the host * need strict C99.
* 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 #if defined(AKERR_THREADS_PTHREAD) && AKERR_THREADS_PTHREAD == 1

View File

@@ -107,7 +107,7 @@ The remaining survivors are dominated by:
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of * **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`, file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`,
`akerr_last_ignored`) changes nothing, because C already zero-initializes `__akerr_last_ignored`) changes nothing, because C already zero-initializes
objects with static storage duration. `int oldid = 0;``1` is likewise objects with static storage duration. `int oldid = 0;``1` is likewise
dead: it is overwritten before use, and so is clearing `akerr_initializing` dead: it is overwritten before use, and so is clearing `akerr_initializing`
at the end of initialization — nothing reads that flag once the once-routine at the end of initialization — nothing reads that flag once the once-routine

View File

@@ -26,13 +26,7 @@ int main(void)
char *nm = akerr_name_for_status(EACCES, NULL); char *nm = akerr_name_for_status(EACCES, NULL);
AKERR_CHECK(nm != NULL); AKERR_CHECK(nm != NULL);
AKERR_CHECK(nm[0] != '\0'); AKERR_CHECK(nm[0] != '\0');
#if AKERR_USE_STDLIB
/* akerr_init_errno() -- the only thing that registers a name for a host
* errno -- is not called when AKERR_USE_STDLIB is OFF (see
* akerr_init_state() in src/error.c), so EACCES deliberately reads back
* as "Unknown Error" in that configuration. */
AKERR_CHECK(strcmp(nm, "Unknown Error") != 0); AKERR_CHECK(strcmp(nm, "Unknown Error") != 0);
#endif
AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL), AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL),
"Unknown Error") == 0); "Unknown Error") == 0);

View File

@@ -1,8 +1,7 @@
#include "akerror.h" #include "akerror.h"
#include "err_capture.h" #include "err_capture.h"
#include <string.h>
/* IGNORE snapshots and logs an error, releases its pool slot, then continues. */ /* IGNORE logs and releases an error, then lets execution continue. */
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
@@ -19,14 +18,10 @@ int main(void)
(void)e; (void)e;
/* More failures than the pool has slots must remain safe: a leaking /* More failures than the pool has slots must remain safe: a leaking
* IGNORE used to exhaust the pool and terminate the process here. The * IGNORE used to exhaust the pool and terminate the process here. */
* copied snapshot must also survive the slot being reused on the next
* iteration. */
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR + 1; i++ ) { for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR + 1; i++ ) {
IGNORE(boom()); IGNORE(boom());
AKERR_CHECK(akerr_last_ignored.status == AKERR_VALUE); AKERR_CHECK(__akerr_last_ignored == NULL);
AKERR_CHECK(strcmp(akerr_last_ignored.message,
"this error is ignored on purpose") == 0);
AKERR_CHECK(akerr_slots_in_use() == 0); AKERR_CHECK(akerr_slots_in_use() == 0);
} }
reached_after_ignore = 1; reached_after_ignore = 1;

View File

@@ -1,20 +0,0 @@
#include "akerror.h"
#include <stdio.h>
/*
* This executable links to akerror_init_failure, a test-only library target
* with no status-range slots. The first reservation in akerr_init() must be
* terminal: continuing would leave every library status unowned and make all
* subsequent name registrations invalid.
*
* CTest marks this WILL_FAIL. Reaching the message and returning zero means
* initialization swallowed its own reservation failure.
*/
int main(void)
{
akerr_init();
fprintf(stderr, "err_init_reservation_fatal: akerr_init did not terminate\n");
return 0;
}

View File

@@ -1,32 +1,24 @@
#include "akerror.h" #include "akerror.h"
#include "err_capture.h" #include "err_capture.h"
#include <string.h>
/* /*
* Releasing an error context back to the pool must reset the state that affects * Releasing an error context back to the pool must wipe it, so the next caller
* the next caller. In particular, a handled error must not make a fresh error * that checks it out never sees stale status/message/stacktrace from a previous
* look handled when its slot is recycled. * error. Mutation testing showed the clearing memset in akerr_release_error
* could be deleted without any test noticing.
*/ */
static int unhandled_calls = 0;
static int unhandled_status = 0;
static void test_unhandled_handler(akerr_ErrorContext *errctx)
{
unhandled_calls++;
unhandled_status = (errctx != NULL) ? errctx->status : 0;
}
akerr_ErrorContext *boom(void) akerr_ErrorContext *boom(void)
{ {
PREPARE_ERROR(e); PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_VALUE, "first error is handled"); FAIL_RETURN(e, AKERR_VALUE, "stale dirty message that must not survive");
} }
int main(void) int main(void)
{ {
akerr_capture_install(); akerr_capture_install();
akerr_init(); akerr_init();
akerr_handler_unhandled_error = &test_unhandled_handler;
/* Raise and fully handle an error; FINISH_NORETURN releases it to the pool. */ /* Raise and fully handle an error; FINISH_NORETURN releases it to the pool. */
PREPARE_ERROR(e); PREPARE_ERROR(e);
@@ -40,31 +32,13 @@ int main(void)
AKERR_CHECK(e == NULL); AKERR_CHECK(e == NULL);
/* A fresh error in the recycled slot must not inherit handled=true. */ /* The next context handed out is the slot we just released: it must be clean. */
PREPARE_ERROR(fresh);
ATTEMPT {
CATCH(fresh, boom());
} CLEANUP {
} PROCESS(fresh) {
} FINISH_NORETURN(fresh);
AKERR_CHECK(unhandled_calls == 1);
AKERR_CHECK(unhandled_status == AKERR_VALUE);
AKERR_CHECK(fresh == NULL);
/* The next context handed out is the same slot, with recycle state reset. */
akerr_ErrorContext *slot = akerr_next_error(); akerr_ErrorContext *slot = akerr_next_error();
AKERR_CHECK(slot != NULL); AKERR_CHECK(slot != NULL);
AKERR_CHECK(slot->handled == false);
AKERR_CHECK(slot->status == 0); AKERR_CHECK(slot->status == 0);
AKERR_CHECK(slot->reported == false);
AKERR_CHECK(slot->message[0] == '\0'); AKERR_CHECK(slot->message[0] == '\0');
AKERR_CHECK(slot->fname[0] == '\0');
AKERR_CHECK(slot->function[0] == '\0');
AKERR_CHECK(slot->stacktracebuf[0] == '\0'); AKERR_CHECK(slot->stacktracebuf[0] == '\0');
AKERR_CHECK(slot->stacktracebufptr == (char *)&slot->stacktracebuf); AKERR_CHECK(strstr(slot->message, "stale dirty message") == NULL);
RELEASE_ERROR(slot);
AKERR_CHECK(akerr_slots_in_use() == 0);
fprintf(stderr, "err_release_clears ok\n"); fprintf(stderr, "err_release_clears ok\n");
return 0; return 0;

View File

@@ -90,9 +90,6 @@ static void one_checkout(akerr_ThreadArg *arg)
static void *pool_body(void *raw) static void *pool_body(void *raw)
{ {
akerr_ThreadArg *arg = raw; akerr_ThreadArg *arg = raw;
char expected[64];
snprintf(expected, sizeof(expected), "ignored by thread %d", arg->id);
pthread_barrier_wait(arg->barrier); pthread_barrier_wait(arg->barrier);
for ( int i = 0; i < ITERATIONS; i++ ) { for ( int i = 0; i < ITERATIONS; i++ ) {
@@ -100,21 +97,10 @@ static void *pool_body(void *raw)
one_checkout(arg); one_checkout(arg);
} }
/* IGNORE's snapshot is thread-local while logging and remains valid after /* IGNORE's scratch pointer is thread-local while logging and cleared after
* release. Concurrent ignored errors must all return their pool slots. */ * release. Concurrent ignored errors must all return their pool slots. */
IGNORE(ignorable(arg)); IGNORE(ignorable(arg));
AKERR_TCHECK(arg, akerr_last_ignored.status == AKERR_IO); AKERR_TCHECK(arg, __akerr_last_ignored == NULL);
AKERR_TCHECK(arg, strcmp(akerr_last_ignored.message, expected) == 0);
/* Reuse a slot after IGNORE and prove that the copied snapshot did not
* become an alias for the newly acquired context. */
akerr_ErrorContext *reused = akerr_next_error();
AKERR_TCHECK(arg, reused != NULL);
if ( reused != NULL ) {
RELEASE_ERROR(reused);
}
AKERR_TCHECK(arg, akerr_last_ignored.status == AKERR_IO);
AKERR_TCHECK(arg, strcmp(akerr_last_ignored.message, expected) == 0);
return NULL; return NULL;
} }

View File

@@ -1,14 +0,0 @@
/*
* Compile-only proof that a genuinely freestanding consumer (-nostdinc
* -ffreestanding, no libc) can include the generated header under
* AKERR_USE_STDLIB=OFF. Never linked or run -- see .gitea/workflows/ci.yaml.
*/
#define AKERR_USE_STDLIB 0
#define AKERR_RUNTIME_HEADER "freestanding_fixture_runtime.h"
#include "akerror.h"
akerr_ErrorContext *akerr_freestanding_fixture_example(void)
{
PREPARE_ERROR(e);
FAIL_RETURN(e, AKERR_VALUE, "freestanding fixture example error");
}

View File

@@ -1,19 +0,0 @@
#ifndef AKERR_FIXTURE_RUNTIME_H_
#define AKERR_FIXTURE_RUNTIME_H_
/*
* A minimal AKERR_RUNTIME_HEADER for tests/freestanding_fixture.c: just
* enough declarations (no definitions -- this fixture is compiled, never
* linked) to prove that -DAKERR_USE_STDLIB=OFF's public header needs nothing
* from a hosted environment beyond these six symbols and the freestanding-safe
* <stddef.h>/<stdbool.h>. size_t comes from <stddef.h>, already included by
* akerror.h before this header is pulled in.
*/
void exit(int status);
void *memset(void *s, int c, size_t n);
int snprintf(char *str, size_t size, const char *format, ...);
int strcmp(const char *a, const char *b);
size_t strlen(const char *s);
char *strncpy(char *dest, const char *src, size_t n);
#endif /* AKERR_FIXTURE_RUNTIME_H_ */