Compare commits
25 Commits
32
...
5eaa956f50
| Author | SHA1 | Date | |
|---|---|---|---|
|
5eaa956f50
|
|||
|
756933c600
|
|||
|
be24f80022
|
|||
|
5ff87908e7
|
|||
|
64da04e83b
|
|||
|
ba2430bfa1
|
|||
|
f1283e21a3
|
|||
| 11d21068df | |||
|
0bb3a4d52c
|
|||
|
539293cc1c
|
|||
|
9f0034a56e
|
|||
|
0c0d81249f
|
|||
|
426efbb2d4
|
|||
|
4ae1decde2
|
|||
|
4212ff0b28
|
|||
|
de13b290d4
|
|||
|
3e24356f07
|
|||
|
8a026d3006
|
|||
|
792e646957
|
|||
|
43516c7e73
|
|||
|
536a269aad
|
|||
|
10f7203e8f
|
|||
|
43f46dca64
|
|||
|
e5f761662c
|
|||
|
4daa411f3f
|
@@ -37,51 +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:
|
||||||
@@ -112,6 +67,34 @@ jobs:
|
|||||||
fail_on_failure: 'false'
|
fail_on_failure: 'false'
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
|
thread_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
|
||||||
|
# The thread tests assert exclusive ownership of pool slots and reserved
|
||||||
|
# ranges, which is checkable without tooling and runs in the job above.
|
||||||
|
# This is the run that proves there is no data race underneath them.
|
||||||
|
# libtsan arrives with gcc (libgcc-N-dev depends on it); the script
|
||||||
|
# disables ASLR because TSan aborts on kernels with vm.mmap_rnd_bits > 28.
|
||||||
|
- name: thread sanitizer
|
||||||
|
run: |
|
||||||
|
scripts/thread_test.sh build/tsan --output-junit "$(pwd)/tsan-junit.xml"
|
||||||
|
- name: publish thread sanitizer results
|
||||||
|
if: always()
|
||||||
|
uses: mikepenz/action-junit-report@v4
|
||||||
|
with:
|
||||||
|
report_paths: 'tsan-junit.xml'
|
||||||
|
annotate_only: true
|
||||||
|
detailed_summary: true
|
||||||
|
include_passed: true
|
||||||
|
fail_on_failure: 'true'
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
mutation_test:
|
mutation_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Run the repository's cheap gates before a push reaches the forge. CI remains
|
|
||||||
# the hard gate; this hook makes the common failure modes cheap to find.
|
|
||||||
#
|
|
||||||
# Install once per clone:
|
|
||||||
#
|
|
||||||
# git config core.hooksPath .githooks
|
|
||||||
#
|
|
||||||
# The mutation harness is deliberately opt-in because it is slow:
|
|
||||||
#
|
|
||||||
# AKERR_HOOK_MUTATION=1 git push
|
|
||||||
#
|
|
||||||
# Git's normal escape hatch remains available:
|
|
||||||
#
|
|
||||||
# git push --no-verify
|
|
||||||
#
|
|
||||||
# Builds go under .git/akerr-prepush so the hook does not disturb build/.
|
|
||||||
# Override that location with AKERR_HOOK_BUILD_DIR when needed.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
ZERO_SHA=0000000000000000000000000000000000000000
|
|
||||||
|
|
||||||
root=$(git rev-parse --show-toplevel) || exit 1
|
|
||||||
cd "$root" || exit 1
|
|
||||||
|
|
||||||
# A delete-only push, or an invocation with no refs on stdin, has no new code
|
|
||||||
# to validate.
|
|
||||||
has_updates=0
|
|
||||||
while read -r _local_ref local_sha _remote_ref _remote_sha; do
|
|
||||||
if [ "$local_sha" != "$ZERO_SHA" ]; then
|
|
||||||
has_updates=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$has_updates" -eq 0 ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
builddir="${AKERR_HOOK_BUILD_DIR:-$(git rev-parse --git-dir)/akerr-prepush}"
|
|
||||||
mkdir -p "$builddir" || exit 1
|
|
||||||
logfile="$builddir/last.log"
|
|
||||||
|
|
||||||
# Keep normal output short, but preserve the complete failing command output.
|
|
||||||
run() {
|
|
||||||
if ! "$@" > "$logfile" 2>&1; then
|
|
||||||
echo >&2
|
|
||||||
echo "pre-push: FAILED: $*" >&2
|
|
||||||
echo "---------------------------------------------------------------" >&2
|
|
||||||
cat "$logfile" >&2
|
|
||||||
echo "---------------------------------------------------------------" >&2
|
|
||||||
echo "pre-push: push aborted. Use 'git push --no-verify' to override." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -f scripts/cppcheck.sh ] && command -v cppcheck > /dev/null 2>&1; then
|
|
||||||
echo "pre-push: cppcheck"
|
|
||||||
run bash scripts/cppcheck.sh
|
|
||||||
elif [ ! -f scripts/cppcheck.sh ]; then
|
|
||||||
echo "pre-push: scripts/cppcheck.sh not present, skipping cppcheck"
|
|
||||||
else
|
|
||||||
echo "pre-push: cppcheck not installed, skipping cppcheck"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "pre-push: default build + ctest"
|
|
||||||
run cmake -S . -B "$builddir/default"
|
|
||||||
run cmake --build "$builddir/default"
|
|
||||||
run ctest --test-dir "$builddir/default" --output-on-failure
|
|
||||||
|
|
||||||
echo "pre-push: AKERR_USE_STDLIB=OFF build + ctest"
|
|
||||||
run cmake -S . -B "$builddir/stdlib-off" \
|
|
||||||
-DAKERR_USE_STDLIB=OFF -DAKERR_THREADS=none
|
|
||||||
run cmake --build "$builddir/stdlib-off"
|
|
||||||
run ctest --test-dir "$builddir/stdlib-off" --output-on-failure
|
|
||||||
|
|
||||||
if [ "${AKERR_HOOK_MUTATION:-0}" = "1" ]; then
|
|
||||||
if command -v python3 > /dev/null 2>&1; then
|
|
||||||
echo "pre-push: mutation testing (this takes a while)"
|
|
||||||
if ! python3 scripts/mutation_test.py \
|
|
||||||
--target src/error.c \
|
|
||||||
--threshold "${AKERR_MUTATION_THRESHOLD:-65}"; then
|
|
||||||
echo >&2
|
|
||||||
echo "pre-push: mutation score below threshold. Push aborted." >&2
|
|
||||||
echo "pre-push: use 'git push --no-verify' to override." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "pre-push: python3 not installed, skipping mutation testing"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "pre-push: OK"
|
|
||||||
exit 0
|
|
||||||
37
AGENTS.md
37
AGENTS.md
@@ -11,13 +11,6 @@ build is thread safe. CMake package and pkg-config templates are in `cmake/` and
|
|||||||
`akerror.pc.in`. Tests are one-file C programs in `tests/`; shared test helpers
|
`akerror.pc.in`. Tests are one-file C programs in `tests/`; shared test helpers
|
||||||
live beside them, such as `tests/err_capture.h` and `tests/err_threads.h`.
|
live beside them, such as `tests/err_capture.h` and `tests/err_threads.h`.
|
||||||
|
|
||||||
Prose documentation lives in `docs/`, one file per topic — `architecture.md`,
|
|
||||||
`usage.md`, `status-codes.md`, `uncaught-errors.md`, `exit-status.md`,
|
|
||||||
`thread-safety.md`, `building.md`. `README.md` is deliberately short: summary,
|
|
||||||
installation, quickstart, and an index into `docs/`. Add new documentation to
|
|
||||||
the `docs/` file that owns the topic and link it from the README's index rather
|
|
||||||
than growing the README back.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
Use an out-of-tree build:
|
Use an out-of-tree build:
|
||||||
@@ -99,9 +92,9 @@ releases it on the single return path. Do not call consumer code
|
|||||||
owns the one mapping from an akerr status to an exit code: an exit status is a
|
owns the one mapping from an akerr status to an exit code: an exit status is a
|
||||||
byte, and every consumer status starts at 256, so passing a status through
|
byte, and every consumer status starts at 256, so passing a status through
|
||||||
`exit()` silently truncates it — status 256 exits 0 and reports success. The
|
`exit()` silently truncates it — status 256 exits 0 and reports success. The
|
||||||
only exits that bypass it are the two that have no status to map:
|
only exits that bypass it are the two that have no status to map, both in
|
||||||
`ENSURE_ERROR_READY`'s pool-exhaustion abort in `include/akerror.tmpl.h`, and the
|
`src/error.c`: `ENSURE_ERROR_READY`'s pool-exhaustion abort and the NULL-context
|
||||||
NULL-context case in `akerr_default_handler_unhandled_error()` in `src/error.c`. This rule applies to test
|
case in `akerr_default_handler_unhandled_error()`. This rule applies to test
|
||||||
programs too, except where the test's whole point is to observe the raw
|
programs too, except where the test's whole point is to observe the raw
|
||||||
truncation.
|
truncation.
|
||||||
|
|
||||||
@@ -133,31 +126,11 @@ anything that touches the pool, the registry, initialization, or the lock.
|
|||||||
Recent commits use short, imperative, sentence-case subjects, for example
|
Recent commits use short, imperative, sentence-case subjects, for example
|
||||||
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
|
`Fix refcount leak and stack-trace buffer overflow`. Keep commits focused and
|
||||||
describe the observable behavior changed. Pull requests should include a brief
|
describe the observable behavior changed. Pull requests should include a brief
|
||||||
summary, tests run, any compatibility impact for public macros, generated
|
summary, tests run, and any compatibility impact for public macros, generated
|
||||||
headers, installation paths, or CMake/pkg-config consumers, and a link to the
|
headers, installation paths, or CMake/pkg-config consumers.
|
||||||
issue they close.
|
|
||||||
|
|
||||||
## Agent-Specific Instructions
|
## Agent-Specific Instructions
|
||||||
|
|
||||||
**Outstanding work goes in the issue tracker, not in a file.** Open an issue at
|
|
||||||
<https://source.starfort.tech/andrew/libakerror/issues> — `tea issues create
|
|
||||||
--repo andrew/libakerror` — naming the file and line, the functional
|
|
||||||
consequence, and what closing it would touch. Label it by kind and blast radius
|
|
||||||
and leave `status::grooming` on it until its scope and approach are settled.
|
|
||||||
**Do not add outstanding items to `TODO.md`**: that file is the record of why
|
|
||||||
the handler ladder is a major-version change, why a copied `akerr_ErrorContext`
|
|
||||||
is a trap, why validating more inputs lowers branch coverage, and why the
|
|
||||||
mutation score is a floor. A description of work still to do goes stale the
|
|
||||||
moment somebody does it.
|
|
||||||
|
|
||||||
**This library's defects are most often found by its consumers, so make filing
|
|
||||||
them cheap.** Three defects in this library — `IGNORE()` leaking a context, the
|
|
||||||
un-namespaced `coverage` target, and the missing `akerrorConfigVersion.cmake` —
|
|
||||||
were written down in `libakstdlib`'s own notes and never here, so each was
|
|
||||||
worked around once per consumer and nobody saw the pattern. A consumer filing
|
|
||||||
upstream should cost them one issue instead of one workaround. `libakgl`,
|
|
||||||
`libakstdlib` and `akbasic` are all on the same forge.
|
|
||||||
|
|
||||||
Do not overwrite uncommitted user changes. Avoid editing generated files in
|
Do not overwrite uncommitted user changes. Avoid editing generated files in
|
||||||
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
|
`build/`; update `include/akerror.tmpl.h`, `src/error.c`, CMake files, tests,
|
||||||
or scripts instead.
|
or scripts instead.
|
||||||
|
|||||||
175
CMakeLists.txt
175
CMakeLists.txt
@@ -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
|
||||||
@@ -157,19 +107,6 @@ if(AKERR_SANITIZE AND NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
|||||||
"AKERR_SANITIZE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
|
"AKERR_SANITIZE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(AKERR_SANITIZE STREQUAL "thread" AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|
||||||
# ThreadSanitizer reserves a fixed, very large virtual-address range. Linux
|
|
||||||
# ASLR can put a loader mapping inside it before the runtime starts, which
|
|
||||||
# makes every test fail with "unexpected memory mapping" before main().
|
|
||||||
# Run the instrumented tests under the normal util-linux ASLR wrapper.
|
|
||||||
find_program(AKERR_SETARCH_EXECUTABLE setarch)
|
|
||||||
if(NOT AKERR_SETARCH_EXECUTABLE)
|
|
||||||
message(FATAL_ERROR
|
|
||||||
"AKERR_SANITIZE=thread on Linux requires setarch (util-linux) to "
|
|
||||||
"start tests with ASLR disabled.")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
function(akerr_instrument_for_sanitizers _target)
|
function(akerr_instrument_for_sanitizers _target)
|
||||||
if(AKERR_SANITIZE)
|
if(AKERR_SANITIZE)
|
||||||
target_compile_options(${_target} PRIVATE
|
target_compile_options(${_target} PRIVATE
|
||||||
@@ -204,36 +141,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 +168,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 +187,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 +214,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
|
||||||
@@ -362,36 +234,24 @@ if(AKERR_THREAD_SAFE)
|
|||||||
err_threads_init
|
err_threads_init
|
||||||
err_threads_pool
|
err_threads_pool
|
||||||
err_threads_registry
|
err_threads_registry
|
||||||
err_threads_handoff
|
|
||||||
)
|
)
|
||||||
endif()
|
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_init_failure)
|
|
||||||
else()
|
|
||||||
target_link_libraries(test_${_test} PRIVATE akerror)
|
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()
|
||||||
akerr_instrument_for_sanitizers(test_${_test})
|
akerr_instrument_for_sanitizers(test_${_test})
|
||||||
if(AKERR_SANITIZE STREQUAL "thread" AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
|
||||||
add_test(NAME ${_test}
|
|
||||||
COMMAND ${AKERR_SETARCH_EXECUTABLE} ${CMAKE_SYSTEM_PROCESSOR}
|
|
||||||
-R $<TARGET_FILE:test_${_test}>)
|
|
||||||
else()
|
|
||||||
add_test(NAME ${_test} COMMAND test_${_test})
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
endif()
|
|
||||||
# A sanitizer report is a test failure. Without halt_on_error the runtime
|
# A sanitizer report is a test failure. Without halt_on_error the runtime
|
||||||
# prints and continues, which leaves a race to be noticed in the log by
|
# prints and continues, which leaves a race to be noticed in the log by
|
||||||
# somebody reading it -- and under a race storm the reporting itself is slow
|
# somebody reading it -- and under a race storm the reporting itself is slow
|
||||||
@@ -402,14 +262,6 @@ foreach(_test IN LISTS AKERR_TESTS)
|
|||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
# HANDLE_GROUP deliberately enters the next case label. Keep that public macro
|
|
||||||
# compiling with the warning enabled, so a future macro edit cannot restore the
|
|
||||||
# warning for consumers which adopt -Wextra.
|
|
||||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
|
||||||
target_compile_options(test_err_handle_group PRIVATE
|
|
||||||
-Werror=implicit-fallthrough)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set_tests_properties(
|
set_tests_properties(
|
||||||
${AKERR_WILL_FAIL_TESTS}
|
${AKERR_WILL_FAIL_TESTS}
|
||||||
PROPERTIES WILL_FAIL TRUE
|
PROPERTIES WILL_FAIL TRUE
|
||||||
@@ -425,14 +277,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 +309,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 +333,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}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
658
README.md
658
README.md
@@ -4,6 +4,21 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
2.0.1 fixes an unhandled error killing the process and still reporting success:
|
||||||
|
the exit code was the status truncated to a byte, and every consumer status
|
||||||
|
starts at 256. Use `akerr_exit()` instead of `exit()` — see
|
||||||
|
[Exit status](#exit-status). No ABI break.
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
|
||||||
|
the capacity limits and how to raise them, and the thread-safety rules.
|
||||||
|
|
||||||
|
|
||||||
# Why?
|
# Why?
|
||||||
|
|
||||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||||
@@ -12,6 +27,10 @@ Instead, this library implements a pragmatic and stylistic choice to assist the
|
|||||||
|
|
||||||
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
|
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
|
||||||
|
|
||||||
|
# Library Architecture
|
||||||
|
|
||||||
|
## Philosophy of Use
|
||||||
|
|
||||||
This library has 6 guiding principles:
|
This library has 6 guiding principles:
|
||||||
|
|
||||||
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
||||||
@@ -21,19 +40,171 @@ This library has 6 guiding principles:
|
|||||||
* Manipulating the call stack directly is error prone and dangerous
|
* Manipulating the call stack directly is error prone and dangerous
|
||||||
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
||||||
|
|
||||||
# Documentation
|
## Lifecycle of an error in the AKError library
|
||||||
|
|
||||||
| Document | What it answers |
|
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
|
||||||
| -------- | --------------- |
|
|
||||||
| [docs/architecture.md](docs/architecture.md) | What an error context is, how one travels up the call stack, and what the macros build |
|
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
|
||||||
| [docs/usage.md](docs/usage.md) | The macro reference: `ATTEMPT`/`CLEANUP`/`PROCESS`/`FINISH`, `CATCH`, `FAIL_*`, `PASS`, `HANDLE`, `SUCCEED_RETURN` |
|
2. The akerr_ErrorContext is returned from the scope where the error was detected
|
||||||
| [docs/status-codes.md](docs/status-codes.md) | Defining your own status codes, and reserving a range so two libraries cannot collide |
|
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
|
||||||
| [docs/uncaught-errors.md](docs/uncaught-errors.md) | `AKERR_NOIGNORE`, what a stack trace looks like, and how to read one |
|
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
|
||||||
| [docs/exit-status.md](docs/exit-status.md) | Why you call `akerr_exit()` and never `exit()`, and how to replace the unhandled-error handler |
|
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
|
||||||
| [docs/thread-safety.md](docs/thread-safety.md) | What thread safety here covers, what it does not, and how to hand an error to another thread |
|
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
|
||||||
| [docs/building.md](docs/building.md) | Configure options, the generated header, and building without stdlib |
|
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
|
||||||
| [UPGRADING.md](UPGRADING.md) | What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate |
|
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
|
||||||
| [TODO.md](TODO.md) | The reasoning behind decisions and measurements. **Outstanding work is in [the issue tracker](https://source.starfort.tech/andrew/libakerror/issues).** |
|
|
||||||
|
## What is in an Error Context
|
||||||
|
|
||||||
|
The Error Context object is a simple object which contains a few things:
|
||||||
|
|
||||||
|
* A numeric error code
|
||||||
|
* The name of the file in which the error occurred
|
||||||
|
* The name of the function in which the error occurred
|
||||||
|
* The line number in the file at which the error occurred
|
||||||
|
* A character buffer containing a message about the error in question
|
||||||
|
|
||||||
|
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.h](include/akerror.h) for more details.
|
||||||
|
|
||||||
|
## What are the control structures
|
||||||
|
|
||||||
|
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
|
||||||
|
|
||||||
|
## Functions and Return Codes
|
||||||
|
|
||||||
|
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
|
||||||
|
|
||||||
|
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
|
||||||
|
|
||||||
|
## Error codes
|
||||||
|
|
||||||
|
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
||||||
|
|
||||||
|
You can define additional error types as integer constants. Values 0 through 255
|
||||||
|
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
||||||
|
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
||||||
|
names are stored sparsely, so any `int` is a legal status and no compile
|
||||||
|
definition is needed to use large values. Note that no consumer status can be a
|
||||||
|
process exit code — see [Exit status](#exit-status).
|
||||||
|
|
||||||
|
Every library that may coexist in one process must reserve its range during
|
||||||
|
initialization. `akerr_reserve_status_range()` and
|
||||||
|
`akerr_register_status_name()` report failure the way everything else in this
|
||||||
|
library does — they return `akerr_ErrorContext *`, and they are marked
|
||||||
|
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
||||||
|
`PASS` up out of your initialization:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define MYLIB_OWNER "my-library"
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
/* Another component owning part of the range propagates to our caller. */
|
||||||
|
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||||
|
|
||||||
|
/* Then name each code, quoting the owner you reserved with. */
|
||||||
|
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
||||||
|
"Some Error Code Description"));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reservations are process-local, fixed-capacity, and idempotent when the same
|
||||||
|
owner repeats the exact same range. They preserve compile-time integer constants
|
||||||
|
so `HANDLE` still works, since `case` labels require them.
|
||||||
|
|
||||||
|
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
||||||
|
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
||||||
|
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
||||||
|
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
||||||
|
functions raise, the capacity limits and how to raise them, and thread-safety
|
||||||
|
rules.
|
||||||
|
|
||||||
|
|
||||||
|
# Thread safety
|
||||||
|
|
||||||
|
The library is thread safe as built by default. Every entry point may be called
|
||||||
|
from any thread at any time, including the first one: `akerr_init()` runs
|
||||||
|
exactly once no matter how many threads race into it.
|
||||||
|
|
||||||
|
What that covers:
|
||||||
|
|
||||||
|
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
|
||||||
|
reference is one operation under a lock, so two threads can never be handed
|
||||||
|
the same context. A context is then owned by the thread that raised it, all
|
||||||
|
the way through `CATCH`, `HANDLE`, and release.
|
||||||
|
* **The status registry.** Reservations and name registrations are serialized
|
||||||
|
against each other and against lookups. Two threads reserving the same range
|
||||||
|
cannot both win — exactly one gets `NULL` and the other gets
|
||||||
|
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
||||||
|
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored`) and
|
||||||
|
the last-ditch context used to report `akerr_release_error(NULL)` are
|
||||||
|
thread-local, so one thread's ignored error is never another's.
|
||||||
|
|
||||||
|
What it does not cover, and cannot:
|
||||||
|
|
||||||
|
* **Sharing one error context between threads.** The library hands a context to
|
||||||
|
one thread; passing it to another is your synchronization to do.
|
||||||
|
* **`akerr_log_method` and `akerr_handler_unhandled_error`.** Set them during
|
||||||
|
startup, before you spawn threads. They are read on every error and the
|
||||||
|
library never writes them after initialization, so setting one while other
|
||||||
|
threads are raising errors is a race the library cannot mediate.
|
||||||
|
* **Renaming a status that other threads are looking up.**
|
||||||
|
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
|
||||||
|
valid for the life of the process; registering a *second* name for the same
|
||||||
|
status overwrites that buffer in place. Register names during initialization.
|
||||||
|
Registering a *new* status concurrently is fine.
|
||||||
|
* **Which unhandled error terminates the process.** An error that reaches
|
||||||
|
`FINISH_NORETURN` unhandled prints its stack trace and calls
|
||||||
|
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
|
||||||
|
thread's trace is whole — the buffer belongs to its context, and each line is
|
||||||
|
one call to `akerr_log_method` — but if two threads get there at the same
|
||||||
|
instant, both traces print and the exit status is whichever one won.
|
||||||
|
|
||||||
|
There is one lock, it is recursive, and it covers both the pool and the
|
||||||
|
registry. That means error construction is serialized across threads: raising an
|
||||||
|
error is the exceptional path, and correctness there is worth more than
|
||||||
|
throughput. A program that raises errors on its hot path will feel it.
|
||||||
|
|
||||||
|
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
|
||||||
|
consumer can check what it linked against:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#if AKERR_THREAD_SAFE
|
||||||
|
/* ... start worker threads ... */
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building single threaded
|
||||||
|
|
||||||
|
The threading backend is chosen when libakerror is configured. `auto` (the
|
||||||
|
default) takes POSIX threads, and **fails the configure** if it cannot find
|
||||||
|
them rather than quietly producing a library that says it is thread safe and is
|
||||||
|
not. To mean it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -DAKERR_THREADS=none
|
||||||
|
```
|
||||||
|
|
||||||
|
That builds with no locking and no thread-local storage, stamps
|
||||||
|
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
|
||||||
|
one thread is then undefined.
|
||||||
|
|
||||||
|
## Proving it
|
||||||
|
|
||||||
|
The thread tests (`tests/err_threads_*.c`) assert the properties above directly:
|
||||||
|
exclusive ownership of pool slots, exactly one winner for a contested range,
|
||||||
|
every registered name readable back. They run in the normal suite. The run that
|
||||||
|
proves the *absence* of a data race underneath them is ThreadSanitizer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/thread_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
|
||||||
|
and every test with it, and runs the suite. Under that build a sanitizer report
|
||||||
|
fails the test rather than being printed and passed over.
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@@ -43,31 +214,39 @@ cmake --build build
|
|||||||
cmake --install build
|
cmake --install build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local pre-push checks
|
## Templating and autogenerated code
|
||||||
|
|
||||||
Enable the repository's local pre-push checks once in each clone:
|
The build process relies upon `scripts/generrno.sh` which performs the following:
|
||||||
|
|
||||||
```bash
|
1. Executes `errno --list` and gathers up the output
|
||||||
git config core.hooksPath .githooks
|
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`
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This library depends upon `stdlib`, and upon POSIX threads unless it is built
|
||||||
|
with `-DAKERR_THREADS=none` (see "Thread safety" above). 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:
|
||||||
|
|
||||||
|
- `memset` function
|
||||||
|
- `strncpy` function
|
||||||
|
- `strlen` function
|
||||||
|
- `strcmp` function
|
||||||
|
- `sprintf` function
|
||||||
|
- `exit` function
|
||||||
|
- `bool` type
|
||||||
|
- `NULL` type
|
||||||
|
- `INT_MAX` constant
|
||||||
|
- `PATH_MAX` constant
|
||||||
|
|
||||||
|
... then you can compile it thusly:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
|
||||||
|
cmake --build build
|
||||||
|
cmake --install build
|
||||||
```
|
```
|
||||||
|
|
||||||
The hook runs cppcheck when `scripts/cppcheck.sh` and cppcheck are available,
|
# Using the library
|
||||||
then the default build and tests, followed by the `AKERR_USE_STDLIB=OFF`
|
|
||||||
build and tests. Builds are kept under `.git/akerr-prepush` and do not disturb
|
|
||||||
the normal `build/` directory. Mutation testing is opt-in because it is slow:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AKERR_HOOK_MUTATION=1 git push
|
|
||||||
```
|
|
||||||
|
|
||||||
CI remains the hard gate. For an intentional local escape, use Git's standard
|
|
||||||
`git push --no-verify` option.
|
|
||||||
|
|
||||||
The library depends on `stdlib` and on POSIX threads. Both are optional at the
|
|
||||||
cost of some functionality — see [docs/building.md](docs/building.md) for
|
|
||||||
`-DAKERR_USE_STDLIB=OFF` and
|
|
||||||
[docs/thread-safety.md](docs/thread-safety.md#building-single-threaded) for
|
|
||||||
`-DAKERR_THREADS=none`.
|
|
||||||
|
|
||||||
## Setting up your project
|
## Setting up your project
|
||||||
|
|
||||||
@@ -106,93 +285,368 @@ add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
|||||||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
||||||
```
|
```
|
||||||
|
|
||||||
# Quickstart
|
|
||||||
|
|
||||||
A function that can fail returns an `akerr_ErrorContext *` instead of a value,
|
## (Optional) Configuring the logging function
|
||||||
and moves its real output to a pointer parameter. `AKERR_NOIGNORE` makes the
|
|
||||||
compiler complain if a caller throws that return away.
|
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
||||||
|
|
||||||
|
```
|
||||||
|
void my_logger(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
/* ... do something */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* set your custom error handler */
|
||||||
|
akerr_log_method = &my_logger;
|
||||||
|
|
||||||
|
/* proceed to use the library */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up the Error Context
|
||||||
|
|
||||||
|
Before you can use any of these macros you must set up an error context inside of the current scope.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include <akerror.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
/* Fails with a message; the caller finds out what and where. */
|
|
||||||
static akerr_ErrorContext AKERR_NOIGNORE *open_config(const char *path, FILE **dest)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
```
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER,
|
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
||||||
"no config path was given");
|
|
||||||
|
|
||||||
*dest = fopen(path, "r");
|
## Attempting an Operation
|
||||||
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_IO,
|
|
||||||
"could not open %s", path);
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
```c
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
|
||||||
{
|
|
||||||
FILE *config = NULL;
|
|
||||||
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE,
|
// ... code
|
||||||
"usage: %s <config>", argv[0]);
|
|
||||||
CATCH(errctx, open_config(argv[1], &config));
|
|
||||||
/* ... read the config ... */
|
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
/* Runs whether or not anything failed. */
|
|
||||||
if ( config != NULL ) {
|
|
||||||
fclose(config);
|
|
||||||
}
|
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} HANDLE(errctx, AKERR_VALUE) {
|
} FINISH(errctx, true)
|
||||||
/* A usage error is ours to handle, so handle it and carry on. */
|
```
|
||||||
} FINISH_NORETURN(errctx);
|
|
||||||
|
|
||||||
return 0;
|
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
||||||
|
|
||||||
|
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
||||||
|
|
||||||
|
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
||||||
|
|
||||||
|
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORExbTURN(errctx)` instead.
|
||||||
|
|
||||||
|
|
||||||
|
# Capturing errors
|
||||||
|
|
||||||
|
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
||||||
|
|
||||||
|
## Capturing errors from functions which return akerr_ErrorContext *
|
||||||
|
|
||||||
|
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, errorGeneratingFunction())
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||||
|
|
||||||
|
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
||||||
|
|
||||||
|
## Setting errors from functions or expressions returning integer
|
||||||
|
|
||||||
|
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
||||||
|
|
||||||
|
Here is an example of checking for a NULL pointer
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example of checking for two strings that are not equal
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||||
|
|
||||||
|
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
||||||
|
|
||||||
|
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
for ( int i = 0; i < n; i++ ) {
|
||||||
|
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
||||||
|
}
|
||||||
|
// ... this code still executes, with errctx already in an error state ...
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
||||||
|
|
||||||
|
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
for ( int i = 0; i < n; i++ ) {
|
||||||
|
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ATTEMPT` is where work that can fail goes. `CLEANUP` always runs. `PROCESS`
|
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
||||||
opens the handler section, and each `HANDLE` claims one status. Anything no
|
|
||||||
`HANDLE` claims is still an error when it reaches `FINISH`: inside a function,
|
|
||||||
`FINISH(errctx, true)` returns it to your caller; at the top, as above,
|
|
||||||
`FINISH_NORETURN(errctx)` prints the stack trace and ends the process. You do
|
|
||||||
not need to call `akerr_init()` — every entry point does it for you.
|
|
||||||
|
|
||||||
Three things worth knowing before you write much more than that:
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
||||||
|
} CLEANUP {
|
||||||
|
// ... always runs ...
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_VALUE) {
|
||||||
|
// ... handle a failure from any iteration ...
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
```
|
||||||
|
|
||||||
* [The full macro reference](docs/usage.md), including `PASS` for errors you
|
# Passing errors
|
||||||
cannot do anything about, and `HANDLE_GROUP` for several statuses that fail
|
|
||||||
the same way.
|
|
||||||
* [Never use `CATCH` or a `FAIL_*_BREAK` macro inside a loop](docs/usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
|
|
||||||
They leave the `ATTEMPT` with a C `break`, which only escapes the innermost
|
|
||||||
loop. This is the first thing that bites people.
|
|
||||||
* [Never call `exit()` with a status. Call `akerr_exit()`](docs/exit-status.md).
|
|
||||||
An exit status is a byte and every consumer status starts at 256, so `exit()`
|
|
||||||
truncates status 256 to 0 and reports success.
|
|
||||||
|
|
||||||
# Thread safety
|
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
||||||
|
|
||||||
The library is thread safe as built by default: every entry point may be called
|
```
|
||||||
from any thread at any time, and an error context may be handed from one thread
|
PREPARE_ERROR(e);
|
||||||
to another and released there. There is one recursive lock covering the pool and
|
PASS(e, some_method_that_may_fail());
|
||||||
the registry, so error construction is serialized — a program that raises errors
|
SUCCEED_RETURN(e);
|
||||||
on its hot path will feel it. See [docs/thread-safety.md](docs/thread-safety.md)
|
```
|
||||||
for what that covers, what it cannot, and the handoff pattern.
|
|
||||||
|
|
||||||
# Upgrading
|
This does the same thing as this, but with less code:
|
||||||
|
|
||||||
2.0.1 fixes an unhandled error killing the process and still reporting success:
|
```
|
||||||
the exit code was the status truncated to a byte, and every consumer status
|
PREPARE_ERROR(e);
|
||||||
starts at 256. Use `akerr_exit()` instead of `exit()` — see
|
ATTEMPT {
|
||||||
[docs/exit-status.md](docs/exit-status.md). No ABI break.
|
CATCH(e, some_method_that_may_fail());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
```
|
||||||
|
|
||||||
|
# Handling errors
|
||||||
|
|
||||||
|
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
||||||
|
|
||||||
|
## Handling a specific error with HANDLE
|
||||||
|
|
||||||
|
In order to handle a specific error code, use the `HANDLE` macro.
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
|
// Something is complaining about a null pointer error. Do something about it.
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling a group of errors with HANDLE_GROUP
|
||||||
|
|
||||||
|
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_IO) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||||||
|
// error handling code goes here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
||||||
|
|
||||||
|
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_IO) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||||||
|
// This code handles 3 error cases
|
||||||
|
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||||||
|
// This code handles 1 error case
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Returning success or failure from functions returning akerr_ErrorContext *
|
||||||
|
|
||||||
|
If at all possible, when using this library, your functiions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
||||||
|
|
||||||
|
## SUCCEED_RETURN
|
||||||
|
|
||||||
|
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
ATTEMPT {
|
||||||
|
// ... stuff
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAIL_RETURN
|
||||||
|
|
||||||
|
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
||||||
|
|
||||||
|
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditionally failing and returning
|
||||||
|
|
||||||
|
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Uncaught errors
|
||||||
|
|
||||||
|
## Misbehaving methods
|
||||||
|
|
||||||
|
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
|
||||||
|
|
||||||
|
## Ensuring that all error codes are captured
|
||||||
|
|
||||||
|
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
|
||||||
|
|
||||||
|
Beware that `AKERROR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack Traces
|
||||||
|
|
||||||
|
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
|
||||||
|
|
||||||
|
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
|
||||||
|
tests/err_trace.c:func2:10
|
||||||
|
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
|
||||||
|
tests/err_trace.c:func1:18
|
||||||
|
tests/err_trace.c:func1:21
|
||||||
|
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
|
||||||
|
tests/err_trace.c:main:30
|
||||||
|
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
|
||||||
|
```
|
||||||
|
|
||||||
|
From bottom to top, we have:
|
||||||
|
|
||||||
|
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
||||||
|
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
||||||
|
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||||||
|
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
|
||||||
|
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
|
||||||
|
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||||||
|
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
||||||
|
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
||||||
|
|
||||||
|
## Exit status
|
||||||
|
|
||||||
|
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
void akerr_exit(int status);
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies everywhere you are leaving the process on account of a status, not
|
||||||
|
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
||||||
|
initialization routine that cannot continue, a `main()` that ends by reporting
|
||||||
|
the status it finished with. One function owns the mapping so that a given
|
||||||
|
status produces the same exit code no matter which of your exits it left by.
|
||||||
|
|
||||||
|
The mapping exists because a process exit status is one byte wide. `exit()`
|
||||||
|
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
||||||
|
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
||||||
|
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
||||||
|
truncated value because the truncation happened before the parent looked. There
|
||||||
|
is no wider `exit()` to reach for.
|
||||||
|
|
||||||
|
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
||||||
|
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
||||||
|
status can be an exit code:
|
||||||
|
|
||||||
|
```
|
||||||
|
status exit code
|
||||||
|
0 0 (success)
|
||||||
|
1 .. 255 the status
|
||||||
|
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing the low byte instead would have made status 256 exit 0 and report
|
||||||
|
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
||||||
|
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
||||||
|
belong to the shell.
|
||||||
|
|
||||||
|
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
||||||
|
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
||||||
|
0`, which marks a zero status handled, so a successful context cannot reach
|
||||||
|
`FINISH_NORETURN`'s call to the handler at all.
|
||||||
|
|
||||||
|
Above 255, the exit code tells you the process died of an error, not which one.
|
||||||
|
125 is inside the library's reserved band and so is also some host's `errno`, and
|
||||||
|
every status above 255 collapses onto it. **The stack trace is what identifies
|
||||||
|
the error** — it is printed before the handler runs, carrying the status at full
|
||||||
|
width along with its registered name.
|
||||||
|
|
||||||
|
### Replacing the handler
|
||||||
|
|
||||||
|
After the trace is printed, `FINISH_NORETURN` calls
|
||||||
|
`akerr_handler_unhandled_error`. The default implementation,
|
||||||
|
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
||||||
|
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
||||||
|
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
||||||
|
`akerr_exit()` so the exit code still means what it means everywhere else:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void mylib_handler(akerr_ErrorContext *e)
|
||||||
|
{
|
||||||
|
mylib_flush_telemetry();
|
||||||
|
if ( e == NULL ) {
|
||||||
|
akerr_exit(AKERR_API);
|
||||||
|
}
|
||||||
|
akerr_exit(e->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_handler_unhandled_error = &mylib_handler;
|
||||||
|
```
|
||||||
|
|
||||||
|
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
||||||
|
ending in one of those calls is complete rather than falling off the end.
|
||||||
|
|
||||||
|
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
||||||
|
installs one that does not exit at all, which is how the test suite asserts on
|
||||||
|
unhandled errors without dying.
|
||||||
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
|
|
||||||
the capacity limits and how to raise them, and the thread-safety rules.
|
|
||||||
|
|||||||
191
TODO.md
191
TODO.md
@@ -1,103 +1,122 @@
|
|||||||
# Record
|
# TODO
|
||||||
|
|
||||||
**Outstanding work is in the issue tracker, not in this file:**
|
Working notes for `libakerror`. Outstanding items only.
|
||||||
<https://source.starfort.tech/andrew/libakerror/issues>
|
|
||||||
|
|
||||||
Issues are labelled by kind and blast radius, and milestoned by what they can land
|
## 1. Only ThreadSanitizer is wired into CI, not ASan/UBSan
|
||||||
in: `2.0.x` for anything that breaks no ABI, `2.1.0` for additive surface, `3.0.0`
|
|
||||||
for the handler-ladder rewrite and the contract changes that go with it. Everything
|
|
||||||
filed carries `status::grooming` until it has been through grooming.
|
|
||||||
|
|
||||||
What stays here is the reasoning a tracker has no place for.
|
`AKERR_SANITIZE` builds the library and the tests with any sanitizer list, and
|
||||||
|
CI runs `-DAKERR_SANITIZE=thread` through `scripts/thread_test.sh`. Nothing runs
|
||||||
|
`address,undefined` yet, and that is the one that covers the original
|
||||||
|
motivation: mutation testing caught an out-of-bounds probe in the status-name
|
||||||
|
hash table that the suite could not, because the failure mode was a write into
|
||||||
|
adjacent BSS, which does not crash. Sharpening one test closed that instance;
|
||||||
|
ASan would catch the whole class regardless of how sharp the assertions are.
|
||||||
|
|
||||||
## Three defects were recorded only downstream
|
The machinery is in place — this is one more job in
|
||||||
|
`.gitea/workflows/ci.yaml` running
|
||||||
|
`cmake -S . -B build/asan -DAKERR_SANITIZE=address,undefined`. Left separate
|
||||||
|
because ASan and TSan cannot be combined in one build.
|
||||||
|
|
||||||
Found while moving this file and `libakstdlib`'s into their trackers. Each is a
|
## 2. `HANDLE`-level status aliasing is still undetectable
|
||||||
defect **in this library**, each was written down in a *consumer's* TODO file, and
|
|
||||||
none had an entry here:
|
|
||||||
|
|
||||||
| Issue | What it is | What it costs downstream |
|
Two components can compile the same integer into a `case` label without ever
|
||||||
|---|---|---|
|
reserving a range or registering a name, and nothing sees it. Ownership
|
||||||
| #14 | `IGNORE()` logs a context and never releases it | `libakstdlib`'s `aksl_tree_iterate` open-codes log-then-release by hand; every `IGNORE()` in `libakgl`'s `CLEANUP` blocks is a leaked slot |
|
enforcement covers *naming*, which is the part the library mediates; the `case`
|
||||||
| #15 | The `coverage` target is not namespaced when embedded, though `mutation` is | `libakstdlib` shadows `add_custom_target` to embed this library at all |
|
label never reaches it.
|
||||||
| #16 | No `akerrorConfigVersion.cmake` is installed | No consumer can ask `find_dependency(akerror)` for a version floor |
|
|
||||||
|
|
||||||
**That is the finding worth keeping, more than the three defects.** A library whose
|
Closing this needs the `if`/`else if` handler ladder — rewriting
|
||||||
consumers record its defects in their own files has no way to see them: each
|
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` so status matching
|
||||||
consumer knows one, nobody sees the pattern, and the workaround gets written three
|
is not restricted to integer constant expressions. That would also allow
|
||||||
times. The tracker is now the place, and a consumer filing upstream costs them one
|
matching on ranges or predicates, and would let a handler resolve a code through
|
||||||
issue instead of one workaround.
|
its owner. It touches the most load-bearing code in the library and every
|
||||||
|
consumer's error handling at once, so it wants its own change.
|
||||||
|
|
||||||
## Why the handler ladder is a `3.0.0` change and not a patch
|
Note it would *not* by itself fix the "don't use `CATCH` or `FAIL_*_BREAK`
|
||||||
|
inside a loop" hazard: that comes from exiting via `break`, not from `switch`.
|
||||||
|
|
||||||
`PROCESS`/`HANDLE`/`HANDLE_GROUP`/`HANDLE_DEFAULT`/`FINISH` compile to a `switch`,
|
## 3. No registry introspection
|
||||||
which restricts status matching to integer constant expressions. That is what makes
|
|
||||||
`HANDLE`-level aliasing undetectable (#4): two components can compile the same
|
|
||||||
integer into a `case` label without reserving a range or registering a name, and
|
|
||||||
**ownership enforcement never sees it, because it covers naming and the `case` label
|
|
||||||
never reaches the library.**
|
|
||||||
|
|
||||||
Rewriting it as an `if`/`else if` ladder would allow matching on ranges or
|
There is no way to ask who owns a status, or to enumerate reservations. The
|
||||||
predicates and let a handler resolve a code through its owner. It also touches **the
|
"coordinate ranges at the dependency-stack level" advice in UPGRADING.md
|
||||||
most load-bearing code in the library and every consumer's error handling at once.**
|
therefore has no tooling behind it.
|
||||||
|
|
||||||
One thing it would *not* fix, recorded so nobody expects it to: the "don't use
|
A read-only accessor plus a dump through `akerr_log_method` would let a startup
|
||||||
`CATCH` or `FAIL_*_BREAK` inside a loop" hazard comes from exiting via `break`, not
|
self-check or a CI job print the whole map for a linked stack. Cheap, additive,
|
||||||
from `switch`.
|
and the natural next step for multi-component adoption.
|
||||||
|
|
||||||
## Why a copied `akerr_ErrorContext` is a trap
|
## 4. No way to release a reservation
|
||||||
|
|
||||||
Recorded because the struct looks copyable and is not, and #11 exists only if a
|
A plugin host that `dlopen`s many distinct plugins over a process lifetime
|
||||||
consumer ever needs it.
|
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
||||||
|
an identical repeat by the same owner is idempotent.
|
||||||
|
|
||||||
- **`stacktracebufptr` is self-referential.** `akerr_ErrorContext c = *src;` leaves
|
## 5. Renaming a status is not safe against a concurrent lookup
|
||||||
the copy's cursor pointing into the source's buffer, so the copy logs correctly
|
|
||||||
and then **corrupts a slot it does not own** the first time anything appends.
|
|
||||||
- **`arrayid` is restored after the wipe** in `akerr_release_error()`
|
|
||||||
(`src/error.c:395-398`), so a copied id makes the destination **impersonate the
|
|
||||||
source's slot for the life of the process.**
|
|
||||||
|
|
||||||
If it is ever built, the shape takes the destination as a parameter rather than
|
|
||||||
allocating it: an allocating copy could fail on pool exhaustion, and **reporting
|
|
||||||
that failure needs a pool slot**, so it would have to abort — a third `exit()` site
|
|
||||||
in a library that deliberately has two.
|
|
||||||
|
|
||||||
## Why validating more inputs lowers branch coverage
|
|
||||||
|
|
||||||
Branch coverage on `src/error.c` sits just above its 50% gate, and the gate is set
|
|
||||||
where it is on purpose.
|
|
||||||
|
|
||||||
Every `FAIL_*` site carries about **six** branch outcomes of error-construction
|
|
||||||
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when that
|
|
||||||
specific failure fires. **Every `PASS` site around a call that cannot fail carries
|
|
||||||
about twenty-five.**
|
|
||||||
|
|
||||||
So adding a defensive check lowers the ratio by construction. **Before adding one,
|
|
||||||
expect to add a test that drives it**, as `tests/err_copy_string.c` does.
|
|
||||||
|
|
||||||
## Why the mutation score is a floor
|
|
||||||
|
|
||||||
`scripts/mutation_test.py` configures each mutant with the default CMake options,
|
|
||||||
so **a mutant that only breaks under concurrency is judged by a suite running
|
|
||||||
without ThreadSanitizer.**
|
|
||||||
|
|
||||||
Measured: deleting the pool's `akerr_mutex_lock()` call survives the run, **even
|
|
||||||
though it is a real race.** Rebuilt and run directly, that mutant fails
|
|
||||||
`tests/err_threads_pool.c` in 4 of 10 runs, and fails under `scripts/thread_test.sh`
|
|
||||||
in 5 of 5.
|
|
||||||
|
|
||||||
**81.2% is therefore a floor for that category, not a verdict.** #10 is the
|
|
||||||
passthrough that would fix the measurement, and it belongs behind a flag: a
|
|
||||||
TSan-instrumented suite per mutant costs roughly 6s instead of 0.4s.
|
|
||||||
|
|
||||||
## Why a registered name is returned by pointer
|
|
||||||
|
|
||||||
`akerr_name_for_status(status, NULL)` returns a pointer into the registry rather
|
`akerr_name_for_status(status, NULL)` returns a pointer into the registry rather
|
||||||
than a copy, **which is what makes it usable from inside `FAIL`** — it needs no
|
than a copy, which is what makes it usable from inside `FAIL` — it needs no
|
||||||
buffer and no error context of its own.
|
buffer and no error context of its own. Registering a *second* name for a status
|
||||||
|
that already has one (`tests/err_name_ownership.c` covers that it is allowed)
|
||||||
|
overwrites that buffer in place, so a thread reading the name at that moment can
|
||||||
|
see a torn string. Every other registry operation is serialized; this one cannot
|
||||||
|
be, because the reader is outside the lock by the time it reads the characters.
|
||||||
|
|
||||||
The cost is that renaming a status is not safe against a concurrent lookup (#7):
|
Documented in README.md and UPGRADING.md as "register names during
|
||||||
every other registry operation is serialized, and this one cannot be, because **the
|
initialization". Closing it properly means making a registered name immutable —
|
||||||
reader is outside the lock by the time it reads the characters.** Documented in
|
either refusing a rename outright (a behavior change, and
|
||||||
`docs/thread-safety.md` and `UPGRADING.md` as "register names during
|
`tests/err_name_ownership.c` asserts the current contract), or copying names
|
||||||
initialization".
|
into a bump-allocated arena and publishing the pointer with a release store, so
|
||||||
|
a rename allocates new storage instead of rewriting live storage. The arena is
|
||||||
|
the better answer; it costs a second capacity limit and its exhaustion path.
|
||||||
|
|
||||||
|
## 6. Deprecate the two-argument name-registration path
|
||||||
|
|
||||||
|
`akerr_name_for_status(status, name)` cannot identify its caller, so it can only
|
||||||
|
check that *some* reservation covers the status, not that the caller owns it. It
|
||||||
|
exists for migration. Once consumers have moved to
|
||||||
|
`akerr_register_status_name()`, make the set path a no-op or remove it and leave
|
||||||
|
`akerr_name_for_status()` as pure lookup.
|
||||||
|
|
||||||
|
## 7. `akerr_init()`'s own reservation failure is untested
|
||||||
|
|
||||||
|
`tests/err_library_status_fatal.c` covers the terminal path in
|
||||||
|
`__akerr_name_library_status()` by naming a status the library does not own. The
|
||||||
|
band reservation in `akerr_init()` has no such handle: it can only fail in a
|
||||||
|
build whose tables are too small for the library's own entries, and both sizes
|
||||||
|
are `PRIVATE` to the library target, so a test executable cannot set them.
|
||||||
|
|
||||||
|
Closing it means a second library target built with tiny tables plus a
|
||||||
|
`WILL_FAIL` test linked against it. Nothing in the CMake does that yet: the
|
||||||
|
sanitizer and coverage options vary the *flags* of the one library target, not
|
||||||
|
its compile definitions.
|
||||||
|
|
||||||
|
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
||||||
|
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
||||||
|
machinery (`ENSURE_ERROR_READY`, `AKERR_STACKTRACE_APPEND`) that only run when
|
||||||
|
that specific failure fires, and every `PASS` site around a call that cannot
|
||||||
|
fail carries about twenty-five. Validating more inputs therefore lowers the
|
||||||
|
ratio by construction. Before adding defensive checks, expect to add a test that
|
||||||
|
drives them, as `tests/err_copy_string.c` does.
|
||||||
|
|
||||||
|
## 8. Mutation testing judges concurrency mutants without a sanitizer
|
||||||
|
|
||||||
|
`scripts/mutation_test.py` configures each mutant build with the default CMake
|
||||||
|
options, so a mutant that only breaks under concurrency is judged by a suite
|
||||||
|
running without ThreadSanitizer. Deleting the pool's `akerr_mutex_lock()` call
|
||||||
|
survives the run even though it is a real race: rebuilt and run directly, that
|
||||||
|
mutant fails `tests/err_threads_pool.c` in 4 of 10 runs, and fails under
|
||||||
|
`scripts/thread_test.sh` in 5 of 5. So 81.2% is a floor for that category, not a
|
||||||
|
verdict.
|
||||||
|
|
||||||
|
Closing it means a `--cmake-arg` passthrough on the harness so the mutant build
|
||||||
|
can be configured with `-DAKERR_SANITIZE=thread`. The whole run then costs a
|
||||||
|
TSan-instrumented suite per mutant (roughly 6s instead of 0.4s), so it belongs
|
||||||
|
behind a flag rather than in the default target or in CI.
|
||||||
|
|
||||||
|
## Unrelated pre-existing issues
|
||||||
|
|
||||||
|
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||||
|
and `NULL` are used unconditionally but only included under the stdlib branch.
|
||||||
|
The README's dependency list states what a replacement must provide, but the
|
||||||
|
header still needs its includes untangled for that configuration to work.
|
||||||
|
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
||||||
|
defined and never read. Dead line.
|
||||||
|
|||||||
66
UPGRADING.md
66
UPGRADING.md
@@ -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.
|
||||||
@@ -68,8 +33,8 @@ top-level `HANDLE` block, an init routine that cannot continue — so one mappin
|
|||||||
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
|
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
|
||||||
consumer status out of `$?`, you were never getting it: read the stack trace,
|
consumer status out of `$?`, you were never getting it: read the stack trace,
|
||||||
which carries the status at full width along with its registered name, or
|
which carries the status at full width along with its registered name, or
|
||||||
install a handler that maps your own statuses into a byte. See
|
install a handler that maps your own statuses into a byte. See "Exit status" in
|
||||||
[docs/exit-status.md](docs/exit-status.md).
|
[README.md](README.md).
|
||||||
|
|
||||||
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
|
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
|
||||||
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
|
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
|
||||||
@@ -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
|
||||||
@@ -130,21 +95,11 @@ Safe from any thread, with no coordination on your part:
|
|||||||
* `akerr_name_for_status(status, NULL)` lookups, concurrently with each other
|
* `akerr_name_for_status(status, NULL)` lookups, concurrently with each other
|
||||||
and with registrations of *other* statuses.
|
and with registrations of *other* statuses.
|
||||||
* `akerr_init()`, from any number of threads at once.
|
* `akerr_init()`, from any number of threads at once.
|
||||||
* **Handing a context to another thread, and releasing it there.** The reference
|
|
||||||
count is the only field the library reads across threads and it is only ever
|
|
||||||
touched under the pool lock, so `akerr_release_error()` does not care which
|
|
||||||
thread checked the slot out. Contexts live in process-global storage, not
|
|
||||||
thread-local, so one outlives the thread that raised it. What is still yours is
|
|
||||||
the handoff *itself*: it has to carry a happens-before edge, which any mutex,
|
|
||||||
condvar, `pthread_join` or acquire/release atomic gives you.
|
|
||||||
|
|
||||||
Still yours to coordinate:
|
Still yours to coordinate:
|
||||||
|
|
||||||
* **Two threads in one context at once.** Ownership moves; it does not fork.
|
* **One error context is owned by one thread.** The library hands it to the
|
||||||
Hand a context over and stop touching it — the content is written with no
|
thread that raised it. Handing it to another thread is your synchronization.
|
||||||
lock, so the handoff is what publishes it. See "Handing an error to another
|
|
||||||
thread" in docs/thread-safety.md for the pattern, and for why the queue has
|
|
||||||
to be bounded.
|
|
||||||
* **`akerr_log_method` and `akerr_handler_unhandled_error`** are read on every
|
* **`akerr_log_method` and `akerr_handler_unhandled_error`** are read on every
|
||||||
error and written by nobody but you. Set them during startup, before spawning.
|
error and written by nobody but you. Set them during startup, before spawning.
|
||||||
* **Renaming a status while another thread looks it up.**
|
* **Renaming a status while another thread looks it up.**
|
||||||
@@ -167,20 +122,15 @@ 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.
|
||||||
|
|
||||||
## Proving it
|
## Proving it
|
||||||
|
|
||||||
`tests/err_threads_init.c`, `tests/err_threads_pool.c`,
|
`tests/err_threads_init.c`, `tests/err_threads_pool.c` and
|
||||||
`tests/err_threads_registry.c` and `tests/err_threads_handoff.c` assert the
|
`tests/err_threads_registry.c` assert the properties directly and run in the
|
||||||
properties directly and run in the normal suite. The run that proves there is no data race underneath them is
|
normal suite. The run that proves there is no data race underneath them is
|
||||||
ThreadSanitizer:
|
ThreadSanitizer:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -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_ */
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Library Architecture
|
|
||||||
|
|
||||||
## Philosophy of Use
|
|
||||||
|
|
||||||
This library has 6 guiding principles:
|
|
||||||
|
|
||||||
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
|
||||||
* Functions should return rich descriptive error contexts, not values
|
|
||||||
* Uncaught errors should cause program termination with a stacktrace
|
|
||||||
* Dynamic memory allocation is the source of many errors and should be avoided if possible
|
|
||||||
* Manipulating the call stack directly is error prone and dangerous
|
|
||||||
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
|
||||||
|
|
||||||
## Lifecycle of an error in the AKError library
|
|
||||||
|
|
||||||
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
|
|
||||||
|
|
||||||
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
|
|
||||||
2. The akerr_ErrorContext is returned from the scope where the error was detected
|
|
||||||
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
|
|
||||||
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
|
|
||||||
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
|
|
||||||
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
|
|
||||||
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
|
|
||||||
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
|
|
||||||
|
|
||||||
## What is in an Error Context
|
|
||||||
|
|
||||||
The Error Context object is a simple object which contains a few things:
|
|
||||||
|
|
||||||
* A numeric error code
|
|
||||||
* The name of the file in which the error occurred
|
|
||||||
* The name of the function in which the error occurred
|
|
||||||
* The line number in the file at which the error occurred
|
|
||||||
* A character buffer containing a message about the error in question
|
|
||||||
|
|
||||||
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.tmpl.h](../include/akerror.tmpl.h) for more details.
|
|
||||||
|
|
||||||
## What are the control structures
|
|
||||||
|
|
||||||
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
|
|
||||||
|
|
||||||
## Functions and Return Codes
|
|
||||||
|
|
||||||
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
|
|
||||||
|
|
||||||
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Building libakerror
|
|
||||||
|
|
||||||
The ordinary build is an out-of-tree CMake build, described in
|
|
||||||
[the README](../README.md#installation). This file covers what the build
|
|
||||||
generates for you, what it links against, and the configure options that change
|
|
||||||
either of those.
|
|
||||||
|
|
||||||
## Configure options
|
|
||||||
|
|
||||||
| 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_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_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_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
|
|
||||||
`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.
|
|
||||||
|
|
||||||
## Templating and autogenerated code
|
|
||||||
|
|
||||||
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
|
|
||||||
output. When it is `OFF`, this is skipped entirely (`errno --list` needs
|
|
||||||
moreutils and `<errno.h>`, neither freestanding-safe) and
|
|
||||||
`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.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
- `strncpy` function
|
|
||||||
- `strlen` function
|
|
||||||
- `strcmp` function
|
|
||||||
- `snprintf` function
|
|
||||||
- `exit` function
|
|
||||||
- `bool` type
|
|
||||||
- `NULL` type
|
|
||||||
- `size_t` type
|
|
||||||
- `INT_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:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmake -S . -B build -DAKERR_USE_STDLIB=OFF -DAKERR_THREADS=none \
|
|
||||||
-DAKERR_RUNTIME_HEADER=/path/to/your/runtime.h
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
```
|
|
||||||
|
|
||||||
If you omit `-DAKERR_RUNTIME_HEADER`, the build falls back to a convenience
|
|
||||||
header backed by the host's own libc (see the options table above), so the
|
|
||||||
`OFF` configuration still builds and its test suite still runs on an ordinary
|
|
||||||
hosted machine — useful for exercising the freestanding code paths without a
|
|
||||||
real freestanding runtime, but not what an actually freestanding consumer
|
|
||||||
wants. Supply your own header to get the real thing.
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Exit status
|
|
||||||
|
|
||||||
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
|
||||||
|
|
||||||
```c
|
|
||||||
void akerr_exit(int status);
|
|
||||||
```
|
|
||||||
|
|
||||||
This applies everywhere you are leaving the process on account of a status, not
|
|
||||||
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
|
||||||
initialization routine that cannot continue, a `main()` that ends by reporting
|
|
||||||
the status it finished with. One function owns the mapping so that a given
|
|
||||||
status produces the same exit code no matter which of your exits it left by.
|
|
||||||
|
|
||||||
The mapping exists because a process exit status is one byte wide. `exit()`
|
|
||||||
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
|
||||||
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
|
||||||
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
|
||||||
truncated value because the truncation happened before the parent looked. There
|
|
||||||
is no wider `exit()` to reach for.
|
|
||||||
|
|
||||||
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
|
||||||
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
|
||||||
status can be an exit code:
|
|
||||||
|
|
||||||
```
|
|
||||||
status exit code
|
|
||||||
0 0 (success)
|
|
||||||
1 .. 255 the status
|
|
||||||
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
|
||||||
```
|
|
||||||
|
|
||||||
Passing the low byte instead would have made status 256 exit 0 and report
|
|
||||||
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
|
||||||
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
|
||||||
belong to the shell.
|
|
||||||
|
|
||||||
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
|
||||||
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
|
||||||
0`, which marks a zero status handled, so a successful context cannot reach
|
|
||||||
`FINISH_NORETURN`'s call to the handler at all.
|
|
||||||
|
|
||||||
Above 255, the exit code tells you the process died of an error, not which one.
|
|
||||||
125 is inside the library's reserved band and so is also some host's `errno`, and
|
|
||||||
every status above 255 collapses onto it. **The stack trace is what identifies
|
|
||||||
the error** — it is printed before the handler runs, carrying the status at full
|
|
||||||
width along with its registered name.
|
|
||||||
|
|
||||||
## Replacing the handler
|
|
||||||
|
|
||||||
After the trace is printed, `FINISH_NORETURN` calls
|
|
||||||
`akerr_handler_unhandled_error`. The default implementation,
|
|
||||||
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
|
||||||
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
|
||||||
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
|
||||||
`akerr_exit()` so the exit code still means what it means everywhere else:
|
|
||||||
|
|
||||||
```c
|
|
||||||
static void mylib_handler(akerr_ErrorContext *e)
|
|
||||||
{
|
|
||||||
mylib_flush_telemetry();
|
|
||||||
if ( e == NULL ) {
|
|
||||||
akerr_exit(AKERR_API);
|
|
||||||
}
|
|
||||||
akerr_exit(e->status);
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_handler_unhandled_error = &mylib_handler;
|
|
||||||
```
|
|
||||||
|
|
||||||
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
|
||||||
ending in one of those calls is complete rather than falling off the end.
|
|
||||||
|
|
||||||
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
|
||||||
installs one that does not exit at all, which is how the test suite asserts on
|
|
||||||
unhandled errors without dying.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Error codes
|
|
||||||
|
|
||||||
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
|
||||||
|
|
||||||
You can define additional error types as integer constants. Values 0 through 255
|
|
||||||
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
|
||||||
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
|
||||||
names are stored sparsely, so any `int` is a legal status and no compile
|
|
||||||
definition is needed to use large values. Note that no consumer status can be a
|
|
||||||
process exit code — see [Exit status](exit-status.md).
|
|
||||||
|
|
||||||
Every library that may coexist in one process must reserve its range during
|
|
||||||
initialization. `akerr_reserve_status_range()` and
|
|
||||||
`akerr_register_status_name()` report failure the way everything else in this
|
|
||||||
library does — they return `akerr_ErrorContext *`, and they are marked
|
|
||||||
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
|
||||||
`PASS` up out of your initialization:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define MYLIB_OWNER "my-library"
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
/* Another component owning part of the range propagates to our caller. */
|
|
||||||
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
|
||||||
|
|
||||||
/* Then name each code, quoting the owner you reserved with. */
|
|
||||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
|
||||||
"Some Error Code Description"));
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reservations are process-local, fixed-capacity, and idempotent when the same
|
|
||||||
owner repeats the exact same range. They preserve compile-time integer constants
|
|
||||||
so `HANDLE` still works, since `case` labels require them.
|
|
||||||
|
|
||||||
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
|
||||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
|
||||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
|
||||||
owner. See [UPGRADING.md](../UPGRADING.md) for the full list of statuses these two
|
|
||||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
|
||||||
rules.
|
|
||||||
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
# Thread safety
|
|
||||||
|
|
||||||
The library is thread safe as built by default. Every entry point may be called
|
|
||||||
from any thread at any time, including the first one: `akerr_init()` runs
|
|
||||||
exactly once no matter how many threads race into it.
|
|
||||||
|
|
||||||
What that covers:
|
|
||||||
|
|
||||||
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
|
|
||||||
reference is one operation under a lock, so two threads can never be handed
|
|
||||||
the same context. A context is then owned by exactly one thread at a time, all
|
|
||||||
the way through `CATCH`, `HANDLE`, and release.
|
|
||||||
* **The status registry.** Reservations and name registrations are serialized
|
|
||||||
against each other and against lookups. Two threads reserving the same range
|
|
||||||
cannot both win — exactly one gets `NULL` and the other gets
|
|
||||||
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
|
||||||
* **Per-thread state.** `IGNORE` copies the swallowed context into its
|
|
||||||
thread-local `akerr_last_ignored` snapshot before releasing the pool slot.
|
|
||||||
The snapshot remains valid until that thread ignores another error, so a
|
|
||||||
later pool checkout cannot overwrite it. The snapshot and the last-ditch
|
|
||||||
context used to report `akerr_release_error(NULL)` are thread-local, so
|
|
||||||
concurrent calls cannot overwrite each other's state.
|
|
||||||
* **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
|
|
||||||
outlives the thread that raised it. The reference count is the only field the
|
|
||||||
library reads across threads, and it is only ever touched under the pool lock,
|
|
||||||
so `akerr_release_error()` does not care which thread checked the slot out.
|
|
||||||
Raise on a worker, queue it, let the worker exit; the collector still has a
|
|
||||||
whole error with a whole stack trace. See
|
|
||||||
[Handing an error to another thread](#handing-an-error-to-another-thread).
|
|
||||||
|
|
||||||
What it does not cover, and cannot:
|
|
||||||
|
|
||||||
* **Two threads inside one context at the same time.** A context has one owner,
|
|
||||||
and only one. `FAIL` rewrites the message, `HANDLE` rewinds the stack-trace
|
|
||||||
cursor, and none of that is locked — two threads in one context splice their
|
|
||||||
messages together and truncate each other's trace, without crashing. Handing a
|
|
||||||
context *from* one thread *to* another is a different thing, and is supported.
|
|
||||||
* **`akerr_log_method` and `akerr_handler_unhandled_error`.** Set them during
|
|
||||||
startup, before you spawn threads. They are read on every error and the
|
|
||||||
library never writes them after initialization, so setting one while other
|
|
||||||
threads are raising errors is a race the library cannot mediate.
|
|
||||||
* **Renaming a status that other threads are looking up.**
|
|
||||||
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
|
|
||||||
valid for the life of the process; registering a *second* name for the same
|
|
||||||
status overwrites that buffer in place. Register names during initialization.
|
|
||||||
Registering a *new* status concurrently is fine.
|
|
||||||
* **Which unhandled error terminates the process.** An error that reaches
|
|
||||||
`FINISH_NORETURN` unhandled prints its stack trace and calls
|
|
||||||
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
|
|
||||||
thread's trace is whole — the buffer belongs to its context, and each line is
|
|
||||||
one call to `akerr_log_method` — but if two threads get there at the same
|
|
||||||
instant, both traces print and the exit status is whichever one won.
|
|
||||||
|
|
||||||
There is one lock, it is recursive, and it covers both the pool and the
|
|
||||||
registry. That means error construction is serialized across threads: raising an
|
|
||||||
error is the exceptional path, and correctness there is worth more than
|
|
||||||
throughput. A program that raises errors on its hot path will feel it.
|
|
||||||
|
|
||||||
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
|
|
||||||
consumer can check what it linked against:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#if AKERR_THREAD_SAFE
|
|
||||||
/* ... start worker threads ... */
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handing an error to another thread
|
|
||||||
|
|
||||||
**Transfer** an error context; never **share** one. One thread owns it at a time,
|
|
||||||
and ownership moves in a single step: the thread giving it up stops touching it
|
|
||||||
in the same act that makes it visible to the thread taking it over.
|
|
||||||
|
|
||||||
This works because a context is not thread state. It lives in
|
|
||||||
`AKERR_ARRAY_ERROR`, which is process-global, so a context outlives the thread
|
|
||||||
that raised it — a worker can raise an error, queue it, and exit, and the
|
|
||||||
collector still has a whole error with a whole stack trace. Nothing in the
|
|
||||||
library reads a context through any pointer but the one its current owner handed
|
|
||||||
in. The one field it reads across threads is the reference count, and that is
|
|
||||||
only ever touched under the pool lock, so `akerr_release_error()` does not care
|
|
||||||
which thread checked the slot out. Release it wherever it ended up.
|
|
||||||
|
|
||||||
**Always** hand it over through something that synchronizes: a mutex, a
|
|
||||||
condition variable, `pthread_join`, or an acquire/release atomic. The content of
|
|
||||||
a context is written with no lock at all — that is deliberate, error
|
|
||||||
construction should not pay for a lock it does not need — so the handoff itself
|
|
||||||
is what publishes those writes. Push the pointer through a relaxed atomic or a
|
|
||||||
plain global and the receiver can read a half-written message, on a machine you
|
|
||||||
did not test on.
|
|
||||||
|
|
||||||
**Never** touch a context after you have given it away. Not a `->status`, not a
|
|
||||||
log line. The receiver may be inside `HANDLE` rewinding the trace cursor, or
|
|
||||||
inside `akerr_release_error()` memsetting the slot.
|
|
||||||
|
|
||||||
**Release it exactly once.** A context released twice from a stale pointer takes
|
|
||||||
the refcount-zero branch a second time and wipes the slot again — which by then
|
|
||||||
holds somebody else's live error. Nothing crashes; a different thread's error
|
|
||||||
just quietly goes blank.
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* Producer. Owns the context until queue_push() returns, and not after. */
|
|
||||||
static void report_unit_failure(int unit)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
|
|
||||||
FAIL(e, MYLIB_UNIT_FAILED, "unit %d stopped responding", unit);
|
|
||||||
/* The last thing this thread does to it, so the trace records the crossing.
|
|
||||||
The buffer belongs to the context, so it travels with it. */
|
|
||||||
AKERR_STACKTRACE_APPEND(e, "%s:%s:%d: queued for the collector\n",
|
|
||||||
__FILE__, __func__, __LINE__);
|
|
||||||
queue_push(e); /* takes the queue mutex; `e` is not ours after this */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collector. Owns it from the moment queue_pop() returns. */
|
|
||||||
static void collect_one(akerr_ErrorContext *e)
|
|
||||||
{
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} HANDLE(e, MYLIB_UNIT_FAILED) {
|
|
||||||
restart_unit(e);
|
|
||||||
} HANDLE_DEFAULT(e) {
|
|
||||||
LOG_ERROR_WITH_MESSAGE(e, "collector: unrecognized failure");
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *collector(void *unused)
|
|
||||||
{
|
|
||||||
akerr_ErrorContext *e;
|
|
||||||
|
|
||||||
(void)unused;
|
|
||||||
while ( (e = queue_pop()) != NULL ) {
|
|
||||||
collect_one(e);
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Four things about the receiving side:
|
|
||||||
|
|
||||||
* **Declare a plain pointer, not `PREPARE_ERROR`.** That macro *declares* a
|
|
||||||
fresh context variable set to `NULL`; it cannot adopt one. For the same
|
|
||||||
reason, never `CATCH` into the variable holding a received context — `CATCH`
|
|
||||||
assigns over it, and the slot you were handed is gone.
|
|
||||||
* **In a `void` helper, use `FINISH_NORETURN`.** `FINISH_LOGIC` decides whether
|
|
||||||
to propagate at run time, so the compiler still *parses* its
|
|
||||||
`return __err_context` even when the second argument is the literal `false`.
|
|
||||||
`FINISH(e, false)` in a function returning void therefore draws
|
|
||||||
`warning: 'return' with a value, in function returning void` from gcc — a
|
|
||||||
constraint violation, and a build failure under `-Werror`. To propagate
|
|
||||||
inside the collector's own call stack, give the
|
|
||||||
helper an `akerr_ErrorContext *` return and use `FINISH(e, true)` as usual —
|
|
||||||
just never let an error propagate out of the thread body itself, whose
|
|
||||||
`void *` return nobody reads. `PASS` has the same problem for the same reason:
|
|
||||||
in a thread body it compiles, hands the pointer back as a `void *`, and leaks
|
|
||||||
the slot.
|
|
||||||
* **`FINISH_NORETURN(e)` on a received context still terminates the process.**
|
|
||||||
That is right — an unhandled error is unhandled wherever it was raised — but
|
|
||||||
it is now the collector's thread deciding the exit status, not the raiser's.
|
|
||||||
* **Give the handler blocks their own function.** `ATTEMPT` is a `switch`, and a
|
|
||||||
`break` inside one written directly in a loop leaves the `switch`, not the
|
|
||||||
loop. Same hazard as
|
|
||||||
[do not use CATCH or FAIL_*_BREAK inside a loop](usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
|
|
||||||
|
|
||||||
**Always** bound the queue. A queued error is a checked-out pool slot, and there
|
|
||||||
are `AKERR_MAX_ARRAY_ERROR` (128) of them in the entire process. Size *queue
|
|
||||||
depth + producers with an error in flight* well under that. When the pool runs
|
|
||||||
dry the library logs and calls `exit(1)` from inside `FAIL` — there is no slot
|
|
||||||
left to raise the failure *from*, which is exactly why a collector that stops
|
|
||||||
draining takes the process with it.
|
|
||||||
|
|
||||||
### If you need to keep it as well as report it
|
|
||||||
|
|
||||||
There is no copy or retain call, on purpose: a context is a pool slot, and two
|
|
||||||
owners of one slot is the thing this whole section exists to prevent. So either
|
|
||||||
read out what you want to keep — `status` is an `int`, `message` and
|
|
||||||
`stacktracebuf` are ordinary NUL-terminated strings you can `snprintf` into your
|
|
||||||
own, much smaller, record — or raise two errors and hand one of them over.
|
|
||||||
|
|
||||||
**Never** copy an `akerr_ErrorContext` by assignment and keep the copy:
|
|
||||||
|
|
||||||
```c
|
|
||||||
akerr_ErrorContext snapshot = *failed; /* looks fine. is not. */
|
|
||||||
```
|
|
||||||
|
|
||||||
`stacktracebufptr` points into the context's *own* `stacktracebuf`, so after that
|
|
||||||
assignment `snapshot`'s cursor still points into `failed`'s buffer.
|
|
||||||
`LOG_ERROR(&snapshot)` reads the array and prints correctly, so it looks healthy
|
|
||||||
— and then the first `AKERR_STACKTRACE_APPEND(&snapshot, ...)` writes into a
|
|
||||||
pool slot you no longer own, arbitrarily far from the copy. `arrayid` has the
|
|
||||||
same shape: it is restored after the wipe, so a copied id makes the destination
|
|
||||||
impersonate the source's slot forever. And the copy is not a pool address, so
|
|
||||||
`akerr_valid_error_address()` rejects it, every `CATCH` on it becomes
|
|
||||||
`AKERR_BADEXC`, and releasing it memsets your own storage while the real slot
|
|
||||||
stays checked out for the life of the process.
|
|
||||||
|
|
||||||
## Building single threaded
|
|
||||||
|
|
||||||
The threading backend is chosen when libakerror is configured. `auto` (the
|
|
||||||
default) takes POSIX threads, and **fails the configure** if it cannot find
|
|
||||||
them rather than quietly producing a library that says it is thread safe and is
|
|
||||||
not. To mean it:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake -S . -B build -DAKERR_THREADS=none
|
|
||||||
```
|
|
||||||
|
|
||||||
That builds with no locking and no thread-local storage, stamps
|
|
||||||
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
|
|
||||||
one thread is then undefined.
|
|
||||||
|
|
||||||
## Proving it
|
|
||||||
|
|
||||||
The thread tests (`tests/err_threads_*.c`) assert the properties above directly:
|
|
||||||
exclusive ownership of pool slots, exactly one winner for a contested range,
|
|
||||||
every registered name readable back, and — in `err_threads_handoff.c` — an error
|
|
||||||
raised on one thread arriving whole on another and released there. They run in
|
|
||||||
the normal suite. The run that
|
|
||||||
proves the *absence* of a data race underneath them is ThreadSanitizer:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
scripts/thread_test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
|
|
||||||
and every test with it, and runs the suite. Under that build a sanitizer report
|
|
||||||
fails the test rather than being printed and passed over.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Uncaught errors
|
|
||||||
|
|
||||||
## Misbehaving methods
|
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
|
|
||||||
|
|
||||||
## Ensuring that all error codes are captured
|
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERR_NOIGNORE`.
|
|
||||||
|
|
||||||
```c
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *f(...);
|
|
||||||
```
|
|
||||||
|
|
||||||
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
|
|
||||||
|
|
||||||
Beware that `AKERR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stack Traces
|
|
||||||
|
|
||||||
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
|
|
||||||
|
|
||||||
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
|
|
||||||
tests/err_trace.c:func2:10
|
|
||||||
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
|
|
||||||
tests/err_trace.c:func1:18
|
|
||||||
tests/err_trace.c:func1:21
|
|
||||||
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
|
|
||||||
tests/err_trace.c:main:30
|
|
||||||
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
|
|
||||||
```
|
|
||||||
|
|
||||||
From bottom to top, we have:
|
|
||||||
|
|
||||||
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
|
||||||
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
|
||||||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
|
||||||
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
|
|
||||||
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
|
|
||||||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
|
||||||
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
|
||||||
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
|
||||||
|
|
||||||
238
docs/usage.md
238
docs/usage.md
@@ -1,238 +0,0 @@
|
|||||||
# Using the library
|
|
||||||
|
|
||||||
## (Optional) Configuring the logging function
|
|
||||||
|
|
||||||
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
|
||||||
|
|
||||||
```
|
|
||||||
void my_logger(const char *fmt, ...)
|
|
||||||
{
|
|
||||||
/* ... do something */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* set your custom error handler */
|
|
||||||
akerr_log_method = &my_logger;
|
|
||||||
|
|
||||||
/* proceed to use the library */
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setting Up the Error Context
|
|
||||||
|
|
||||||
Before you can use any of these macros you must set up an error context inside of the current scope.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
|
||||||
|
|
||||||
## Attempting an Operation
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
// ... code
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true)
|
|
||||||
```
|
|
||||||
|
|
||||||
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
|
||||||
|
|
||||||
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
|
||||||
|
|
||||||
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
|
||||||
|
|
||||||
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORETURN(errctx)` instead.
|
|
||||||
|
|
||||||
|
|
||||||
## Capturing errors
|
|
||||||
|
|
||||||
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
|
||||||
|
|
||||||
### Capturing errors from functions which return akerr_ErrorContext *
|
|
||||||
|
|
||||||
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, errorGeneratingFunction())
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
|
||||||
|
|
||||||
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
|
||||||
|
|
||||||
### Setting errors from functions or expressions returning integer
|
|
||||||
|
|
||||||
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
|
||||||
|
|
||||||
Here is an example of checking for a NULL pointer
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_ZERO_BREAK(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is an example of checking for two strings that are not equal
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
|
||||||
|
|
||||||
### Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
|
||||||
|
|
||||||
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
for ( int i = 0; i < n; i++ ) {
|
|
||||||
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
|
||||||
}
|
|
||||||
// ... this code still executes, with errctx already in an error state ...
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
|
||||||
|
|
||||||
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
|
||||||
|
|
||||||
```c
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
for ( int i = 0; i < n; i++ ) {
|
|
||||||
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
|
||||||
}
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
|
||||||
} CLEANUP {
|
|
||||||
// ... always runs ...
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_VALUE) {
|
|
||||||
// ... handle a failure from any iteration ...
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Passing errors
|
|
||||||
|
|
||||||
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
|
||||||
|
|
||||||
```
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
PASS(e, some_method_that_may_fail());
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
This does the same thing as this, but with less code:
|
|
||||||
|
|
||||||
```
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, some_method_that_may_fail());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handling errors
|
|
||||||
|
|
||||||
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
|
||||||
|
|
||||||
### Handling a specific error with HANDLE
|
|
||||||
|
|
||||||
In order to handle a specific error code, use the `HANDLE` macro.
|
|
||||||
|
|
||||||
```c
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
|
||||||
// Something is complaining about a null pointer error. Do something about it.
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling a group of errors with HANDLE_GROUP
|
|
||||||
|
|
||||||
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
|
||||||
|
|
||||||
```c
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_IO) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
|
||||||
// error handling code goes here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
|
||||||
|
|
||||||
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
|
||||||
|
|
||||||
```c
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_IO) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
|
||||||
// This code handles 3 error cases
|
|
||||||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
|
||||||
// This code handles 1 error case
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Returning success or failure from functions returning akerr_ErrorContext *
|
|
||||||
|
|
||||||
If at all possible, when using this library, your functions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
|
||||||
|
|
||||||
### SUCCEED_RETURN
|
|
||||||
|
|
||||||
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
ATTEMPT {
|
|
||||||
// ... stuff
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
```
|
|
||||||
|
|
||||||
### FAIL_RETURN
|
|
||||||
|
|
||||||
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
|
||||||
|
|
||||||
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conditionally failing and returning
|
|
||||||
|
|
||||||
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_ZERO_RETURN(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
|
||||||
```
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
|
||||||
```
|
|
||||||
@@ -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,14 +15,14 @@
|
|||||||
* 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.
|
||||||
*
|
*
|
||||||
* 1 The error pool and the status registry are mutex protected, and the
|
* 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
|
* per-thread state below is thread local. Every entry point may be called
|
||||||
* from any thread. See docs/thread-safety.md for what that does and does
|
* from any thread. See "Thread safety" in README.md for what that does and
|
||||||
* not cover.
|
* does not cover.
|
||||||
* 0 The library was built -DAKERR_THREADS=none for a single-threaded
|
* 0 The library was built -DAKERR_THREADS=none for a single-threaded
|
||||||
* process: no locking, no thread-local storage, and calling it from more
|
* process: no locking, no thread-local storage, and calling it from more
|
||||||
* than one thread is undefined.
|
* than one thread is undefined.
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -197,11 +159,6 @@ typedef struct
|
|||||||
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
||||||
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||||
/*
|
/*
|
||||||
* Set these before starting threads. They are read on every error and written
|
* Set these before starting threads. They are read on every error and written
|
||||||
@@ -211,24 +168,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
|
* The error IGNORE() last swallowed, per thread: an ignored error is a fact
|
||||||
* before releasing its pool context, so this remains a useful debugging aid
|
* about the thread that ignored it, and one shared slot would have two threads
|
||||||
* after the pool slot is reused. The snapshot is read-only and is replaced by
|
* overwriting each other's. Thread local only when AKERR_THREAD_SAFE is 1.
|
||||||
* the next ignored error. Thread local only when 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
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||||
/*
|
/*
|
||||||
* Check a context out of the pool. The returned context already carries one
|
* Check a context out of the pool. The returned context already carries one
|
||||||
@@ -334,7 +279,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); \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,18 +375,6 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
|
|||||||
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
|
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
* HANDLE_GROUP deliberately enters the next case label. GCC and clang both
|
|
||||||
* understand this spelling in the C modes supported by libakerror. Other
|
|
||||||
* compilers keep the established control flow without receiving an attribute
|
|
||||||
* they do not implement.
|
|
||||||
*/
|
|
||||||
#if defined(__GNUC__) || defined(__clang__)
|
|
||||||
#define AKERR_FALLTHROUGH __attribute__((fallthrough));
|
|
||||||
#else
|
|
||||||
#define AKERR_FALLTHROUGH
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define ATTEMPT \
|
#define ATTEMPT \
|
||||||
switch ( 0 ) { \
|
switch ( 0 ) { \
|
||||||
case 0: \
|
case 0: \
|
||||||
@@ -473,20 +406,10 @@ 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, \
|
}
|
||||||
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 \
|
||||||
};
|
};
|
||||||
@@ -504,7 +427,6 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
|
|||||||
__err_context->handled = true;
|
__err_context->handled = true;
|
||||||
|
|
||||||
#define HANDLE_GROUP(__err_context, __err_status) \
|
#define HANDLE_GROUP(__err_context, __err_status) \
|
||||||
AKERR_FALLTHROUGH \
|
|
||||||
case __err_status: \
|
case __err_status: \
|
||||||
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
||||||
__err_context->handled = true;
|
__err_context->handled = true;
|
||||||
|
|||||||
@@ -7,36 +7,15 @@ 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
|
||||||
|
|
||||||
if [ "${use_stdlib}" = "1" ]; then
|
|
||||||
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
|
echo "#include <akerror.h>" >> ${outdir}/src/errno.c
|
||||||
echo "#include <errno.h>" >> ${outdir}/src/errno.c
|
echo "#include <errno.h>" >> ${outdir}/src/errno.c
|
||||||
cat >> ${outdir}/src/errno.c <<'EOF'
|
cat >> ${outdir}/src/errno.c <<'EOF'
|
||||||
@@ -59,16 +38,6 @@ EOF
|
|||||||
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
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ Usage:
|
|||||||
--work DIR scratch dir for the mutated copy (default: a temp dir)
|
--work DIR scratch dir for the mutated copy (default: a temp dir)
|
||||||
--timeout SECONDS per-suite ctest timeout (default: 120)
|
--timeout SECONDS per-suite ctest timeout (default: 120)
|
||||||
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
|
--threshold PCT exit non-zero if mutation score < PCT (default: 0 = off)
|
||||||
--cmake-arg ARG pass an additional argument to the mutant CMake configure;
|
|
||||||
repeat for multiple arguments (e.g. -DAKERR_SANITIZE=thread)
|
|
||||||
--list only list the mutants that would be run, then exit
|
--list only list the mutants that would be run, then exit
|
||||||
--keep keep the scratch working copy on exit (for debugging)
|
--keep keep the scratch working copy on exit (for debugging)
|
||||||
-j N (reserved) currently runs sequentially
|
-j N (reserved) currently runs sequentially
|
||||||
@@ -201,11 +199,10 @@ def generate_mutants(root, rel_target):
|
|||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
class Runner:
|
class Runner:
|
||||||
def __init__(self, work, timeout, cmake_args):
|
def __init__(self, work, timeout):
|
||||||
self.work = work
|
self.work = work
|
||||||
self.build = os.path.join(work, "build")
|
self.build = os.path.join(work, "build")
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.cmake_args = cmake_args
|
|
||||||
|
|
||||||
def _run(self, cmd, timeout=None):
|
def _run(self, cmd, timeout=None):
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
@@ -214,8 +211,7 @@ class Runner:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
r = self._run(["cmake", "-S", ".", "-B", "build", *self.cmake_args],
|
r = self._run(["cmake", "-S", ".", "-B", "build"], timeout=self.timeout)
|
||||||
timeout=self.timeout)
|
|
||||||
return r.returncode == 0, r.stdout
|
return r.returncode == 0, r.stdout
|
||||||
|
|
||||||
def build_and_test(self):
|
def build_and_test(self):
|
||||||
@@ -311,8 +307,6 @@ def main():
|
|||||||
ap.add_argument("--work", default=None)
|
ap.add_argument("--work", default=None)
|
||||||
ap.add_argument("--timeout", type=int, default=120)
|
ap.add_argument("--timeout", type=int, default=120)
|
||||||
ap.add_argument("--threshold", type=float, default=0.0)
|
ap.add_argument("--threshold", type=float, default=0.0)
|
||||||
ap.add_argument("--cmake-arg", action="append", default=[],
|
|
||||||
help="pass an argument to the mutant CMake configure; repeatable")
|
|
||||||
ap.add_argument("--junit", default=None,
|
ap.add_argument("--junit", default=None,
|
||||||
help="write a JUnit XML report to this path")
|
help="write a JUnit XML report to this path")
|
||||||
ap.add_argument("--max-mutants", type=int, default=0,
|
ap.add_argument("--max-mutants", type=int, default=0,
|
||||||
@@ -360,7 +354,7 @@ def main():
|
|||||||
print(f"\nCopying sources to scratch dir: {work}")
|
print(f"\nCopying sources to scratch dir: {work}")
|
||||||
copy_tree(root, work)
|
copy_tree(root, work)
|
||||||
|
|
||||||
runner = Runner(work, args.timeout, args.cmake_arg)
|
runner = Runner(work, args.timeout)
|
||||||
|
|
||||||
print("Configuring baseline ...")
|
print("Configuring baseline ...")
|
||||||
ok, out = runner.configure()
|
ok, out = runner.configure()
|
||||||
|
|||||||
39
src/error.c
39
src/error.c
@@ -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];
|
||||||
@@ -136,11 +129,6 @@ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
|||||||
*
|
*
|
||||||
* Takes no lock: the addresses of the pool slots are fixed for the life of the
|
* Takes no lock: the addresses of the pool slots are fixed for the life of the
|
||||||
* process, and nothing here reads a slot's contents.
|
* process, and nothing here reads a slot's contents.
|
||||||
*
|
|
||||||
* NULL reads back as valid, because the caller is VALID() asking whether a
|
|
||||||
* function returned something it has no business returning, and NULL is how a
|
|
||||||
* function says it succeeded. Anything reading this as "is this a pool slot"
|
|
||||||
* must check for NULL itself.
|
|
||||||
*/
|
*/
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||||
{
|
{
|
||||||
@@ -158,7 +146,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 +228,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 +278,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 +372,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 +393,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 +659,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 &&
|
||||||
|
|||||||
10
src/lock.h
10
src/lock.h
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ scripts/mutation_test.py --target src/error.c --list
|
|||||||
|
|
||||||
# Gate CI: exit non-zero if the score drops below 90%
|
# Gate CI: exit non-zero if the score drops below 90%
|
||||||
scripts/mutation_test.py --threshold 90
|
scripts/mutation_test.py --threshold 90
|
||||||
|
|
||||||
# Check concurrency mutants under ThreadSanitizer
|
|
||||||
scripts/mutation_test.py --cmake-arg=-DAKERR_SANITIZE=thread
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Via CMake (configures a build first if needed):
|
Via CMake (configures a build first if needed):
|
||||||
@@ -43,10 +40,8 @@ Via CMake (configures a build first if needed):
|
|||||||
cmake --build build --target mutation
|
cmake --build build --target mutation
|
||||||
```
|
```
|
||||||
|
|
||||||
Useful flags: `--cmake-arg ARG` (pass a CMake configure argument to every
|
Useful flags: `--timeout SECONDS` (per-suite build+test cap; a mutant that
|
||||||
mutant build; repeat it for multiple arguments), `--timeout SECONDS` (per-suite
|
hangs is counted as killed), `--keep` (retain the scratch copy for debugging),
|
||||||
build+test cap; a mutant that hangs is counted as killed), `--keep` (retain the
|
|
||||||
scratch copy for debugging),
|
|
||||||
`--work DIR` (use a specific scratch directory), `--junit FILE` (write a JUnit
|
`--work DIR` (use a specific scratch directory), `--junit FILE` (write a JUnit
|
||||||
XML report — surviving mutants appear as failing test cases).
|
XML report — surviving mutants appear as failing test cases).
|
||||||
|
|
||||||
@@ -99,8 +94,8 @@ Re-run after adding tests and confirm the score went up.
|
|||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
`src/error.c` scores 81.4% — 245 of 301 mutants killed (211 by a failing test,
|
`src/error.c` scores 81.2% — 238 of 293 mutants killed (204 by a failing test,
|
||||||
24 by failing to compile, 10 by hanging the suite), 56 surviving. The CI gate is
|
24 by failing to compile, 10 by hanging the suite), 55 surviving. The CI gate is
|
||||||
set to 65% for headroom.
|
set to 65% for headroom.
|
||||||
|
|
||||||
The ten timeout kills are all in the locking: deleting `akerr_mutex_init()` or
|
The ten timeout kills are all in the locking: deleting `akerr_mutex_init()` or
|
||||||
@@ -112,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
|
||||||
@@ -128,16 +123,10 @@ The remaining survivors are dominated by:
|
|||||||
it never sees that. Deleting an `akerr_init()` call survives for a duller
|
it never sees that. Deleting an `akerr_init()` call survives for a duller
|
||||||
reason: something else has always initialized the library by the time that
|
reason: something else has always initialized the library by the time that
|
||||||
line runs.
|
line runs.
|
||||||
* **The default logger** (`vfprintf`, `va_end`, and the `return` in the
|
* **Default logger / handler internals** (`vfprintf`, `va_end`, the
|
||||||
no-stdlib branch): the other tests replace `akerr_log_method` with the
|
`errctx == NULL` branch, `exit(1)`): killing these needs a subprocess-based
|
||||||
in-process capturing logger, so nothing observes what the default one writes
|
test that captures a child's stderr and exit code, rather than the in-process
|
||||||
to a real stderr. Killing these needs a test that captures a child's stderr.
|
capturing logger the other tests use.
|
||||||
The *handler* internals next to them are no longer in this category:
|
|
||||||
`tests/err_unhandled_null.c` and `tests/err_exit_status.c` read a forked
|
|
||||||
child's exit code, which kills every mutant in `akerr_exit()` and in
|
|
||||||
`akerr_default_handler_unhandled_error()` — all twelve of them, including the
|
|
||||||
`status < 0` → `status < 1` variant that only a test asserting
|
|
||||||
`akerr_exit(0)` exits 0 can distinguish.
|
|
||||||
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
|
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
|
||||||
it guards): a mutated compile-time assertion that still compiles has no
|
it guards): a mutated compile-time assertion that still compiles has no
|
||||||
runtime behavior to observe. Unkillable by construction — the assertion is
|
runtime behavior to observe. Unkillable by construction — the assertion is
|
||||||
@@ -154,8 +143,9 @@ Findings surfaced by mutation testing:
|
|||||||
* **Open:** the harness builds every mutant with the default CMake options, so a
|
* **Open:** the harness builds every mutant with the default CMake options, so a
|
||||||
mutant that only breaks under concurrency is judged by a suite running without
|
mutant that only breaks under concurrency is judged by a suite running without
|
||||||
ThreadSanitizer. Mutating under `-DAKERR_SANITIZE=thread` would close that,
|
ThreadSanitizer. Mutating under `-DAKERR_SANITIZE=thread` would close that,
|
||||||
and needs a way to pass CMake options through to the mutant build. That is
|
and needs a way to pass CMake options through to the mutant build. See
|
||||||
issue #10; `TODO.md` records why the score is a floor rather than a verdict.
|
"Mutation testing judges concurrency mutants without a sanitizer" in
|
||||||
|
`TODO.md`.
|
||||||
|
|
||||||
* **Superseded:** status names now use a private sparse registry, so the old
|
* **Superseded:** status names now use a private sparse registry, so the old
|
||||||
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer
|
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#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 deliberately swallows an error: it records the context in
|
||||||
|
* __akerr_last_ignored, logs it with an "IGNORED ERROR" marker, and lets
|
||||||
|
* execution continue.
|
||||||
|
*/
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
@@ -18,19 +21,11 @@ int main(void)
|
|||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
(void)e;
|
(void)e;
|
||||||
|
|
||||||
/* More failures than the pool has slots must remain safe: a leaking
|
|
||||||
* IGNORE used to exhaust the pool and terminate the process here. The
|
|
||||||
* copied snapshot must also survive the slot being reused on the next
|
|
||||||
* iteration. */
|
|
||||||
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(strcmp(akerr_last_ignored.message,
|
|
||||||
"this error is ignored on purpose") == 0);
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
}
|
|
||||||
reached_after_ignore = 1;
|
reached_after_ignore = 1;
|
||||||
|
|
||||||
|
AKERR_CHECK(__akerr_last_ignored != NULL);
|
||||||
|
AKERR_CHECK(__akerr_last_ignored->status == AKERR_VALUE);
|
||||||
AKERR_CHECK(reached_after_ignore == 1);
|
AKERR_CHECK(reached_after_ignore == 1);
|
||||||
AKERR_CHECK_CONTAINS("IGNORED ERROR");
|
AKERR_CHECK_CONTAINS("IGNORED ERROR");
|
||||||
AKERR_CHECK_CONTAINS("this error is ignored on purpose");
|
AKERR_CHECK_CONTAINS("this error is ignored on purpose");
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
#include "akerror.h"
|
|
||||||
#include "err_capture.h"
|
|
||||||
#include "err_threads.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Handing an error context from one thread to another.
|
|
||||||
*
|
|
||||||
* A context is not thread state. It lives in AKERR_ARRAY_ERROR, which is
|
|
||||||
* process-global, and the only field of it the library reads across an
|
|
||||||
* ownership boundary is the reference count -- which is only ever touched under
|
|
||||||
* the pool lock. So a context can be raised on one thread, handed to another,
|
|
||||||
* and handled and released there, and akerr_release_error() does not care which
|
|
||||||
* thread checked the slot out. That is a property this library promises, and it
|
|
||||||
* is what makes the worker/collector shape usable at all.
|
|
||||||
*
|
|
||||||
* The other half of the promise is what it does *not* cover: two threads inside
|
|
||||||
* one context at once. Ownership moves, it does not fork. This test asserts the
|
|
||||||
* supported half; the unsupported half cannot be asserted without deliberately
|
|
||||||
* racing, which ThreadSanitizer would then correctly fail.
|
|
||||||
*
|
|
||||||
* The queue below is a plain mutex and two condition variables rather than the
|
|
||||||
* __atomic builtins the rest of these tests use, and that is deliberate: the
|
|
||||||
* mutex *is* the thing under test. Context content is written with no lock at
|
|
||||||
* all, so the handoff itself is what publishes those writes to the receiver.
|
|
||||||
*
|
|
||||||
* The claim is proved from four directions:
|
|
||||||
*
|
|
||||||
* 1. The context is still a live pool slot after it crosses, holding exactly
|
|
||||||
* the one reference it was checked out with.
|
|
||||||
* 2. Its message and its whole stack trace -- producer frame and all -- arrive
|
|
||||||
* intact, and in each producer's own order.
|
|
||||||
* 3. The slot is never recycled underneath the transfer: akerr_slot_owner[]
|
|
||||||
* still names the producer when the collector picks it up.
|
|
||||||
* 4. A context outlives the thread that raised it (see main()).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define ITERATIONS 500
|
|
||||||
#define AKERR_HANDOFF_DEPTH 32
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The sizing rule docs/thread-safety.md gives, made executable. Every queued
|
|
||||||
* error is a checked-out pool slot, and so is every producer's error in flight.
|
|
||||||
* Outrun the pool and ENSURE_ERROR_READY exits the process from inside FAIL,
|
|
||||||
* with no slot left to raise the failure from.
|
|
||||||
*/
|
|
||||||
typedef char akerr_assert_handoff_fits_pool[
|
|
||||||
(AKERR_HANDOFF_DEPTH + AKERR_TEST_THREADS < AKERR_MAX_ARRAY_ERROR) ? 1 : -1];
|
|
||||||
|
|
||||||
static struct
|
|
||||||
{
|
|
||||||
pthread_mutex_t lock;
|
|
||||||
pthread_cond_t not_full;
|
|
||||||
pthread_cond_t not_empty;
|
|
||||||
akerr_ErrorContext *slot[AKERR_HANDOFF_DEPTH];
|
|
||||||
int head;
|
|
||||||
int count;
|
|
||||||
} queue;
|
|
||||||
|
|
||||||
/* Bounded on purpose: an unbounded queue of errors is an unbounded number of
|
|
||||||
* checked-out pool slots. Blocking the producer is the backpressure. */
|
|
||||||
static void queue_push(akerr_ErrorContext *errctx)
|
|
||||||
{
|
|
||||||
pthread_mutex_lock(&queue.lock);
|
|
||||||
while ( queue.count == AKERR_HANDOFF_DEPTH ) {
|
|
||||||
pthread_cond_wait(&queue.not_full, &queue.lock);
|
|
||||||
}
|
|
||||||
queue.slot[(queue.head + queue.count) % AKERR_HANDOFF_DEPTH] = errctx;
|
|
||||||
queue.count += 1;
|
|
||||||
pthread_cond_signal(&queue.not_empty);
|
|
||||||
pthread_mutex_unlock(&queue.lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static akerr_ErrorContext *queue_pop(void)
|
|
||||||
{
|
|
||||||
akerr_ErrorContext *errctx;
|
|
||||||
|
|
||||||
pthread_mutex_lock(&queue.lock);
|
|
||||||
while ( queue.count == 0 ) {
|
|
||||||
pthread_cond_wait(&queue.not_empty, &queue.lock);
|
|
||||||
}
|
|
||||||
errctx = queue.slot[queue.head];
|
|
||||||
queue.head = (queue.head + 1) % AKERR_HANDOFF_DEPTH;
|
|
||||||
queue.count -= 1;
|
|
||||||
pthread_cond_signal(&queue.not_full);
|
|
||||||
pthread_mutex_unlock(&queue.lock);
|
|
||||||
return errctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Raise an error and give it away. The stack-trace frame is appended before the
|
|
||||||
* push so the trace records the crossing, and it is the last thing this thread
|
|
||||||
* does to the context: after queue_push() returns, `e` belongs to the collector
|
|
||||||
* and reading even e->status here would be the unsupported half of the rule.
|
|
||||||
*/
|
|
||||||
static void produce_one(akerr_ThreadArg *arg, int seq)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
|
|
||||||
FAIL(e, AKERR_VALUE, "thread %d seq %d", arg->id, seq);
|
|
||||||
AKERR_TCHECK(arg, akerr_slot_claim(e->arrayid, arg->id) == 0);
|
|
||||||
AKERR_STACKTRACE_APPEND(e, "queued by thread %d\n", arg->id);
|
|
||||||
queue_push(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* One received error, handled and released on a thread that never called
|
|
||||||
* akerr_next_error(). That release is the whole claim.
|
|
||||||
*
|
|
||||||
* `seen` is the collector's own per-producer sequence counter. Collector-local
|
|
||||||
* means no atomics: keeping the producers in order is the queue's job, and
|
|
||||||
* checking it is this thread's.
|
|
||||||
*/
|
|
||||||
static void collect_one(akerr_ThreadArg *arg, akerr_ErrorContext *e, int *seen)
|
|
||||||
{
|
|
||||||
char expected[64];
|
|
||||||
int producer = 0;
|
|
||||||
int seq = 0;
|
|
||||||
|
|
||||||
AKERR_TCHECK(arg, akerr_valid_error_address(e) == 1);
|
|
||||||
/* It crossed holding exactly the reference it was checked out with. */
|
|
||||||
AKERR_TCHECK(arg, e->refcount == 1);
|
|
||||||
AKERR_TCHECK(arg, sscanf(e->message, "thread %d seq %d", &producer, &seq) == 2);
|
|
||||||
AKERR_TCHECK(arg, producer >= 2 && producer <= AKERR_TEST_THREADS);
|
|
||||||
if ( producer >= 2 && producer <= AKERR_TEST_THREADS ) {
|
|
||||||
AKERR_TCHECK(arg, seq == seen[producer]);
|
|
||||||
seen[producer] += 1;
|
|
||||||
}
|
|
||||||
/* The slot still belongs to the producer, so nothing recycled it while it
|
|
||||||
* was in flight. */
|
|
||||||
AKERR_TCHECK(arg, akerr_slot_holder(e->arrayid) == producer);
|
|
||||||
|
|
||||||
snprintf(expected, sizeof(expected), "thread %d seq %d", producer, seq);
|
|
||||||
/* Nothing to attempt -- the error is already in hand. The blocks are here
|
|
||||||
* because this is the assembly the macros require, and because a real
|
|
||||||
* collector reads exactly like this. */
|
|
||||||
ATTEMPT {
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
/* case 0: a handed-off error that arrives with no status means somebody
|
|
||||||
* wrote over the context after the producer let it go. */
|
|
||||||
int error_was_lost = 1;
|
|
||||||
AKERR_TCHECK(arg, error_was_lost == 0);
|
|
||||||
} HANDLE(e, AKERR_VALUE) {
|
|
||||||
/* HANDLE rewinds the cursor, but the bytes are still there: the whole
|
|
||||||
* trace crossed with the context, producer frame and handoff frame. */
|
|
||||||
AKERR_TCHECK(arg, strstr(e->stacktracebuf, expected) != NULL);
|
|
||||||
AKERR_TCHECK(arg, strstr(e->stacktracebuf, "queued by thread") != NULL);
|
|
||||||
/* Give the slot up before FINISH releases the context: the other order
|
|
||||||
* hands it back to the pool while this thread still claims it. */
|
|
||||||
akerr_slot_drop(e->arrayid);
|
|
||||||
} FINISH_NORETURN(e);
|
|
||||||
/* FINISH_NORETURN, not FINISH(e, false): FINISH_LOGIC decides whether to
|
|
||||||
* propagate at run time, so the compiler still parses its
|
|
||||||
* `return __err_context` and diagnoses it in a function returning void,
|
|
||||||
* whatever __pass_up says. An error this collector did not handle takes the
|
|
||||||
* process down from here, which is right -- but note it is now the
|
|
||||||
* collector's thread deciding the exit status. */
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Drain exactly what the producers will send. A fixed count rather than a
|
|
||||||
* sentinel: a miscounted handoff should fail the test, not hang it.
|
|
||||||
*/
|
|
||||||
static void collect_all(akerr_ThreadArg *arg)
|
|
||||||
{
|
|
||||||
int seen[AKERR_TEST_THREADS + 1] = { 0 };
|
|
||||||
int total = (AKERR_TEST_THREADS - 1) * ITERATIONS;
|
|
||||||
|
|
||||||
for ( int i = 0; i < total; i++ ) {
|
|
||||||
collect_one(arg, queue_pop(), seen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *handoff_body(void *raw)
|
|
||||||
{
|
|
||||||
akerr_ThreadArg *arg = raw;
|
|
||||||
|
|
||||||
pthread_barrier_wait(arg->barrier);
|
|
||||||
|
|
||||||
if ( arg->id == 1 ) {
|
|
||||||
collect_all(arg);
|
|
||||||
} else {
|
|
||||||
for ( int i = 0; i < ITERATIONS; i++ ) {
|
|
||||||
produce_one(arg, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Written by the raising thread, read by main() after pthread_join(). The join
|
|
||||||
* is the happens-before edge, which is the same thing the queue's mutex does
|
|
||||||
* above -- a plain global needs no atomics once something orders it.
|
|
||||||
*/
|
|
||||||
static akerr_ErrorContext *parked;
|
|
||||||
|
|
||||||
static void *raise_and_exit(void *unused)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
|
|
||||||
(void)unused;
|
|
||||||
FAIL(e, AKERR_IO, "raised on a thread that exited");
|
|
||||||
parked = e;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
pthread_t raiser;
|
|
||||||
int failures = 0;
|
|
||||||
|
|
||||||
akerr_log_method = &akerr_thread_logger;
|
|
||||||
akerr_init();
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
AKERR_CHECK(pthread_mutex_init(&queue.lock, NULL) == 0);
|
|
||||||
AKERR_CHECK(pthread_cond_init(&queue.not_full, NULL) == 0);
|
|
||||||
AKERR_CHECK(pthread_cond_init(&queue.not_empty, NULL) == 0);
|
|
||||||
|
|
||||||
failures = akerr_run_threads(&handoff_body);
|
|
||||||
AKERR_CHECK(failures == 0);
|
|
||||||
/* Every handed-off context was released by the thread that received it. */
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A context outlives the thread that raised it: the pool is process-global,
|
|
||||||
* not thread-local storage. By the time these checks run, the thread that
|
|
||||||
* called FAIL() no longer exists.
|
|
||||||
*/
|
|
||||||
AKERR_CHECK(pthread_create(&raiser, NULL, &raise_and_exit, NULL) == 0);
|
|
||||||
AKERR_CHECK(pthread_join(raiser, NULL) == 0);
|
|
||||||
AKERR_CHECK(parked != NULL);
|
|
||||||
AKERR_CHECK(akerr_valid_error_address(parked) == 1);
|
|
||||||
AKERR_CHECK(parked->status == AKERR_IO);
|
|
||||||
AKERR_CHECK(parked->refcount == 1);
|
|
||||||
AKERR_CHECK(strstr(parked->stacktracebuf, "raised on a thread that exited") != NULL);
|
|
||||||
RELEASE_ERROR(parked);
|
|
||||||
AKERR_CHECK(parked == NULL);
|
|
||||||
|
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
|
||||||
AKERR_CHECK(akerr_slot_holder(i) == 0);
|
|
||||||
}
|
|
||||||
/* Nothing here reports through the log method: a handoff is not an error. */
|
|
||||||
AKERR_CHECK(akerr_thread_logs() == 0);
|
|
||||||
|
|
||||||
pthread_cond_destroy(&queue.not_empty);
|
|
||||||
pthread_cond_destroy(&queue.not_full);
|
|
||||||
pthread_mutex_destroy(&queue.lock);
|
|
||||||
|
|
||||||
fprintf(stderr, "err_threads_handoff ok (%d producers x %d errors)\n",
|
|
||||||
AKERR_TEST_THREADS - 1, ITERATIONS);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -100,21 +100,17 @@ static void *pool_body(void *raw)
|
|||||||
one_checkout(arg);
|
one_checkout(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IGNORE's snapshot is thread-local while logging and remains valid after
|
/* An ignored error is a fact about the thread that ignored it: each thread
|
||||||
* release. Concurrent ignored errors must all return their pool slots. */
|
* must see its own, not the last one any thread swallowed. */
|
||||||
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);
|
if ( __akerr_last_ignored != NULL ) {
|
||||||
|
AKERR_TCHECK(arg, __akerr_last_ignored->status == AKERR_IO);
|
||||||
/* Reuse a slot after IGNORE and prove that the copied snapshot did not
|
AKERR_TCHECK(arg, strcmp(__akerr_last_ignored->message, expected) == 0);
|
||||||
* 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);
|
/* IGNORE keeps the reference by design; hand it back so the pool is empty
|
||||||
AKERR_TCHECK(arg, strcmp(akerr_last_ignored.message, expected) == 0);
|
* at the end of the test. */
|
||||||
|
RELEASE_ERROR(__akerr_last_ignored);
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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_ */
|
|
||||||
Reference in New Issue
Block a user