From 5695061130fd1a1d6368a24f5c5f2235be333a62 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 1 Aug 2026 16:56:07 -0400 Subject: [PATCH] Split the README reference material into docs/ The README was 794 lines: the summary, the design rationale, the whole macro reference, the threading contract, the build internals and the exit-status specification in one file. It is now 178 lines -- summary, installation, quickstart, and an index -- and the reference material lives in docs/, one file per topic: architecture, usage, status-codes, uncaught-errors, exit-status, thread-safety, building. The prose moved as written. Inbound references followed it: UPGRADING.md, TODO.md, include/akerror.tmpl.h and tests/err_threads_handoff.c now name the docs/ file that owns the text they cite, and AGENTS.md says where new documentation goes so the README does not grow back. Five factual errors fixed in the moved text: - Both NULL-pointer examples inverted their test. FAIL_ZERO_* fails when the expression is zero, so `(somePointer == NULL)` failed on a *valid* pointer. They now read `(somePointer != NULL)`. - AKERROR_NOIGNORE, four times including the #define, is AKERR_NOIGNORE. - FINISH_NORExbTURN is FINISH_NORETURN. - "functiions" is "functions". - The architecture link pointed at include/akerror.h, which is generated and not in the tree; it points at include/akerror.tmpl.h. The quickstart is new text. It compiles under -Wall -Wextra -Werror and was run through all three of its paths: handled usage error exits 0, unhandled IO error prints a trace and exits with the status, success exits 0. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 7 + README.md | 800 +++++------------------------------- TODO.md | 18 +- UPGRADING.md | 7 +- docs/architecture.md | 48 +++ docs/building.md | 61 +++ docs/exit-status.md | 76 ++++ docs/status-codes.md | 46 +++ docs/thread-safety.md | 225 ++++++++++ docs/uncaught-errors.md | 50 +++ docs/usage.md | 238 +++++++++++ include/akerror.tmpl.h | 4 +- tests/err_threads_handoff.c | 8 +- 13 files changed, 863 insertions(+), 725 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/building.md create mode 100644 docs/exit-status.md create mode 100644 docs/status-codes.md create mode 100644 docs/thread-safety.md create mode 100644 docs/uncaught-errors.md create mode 100644 docs/usage.md diff --git a/AGENTS.md b/AGENTS.md index d69dbaf..0ed2b95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,13 @@ 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 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 Use an out-of-tree build: diff --git a/README.md b/README.md index 28c819f..9758ccc 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,6 @@ This library provides a TRY/CATCH style exception handling mechanism for C. ![build badge](https://source.starfort.tech/andrew/libakerror/actions/workflows/ci.yaml/badge.svg?branch=main) -## 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? There is nothing wrong with C as it is. This library does not claim to fix some problem with C. @@ -27,10 +12,6 @@ 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. -# 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 @@ -40,313 +21,19 @@ This library has 6 guiding principles: * 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 +# Documentation -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.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 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.** 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. -* **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](#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. +| Document | What it answers | +| -------- | --------------- | +| [docs/architecture.md](docs/architecture.md) | What an error context is, how one travels up the call stack, and what the macros build | +| [docs/usage.md](docs/usage.md) | The macro reference: `ATTEMPT`/`CLEANUP`/`PROCESS`/`FINISH`, `CATCH`, `FAIL_*`, `PASS`, `HANDLE`, `SUCCEED_RETURN` | +| [docs/status-codes.md](docs/status-codes.md) | Defining your own status codes, and reserving a range so two libraries cannot collide | +| [docs/uncaught-errors.md](docs/uncaught-errors.md) | `AKERR_NOIGNORE`, what a stack trace looks like, and how to read one | +| [docs/exit-status.md](docs/exit-status.md) | Why you call `akerr_exit()` and never `exit()`, and how to replace the unhandled-error handler | +| [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 | +| [docs/building.md](docs/building.md) | Configure options, the generated header, and building without stdlib | +| [UPGRADING.md](UPGRADING.md) | What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate | +| [TODO.md](TODO.md) | Known defects, ordered by blast radius | # Installation @@ -356,39 +43,11 @@ cmake --build build cmake --install build ``` -## Templating and autogenerated code - -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`. - -## 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 -``` - -# Using the library +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 @@ -427,368 +86,93 @@ add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL) target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror) ``` +# Quickstart -## (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. +A function that can fail returns an `akerr_ErrorContext *` instead of a value, +and moves its real output to a pointer parameter. `AKERR_NOIGNORE` makes the +compiler complain if a caller throws that return away. ```c -PREPARE_ERROR(errctx); -``` +#include +#include -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_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) +/* 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); - for ( int i = 0; i < n; i++ ) { - PASS(errctx, process(items[i])); // returns from process_all on the first error - } + + FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER, + "no config path was given"); + + *dest = fopen(path, "r"); + FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_IO, + "could not open %s", path); + 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 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) +int main(int argc, char **argv) { - mylib_flush_telemetry(); - if ( e == NULL ) { - akerr_exit(AKERR_API); - } - akerr_exit(e->status); -} + FILE *config = NULL; -akerr_handler_unhandled_error = &mylib_handler; + PREPARE_ERROR(errctx); + ATTEMPT { + FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE, + "usage: %s ", argv[0]); + CATCH(errctx, open_config(argv[1], &config)); + /* ... read the config ... */ + } CLEANUP { + /* Runs whether or not anything failed. */ + if ( config != NULL ) { + fclose(config); + } + } PROCESS(errctx) { + } HANDLE(errctx, AKERR_VALUE) { + /* A usage error is ours to handle, so handle it and carry on. */ + } FINISH_NORETURN(errctx); + + return 0; +} ``` -`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. +`ATTEMPT` is where work that can fail goes. `CLEANUP` always runs. `PROCESS` +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. -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. +Three things worth knowing before you write much more than that: +* [The full macro reference](docs/usage.md), including `PASS` for errors you + 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 + +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 +to another and released there. There is one recursive lock covering the pool and +the registry, so error construction is serialized — a program that raises errors +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 + +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 +[docs/exit-status.md](docs/exit-status.md). No ABI break. + +2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored` +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. diff --git a/TODO.md b/TODO.md index 22c7748..5d9f9f0 100644 --- a/TODO.md +++ b/TODO.md @@ -60,7 +60,7 @@ 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. -Documented in README.md and UPGRADING.md as "register names during +Documented in docs/thread-safety.md and UPGRADING.md as "register names during initialization". Closing it properly means making a registered name immutable — either refusing a rename outright (a behavior change, and `tests/err_name_ownership.c` asserts the current contract), or copying names @@ -114,11 +114,12 @@ behind a flag rather than in the default target or in CI. ## 9. No way to keep an error context and report it at the same time -A context can be handed to another thread and released there -- `README.md` now -documents that pattern and `tests/err_threads_handoff.c` proves it -- but it is a -*move*. A thread that wants to both keep its error and report it upward has to -read the fields out into its own record, and it loses the stack trace doing so, -because `stacktracebuf` is the one thing that cannot be usefully summarized. +A context can be handed to another thread and released there -- +`docs/thread-safety.md` now documents that pattern and +`tests/err_threads_handoff.c` proves it -- but it is a *move*. A thread that +wants to both keep its error and report it upward has to read the fields out +into its own record, and it loses the stack trace doing so, because +`stacktracebuf` is the one thing that cannot be usefully summarized. Copying the struct is not a workaround. `stacktracebufptr` is self-referential (`include/akerror.tmpl.h`), so `akerr_ErrorContext c = *src;` leaves the copy's @@ -153,7 +154,8 @@ point -- `libakstdlib`'s planned `pthread_*` wrappers are the likely first. - 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. + `docs/building.md`'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. diff --git a/UPGRADING.md b/UPGRADING.md index 2dcf50c..ec9a877 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -33,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 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 -install a handler that maps your own statuses into a byte. See "Exit status" in -[README.md](README.md). +install a handler that maps your own statuses into a byte. See +[docs/exit-status.md](docs/exit-status.md). 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 @@ -108,7 +108,8 @@ Still yours to coordinate: * **Two threads in one context at once.** Ownership moves; it does not fork. Hand a context over and stop touching it — the content is written with no lock, so the handoff is what publishes it. See "Handing an error to another - thread" in README.md for the pattern, and for why the queue has to be bounded. + 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 error and written by nobody but you. Set them during startup, before spawning. * **Renaming a status while another thread looks it up.** diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..da4d52a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,48 @@ +# 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()`. + diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..667e8ad --- /dev/null +++ b/docs/building.md @@ -0,0 +1,61 @@ +# 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). | +| `AKERR_USE_STDLIB` | `ON` | Link against the C standard library. See [Dependencies](#dependencies) for what you must supply instead when this is `OFF`. | +| `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. | + +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. 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`. + +Neither 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: + +- `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 +``` + +**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. See [TODO.md](../TODO.md). diff --git a/docs/exit-status.md b/docs/exit-status.md new file mode 100644 index 0000000..d7fdfb8 --- /dev/null +++ b/docs/exit-status.md @@ -0,0 +1,76 @@ +# 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. diff --git a/docs/status-codes.md b/docs/status-codes.md new file mode 100644 index 0000000..c985eca --- /dev/null +++ b/docs/status-codes.md @@ -0,0 +1,46 @@ +# 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. + diff --git a/docs/thread-safety.md b/docs/thread-safety.md new file mode 100644 index 0000000..76ccc36 --- /dev/null +++ b/docs/thread-safety.md @@ -0,0 +1,225 @@ +# 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.** 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. +* **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. diff --git a/docs/uncaught-errors.md b/docs/uncaught-errors.md new file mode 100644 index 0000000..0149525 --- /dev/null +++ b/docs/uncaught-errors.md @@ -0,0 +1,50 @@ +# 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 + diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..2d63462 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,238 @@ +# 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") +``` diff --git a/include/akerror.tmpl.h b/include/akerror.tmpl.h index b8517fd..dad4c45 100644 --- a/include/akerror.tmpl.h +++ b/include/akerror.tmpl.h @@ -21,8 +21,8 @@ * * 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 - * from any thread. See "Thread safety" in README.md for what that does and - * does not cover. + * from any thread. See docs/thread-safety.md for what that does and does + * not cover. * 0 The library was built -DAKERR_THREADS=none for a single-threaded * process: no locking, no thread-local storage, and calling it from more * than one thread is undefined. diff --git a/tests/err_threads_handoff.c b/tests/err_threads_handoff.c index 91e5b84..4e6fe0e 100644 --- a/tests/err_threads_handoff.c +++ b/tests/err_threads_handoff.c @@ -39,10 +39,10 @@ #define AKERR_HANDOFF_DEPTH 32 /* - * The sizing rule the README 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. + * 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];