Tachikoma 983ecf31c9
All checks were successful
libakerror CI Build / cmake_build_freestanding (push) Successful in 2m51s
libakerror CI Build / coverage (push) Successful in 2m51s
libakerror CI Build / cmake_build (push) Successful in 2m55s
libakerror CI Build / mutation_test (push) Successful in 47m2s
Fix the AKERR_USE_STDLIB=OFF build (issue #12)
The freestanding build (-DAKERR_USE_STDLIB=OFF) did not compile at all:
bool, PATH_MAX and NULL were used unconditionally in the public header
but only included under the stdlib branch, and the CMake option was
pasted straight into a preprocessor definition, so a non-numeric cache
spelling (-DAKERR_USE_STDLIB=ON) silently evaluated to 0.

- Normalize AKERR_USE_STDLIB to a plain 1/0 in CMake before stamping it,
  and use a consistent '#if AKERR_USE_STDLIB' everywhere it is tested.
- Include <stdbool.h>/<stddef.h> unconditionally (freestanding-safe);
  keep <stdlib.h>/<string.h>/<stdio.h> behind AKERR_USE_STDLIB; move
  <limits.h> out of the public header into src/error.c, its only user.
- Define the freestanding runtime contract: AKERR_RUNTIME_HEADER must
  name a header providing exit, memset, snprintf, strcmp, strlen and
  strncpy when AKERR_USE_STDLIB is OFF, or the header #errors naming
  them. Add cmake/akerr_default_runtime.h, a libc-backed convenience
  default so this repo's own OFF build and tests work out of the box.
- Route ENSURE_ERROR_READY's pool-exhaustion path through akerr_exit()
  instead of a direct exit(1). Deliberate behavior change: that exit
  code moves from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125), the same
  sentinel every other unrepresentable status already uses. Documented
  in docs/building.md and UPGRADING.md. Not an ABI break.
- Retire PATH_MAX: AKERR_MAX_ERROR_FNAME_LENGTH is now stamped by
  scripts/generrno.sh from a new AKERR_MAX_ERROR_FNAME_LENGTH cache
  variable, defaulting to 4096 (PATH_MAX on Linux/glibc) so
  sizeof(akerr_ErrorContext) and the soname are unchanged. Rewrite the
  now-stale PATH_MAX justification in src/lock.h's feature-test-macro
  comment.
- Fail the configure with a FATAL_ERROR, not a warning, when
  AKERR_USE_STDLIB=OFF and AKERR_THREADS would resolve to pthread,
  naming -DAKERR_THREADS=none as the fix.
- Keep the generated errno table (errno.c) out of the OFF build; skip
  the 'errno --list' shellout in scripts/generrno.sh under OFF and
  stamp AKERR_LAST_ERRNO_VALUE from a new fallback cache variable
  (default 133, Linux's EHWPOISON) instead.
- Update docs/building.md: drop the known-defect paragraph, fix
  sprintf -> snprintf, add size_t, drop PATH_MAX, and document the new
  options and the exit-code change.
- Guard tests/err_errno.c's registered-name assertion behind
  AKERR_USE_STDLIB: akerr_init_errno() is not called when it is OFF.
- Add a CI job that configures/builds/tests AKERR_USE_STDLIB=OFF with
  AKERR_THREADS=none, plus a compile-only -nostdinc -ffreestanding
  check of tests/freestanding_fixture.c against the generated header.

Bump the project version to 2.0.2 (no ABI break: soname and struct
layout are unchanged).

Verified locally: OFF+none configures and builds clean with all tests
passing; ON and the plain default build both build and pass their full
test suites (37/37); OFF with the default/auto thread backend fails
configure with a message naming -DAKERR_THREADS=none; sizeof(akerr_ErrorContext)
is unchanged (37296 bytes, fname/function still 4096 each) versus the
pre-change tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:30:40 -04:00
2026-07-29 17:42:56 -04:00
2026-08-04 11:24:30 -04:00

Summary

This library provides a TRY/CATCH style exception handling mechanism for C.

build badge

Why?

There is nothing wrong with C as it is. This library does not claim to fix some problem with C.

Instead, this library implements a pragmatic and stylistic choice to assist the programmer in better handling errors in their programs. Vanilla C provides everything you need to do this out of the box, but this library makes it easier to avoid pointing certain guns at your foot, and when you do, it provides better context with those errors to help you more quickly recover.

Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.

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

Documentation

Document What it answers
docs/architecture.md What an error context is, how one travels up the call stack, and what the macros build
docs/usage.md The macro reference: ATTEMPT/CLEANUP/PROCESS/FINISH, CATCH, FAIL_*, PASS, HANDLE, SUCCEED_RETURN
docs/status-codes.md Defining your own status codes, and reserving a range so two libraries cannot collide
docs/uncaught-errors.md AKERR_NOIGNORE, what a stack trace looks like, and how to read one
docs/exit-status.md Why you call akerr_exit() and never exit(), and how to replace the unhandled-error handler
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 Configure options, the generated header, and building without stdlib
UPGRADING.md What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate
TODO.md The reasoning behind decisions and measurements. Outstanding work is in the issue tracker.

Installation

cmake -S . -B build
cmake --build build
cmake --install build

The library depends on stdlib and on POSIX threads. Both are optional at the cost of some functionality — see docs/building.md for -DAKERR_USE_STDLIB=OFF and docs/thread-safety.md for -DAKERR_THREADS=none.

Setting up your project

Include it

#include <akerror.h>

Link the library directly, or

cc -lakerror

Using pkg-config, or

pkg-config akerror --cflags
pkg-config akerror --ldflags

Using cmake:

find_package(akerror REQUIRED)
pkg_check_modules(akerror REQUIRED akerror)
target_link_libraries(YOUR_TARGET PRIVATE akerror::akerror)

Using this project as a submodule with cmake:

add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)

target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)

Quickstart

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.

#include <akerror.h>
#include <stdio.h>

/* Fails with a message; the caller finds out what and where. */
static akerr_ErrorContext AKERR_NOIGNORE *open_config(const char *path, FILE **dest)
{
    PREPARE_ERROR(errctx);

    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);
}

int main(int argc, char **argv)
{
    FILE *config = NULL;

    PREPARE_ERROR(errctx);
    ATTEMPT {
	FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE,
			"usage: %s <config>", 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;
}

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.

Three things worth knowing before you write much more than that:

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 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. 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 for all three, what was removed, how to migrate, the capacity limits and how to raise them, and the thread-safety rules.

Description
C error handling library
Readme 1.2 MiB
Languages
C 72.3%
Python 15.2%
CMake 10%
Shell 2.5%