From 983ecf31c9b9879aca74c5b2d9d45c16c979ca0a Mon Sep 17 00:00:00 2001 From: Tachikoma Date: Wed, 5 Aug 2026 10:29:39 -0400 Subject: [PATCH] Fix the AKERR_USE_STDLIB=OFF build (issue #12) The freestanding build (-DAKERR_USE_STDLIB=OFF) did not compile at all: bool, PATH_MAX and NULL were used unconditionally in the public header but only included under the stdlib branch, and the CMake option was pasted straight into a preprocessor definition, so a non-numeric cache spelling (-DAKERR_USE_STDLIB=ON) silently evaluated to 0. - Normalize AKERR_USE_STDLIB to a plain 1/0 in CMake before stamping it, and use a consistent '#if AKERR_USE_STDLIB' everywhere it is tested. - Include / unconditionally (freestanding-safe); keep // behind AKERR_USE_STDLIB; move out of the public header into src/error.c, its only user. - Define the freestanding runtime contract: AKERR_RUNTIME_HEADER must name a header providing exit, memset, snprintf, strcmp, strlen and strncpy when AKERR_USE_STDLIB is OFF, or the header #errors naming them. Add cmake/akerr_default_runtime.h, a libc-backed convenience default so this repo's own OFF build and tests work out of the box. - Route ENSURE_ERROR_READY's pool-exhaustion path through akerr_exit() instead of a direct exit(1). Deliberate behavior change: that exit code moves from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125), the same sentinel every other unrepresentable status already uses. Documented in docs/building.md and UPGRADING.md. Not an ABI break. - Retire PATH_MAX: AKERR_MAX_ERROR_FNAME_LENGTH is now stamped by scripts/generrno.sh from a new AKERR_MAX_ERROR_FNAME_LENGTH cache variable, defaulting to 4096 (PATH_MAX on Linux/glibc) so sizeof(akerr_ErrorContext) and the soname are unchanged. Rewrite the now-stale PATH_MAX justification in src/lock.h's feature-test-macro comment. - Fail the configure with a FATAL_ERROR, not a warning, when AKERR_USE_STDLIB=OFF and AKERR_THREADS would resolve to pthread, naming -DAKERR_THREADS=none as the fix. - Keep the generated errno table (errno.c) out of the OFF build; skip the 'errno --list' shellout in scripts/generrno.sh under OFF and stamp AKERR_LAST_ERRNO_VALUE from a new fallback cache variable (default 133, Linux's EHWPOISON) instead. - Update docs/building.md: drop the known-defect paragraph, fix sprintf -> snprintf, add size_t, drop PATH_MAX, and document the new options and the exit-code change. - Guard tests/err_errno.c's registered-name assertion behind AKERR_USE_STDLIB: akerr_init_errno() is not called when it is OFF. - Add a CI job that configures/builds/tests AKERR_USE_STDLIB=OFF with AKERR_THREADS=none, plus a compile-only -nostdinc -ffreestanding check of tests/freestanding_fixture.c against the generated header. Bump the project version to 2.0.2 (no ABI break: soname and struct layout are unchanged). Verified locally: OFF+none configures and builds clean with all tests passing; ON and the plain default build both build and pass their full test suites (37/37); OFF with the default/auto thread backend fails configure with a message naming -DAKERR_THREADS=none; sizeof(akerr_ErrorContext) is unchanged (37296 bytes, fname/function still 4096 each) versus the pre-change tree. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/ci.yaml | 28 +++++++++ CMakeLists.txt | 90 ++++++++++++++++++++++++++-- UPGRADING.md | 35 +++++++++++ cmake/akerr_default_runtime.h | 22 +++++++ docs/building.md | 60 ++++++++++++++----- include/akerror.tmpl.h | 48 +++++++++++++-- scripts/generrno.sh | 55 +++++++++++++---- src/error.c | 10 +++- src/lock.h | 10 ++-- tests/err_errno.c | 6 ++ tests/freestanding_fixture.c | 14 +++++ tests/freestanding_fixture_runtime.h | 19 ++++++ 12 files changed, 352 insertions(+), 45 deletions(-) create mode 100644 cmake/akerr_default_runtime.h create mode 100644 tests/freestanding_fixture.c create mode 100644 tests/freestanding_fixture_runtime.h diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 7113c07..4c87eb0 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -37,6 +37,34 @@ jobs: fail_on_failure: 'true' - 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 }}." + coverage: runs-on: ubuntu-latest steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index c501683..d391805 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,13 +9,35 @@ cmake_minimum_required(VERSION 3.10) # 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 # soname is unchanged and no existing entry point changed shape. -project(akerror VERSION 2.0.1 LANGUAGES C) +# 2.0.2 fixes the AKERR_USE_STDLIB=OFF build, which did not compile at all +# (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(CMakePackageConfigHelpers) include(CTest) 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_SANITIZE "" CACHE STRING "Sanitizers to build the library and tests with, e.g. thread or address,undefined") @@ -51,6 +73,34 @@ else() "AKERR_THREADS must be auto, pthread or none, not '${AKERR_THREADS}'") 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 # 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 @@ -154,6 +204,9 @@ add_custom_command( ${CMAKE_CURRENT_SOURCE_DIR} ${GENERATED_DIR} ${AKERR_THREAD_SAFE} + ${AKERR_USE_STDLIB_DEFINE} + ${AKERR_LAST_ERRNO_VALUE_FALLBACK} + ${AKERR_MAX_ERROR_FNAME_LENGTH} DEPENDS ${SCRIPT} ${INFILE} ${GENERATED_THREAD_STAMP} VERBATIM ) @@ -165,9 +218,20 @@ 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 , 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 src/error.c - ${GENERATED_ERRNO_C} + ${AKERR_ERRNO_SOURCES} ) add_dependencies(akerror akerror_generated) @@ -189,12 +253,25 @@ else() set(AKERR_THREADS_DEFINE AKERR_THREADS_NONE=1) 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 - PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB} + PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB_DEFINE} PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS} PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES} PRIVATE ${AKERR_THREADS_DEFINE} ) +if(AKERR_RUNTIME_HEADER_DEFINE) + target_compile_definitions(akerror PUBLIC ${AKERR_RUNTIME_HEADER_DEFINE}) +endif() if(AKERR_THREAD_SAFE) target_link_libraries(akerror PRIVATE Threads::Threads) @@ -216,18 +293,21 @@ akerr_instrument_for_sanitizers(akerror) # weakens the production library. add_library(akerror_init_failure SHARED src/error.c - ${GENERATED_ERRNO_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} + 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() diff --git a/UPGRADING.md b/UPGRADING.md index ee14e1c..e94d801 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,38 @@ +# 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: +`` and `` 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) An unhandled error could kill the process and still report success. diff --git a/cmake/akerr_default_runtime.h b/cmake/akerr_default_runtime.h new file mode 100644 index 0000000..2d36cb4 --- /dev/null +++ b/cmake/akerr_default_runtime.h @@ -0,0 +1,22 @@ +#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 +#include +#include + +#endif /* AKERR_DEFAULT_RUNTIME_H_ */ diff --git a/docs/building.md b/docs/building.md index 1b5cd1a..a8380b0 100644 --- a/docs/building.md +++ b/docs/building.md @@ -9,13 +9,22 @@ either of those. | 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). | +| `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. @@ -24,40 +33,59 @@ The two capacity options are applied `PRIVATE`: the tables live entirely in The build process relies upon `scripts/generrno.sh` which performs the following: -1. Executes `errno --list` and gathers up the output -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`. +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 ``, 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 output is meant to be edited. Change the template or the generator. +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, you must modify the library code to include headers and link against a library that provides the following: +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 -- `sprintf` function +- `snprintf` function - `exit` function - `bool` type - `NULL` type +- `size_t` type - `INT_MAX` constant -- `PATH_MAX` constant + +`` and `` — 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 +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 ``` -**Known defect:** that configuration does not currently compile. `bool`, -`PATH_MAX` and `NULL` are used unconditionally but only included under the -stdlib branch, so the header's includes need untangling before -`-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement -must provide. That configuration does not currently compile -- `bool`, `PATH_MAX` and `NULL` -are used unconditionally but included only under the stdlib branch. See -[issue #12](https://source.starfort.tech/andrew/libakerror/issues/12). +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. diff --git a/include/akerror.tmpl.h b/include/akerror.tmpl.h index e495a65..851bb77 100644 --- a/include/akerror.tmpl.h +++ b/include/akerror.tmpl.h @@ -1,12 +1,42 @@ #ifndef _AKERR_H_ #define _AKERR_H_ -#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB)) -#include +/* + * 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 + +/* + * and 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 +#include + +#if AKERR_USE_STDLIB +#include #include #include -#include +#else +/* + * 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 /* @@ -52,7 +82,15 @@ #define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384 #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_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) @@ -296,7 +334,7 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca __err_context = akerr_next_error(); \ if ( __err_context == NULL ) { \ akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \ - exit(1); \ + akerr_exit(AKERR_EXIT_STATUS_UNREPRESENTABLE); \ } \ } diff --git a/scripts/generrno.sh b/scripts/generrno.sh index 9eebcab..3b59d0b 100644 --- a/scripts/generrno.sh +++ b/scripts/generrno.sh @@ -7,18 +7,39 @@ outdir=$2 # 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_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 , 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 echo "$0: thread-safe argument must be 0 or 1, got '${thread_safe}'" >&2 exit 1 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}/include rm -f ${outdir}/src/errno.c -echo "#include " >> ${outdir}/src/errno.c -echo "#include " >> ${outdir}/src/errno.c -cat >> ${outdir}/src/errno.c <<'EOF' + +if [ "${use_stdlib}" = "1" ]; then + echo "#include " >> ${outdir}/src/errno.c + echo "#include " >> ${outdir}/src/errno.c + cat >> ${outdir}/src/errno.c <<'EOF' /* * These names belong to the library's own reserved band, and this runs from @@ -29,15 +50,25 @@ cat >> ${outdir}/src/errno.c <<'EOF' * control flow no test can reach. */ EOF -echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c -maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1) -errno --list | while read LINE; do - define=$(echo "$LINE" | cut -d ' ' -f 1); - value=$(echo "$LINE" | cut -d ' ' -f 2); - desc=$(echo "$LINE" | cut -d ' ' -f 3-); - echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ; -done; -echo "}" >> ${outdir}/src/errno.c + echo "void akerr_init_errno(void) {" >> ${outdir}/src/errno.c + maxval=$(errno --list | cut -d ' ' -f 2 | sort -g | tail -n 1) + errno --list | while read LINE; do + define=$(echo "$LINE" | cut -d ' ' -f 1); + value=$(echo "$LINE" | cut -d ' ' -f 2); + desc=$(echo "$LINE" | cut -d ' ' -f 3-); + echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ; + done; + echo "}" >> ${outdir}/src/errno.c +else + # Freestanding: no `errno --list` shellout (requires moreutils and + # , 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}/" \ -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 diff --git a/src/error.c b/src/error.c index 0f403dc..1f12459 100644 --- a/src/error.c +++ b/src/error.c @@ -1,6 +1,10 @@ #include "akerror.h" #include "lock.h" -#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1 +/* INT_MAX (used below in akerr_reserve_status_range_locked) only, not in the + * public header: is freestanding-safe, but nothing else in this + * file's freestanding build needs it, so it stays out of the shared header. */ +#include +#if AKERR_USE_STDLIB #include #include #include @@ -154,7 +158,7 @@ int akerr_valid_error_address(akerr_ErrorContext *ptr) void akerr_default_logger(const char *fmt, ...) { -#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1 +#if AKERR_USE_STDLIB va_list ap; va_start(ap, fmt); @@ -285,7 +289,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_FULL, "Status Name Registry Full"); __akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name"); -#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB)) +#if AKERR_USE_STDLIB akerr_init_errno(); #endif diff --git a/src/lock.h b/src/lock.h index b366590..7ead246 100644 --- a/src/lock.h +++ b/src/lock.h @@ -33,10 +33,12 @@ /* * PTHREAD_MUTEX_RECURSIVE is XSI, so glibc hides it under a strict -std=c99 - * without _XOPEN_SOURCE. No feature-test macro is defined here, because the - * public header already needs the same one for PATH_MAX: a build strict enough - * to lose one has already lost the other. Build with -D_XOPEN_SOURCE=700 if you - * need strict C99. + * without _XOPEN_SOURCE. No feature-test macro is defined here: this file is + * only ever compiled with AKERR_THREADS_PTHREAD, which CMake now refuses to + * pair with AKERR_USE_STDLIB=OFF (see the AKERR_THREADS/AKERR_USE_STDLIB + * check in CMakeLists.txt), so whatever default feature-test macros the host + * libc uses when building the rest of the (hosted) library apply here too. + * Build with -D_XOPEN_SOURCE=700 if you need strict C99. */ #if defined(AKERR_THREADS_PTHREAD) && AKERR_THREADS_PTHREAD == 1 diff --git a/tests/err_errno.c b/tests/err_errno.c index 8fa54cc..8202e1a 100644 --- a/tests/err_errno.c +++ b/tests/err_errno.c @@ -26,7 +26,13 @@ int main(void) char *nm = akerr_name_for_status(EACCES, NULL); AKERR_CHECK(nm != NULL); 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); +#endif AKERR_CHECK(strcmp(akerr_name_for_status(1000000, NULL), "Unknown Error") == 0); diff --git a/tests/freestanding_fixture.c b/tests/freestanding_fixture.c new file mode 100644 index 0000000..0e6738a --- /dev/null +++ b/tests/freestanding_fixture.c @@ -0,0 +1,14 @@ +/* + * 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"); +} diff --git a/tests/freestanding_fixture_runtime.h b/tests/freestanding_fixture_runtime.h new file mode 100644 index 0000000..e3cc6af --- /dev/null +++ b/tests/freestanding_fixture_runtime.h @@ -0,0 +1,19 @@ +#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 + * /. size_t comes from , 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_ */