Files
libakgl/docs/04-errors.md
Andrew Kesterson 4e32328681
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 21s
libakgl CI Build / performance (push) Failing after 21s
libakgl CI Build / memory_check (push) Failing after 17s
libakgl CI Build / mutation_test (push) Failing after 19s
Remove the corner helpers akgl_collide_rectangles no longer uses
akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and
akgl_RectanglePoints go. They were the intermediate form of an implementation
that changed: akgl_collide_rectangles was eight corner-containment tests built on
them, and it has been four span comparisons since the cross-case fix. Nothing
outside tests/ called either function, and a point-in-rectangle test is four
comparisons a caller can write without a struct conversion in front of them.

akgl_collide_rectangles stays. It has two correct callers in the sidescroller
asking a game-level overlap question -- a coin, a hazard, from an updatefunc --
where a bool is the whole answer and a proxy plus a narrowphase call would be
computing a normal nothing reads. TODO.md records the split rather than leaving
it to be rediscovered.

Public API removal, so 194 exported akgl_ symbols against 196, and the manual's
counts move with them. The perf suite loses its rectangle_points row; the
all-pairs sweep stays as the control it is now labelled, and PERFORMANCE.md says
what 0.8.0 measured against it -- 188.5 us for 32,640 pairs at 256 actors, where
a whole step with collision attached is 54.1 us doing strictly more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 08:09:42 -04:00

272 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 04. Errors and status codes
Every one of libakgl's exported functions returns `akerr_ErrorContext AKERR_NOIGNORE *`.
You cannot read a single example in this manual until you can read that return value, which
is why this chapter comes before any subsystem.
## The protocol is libakerror's
`ATTEMPT` / `CLEANUP` / `PROCESS` / `HANDLE` / `HANDLE_GROUP` / `FINISH`, and the `PASS`,
`CATCH`, `IGNORE`, `FAIL_*` and `SUCCEED_RETURN` macros, all belong to **libakerror**.
They are documented by the project that owns them, in `deps/libakerror/README.md`
"Library Architecture", "Lifecycle of an error in the AKError library", and "Exit status".
This manual does not restate them, because a copy here would be wrong the day libakerror
changes and nothing in this repository would notice.
What is genuinely libakgl's, and is written down nowhere else, is **which statuses these
193 functions raise and what each one means here**. That is the three tables below.
The house rules for *writing* code against the protocol — never a `*_RETURN` inside an
`ATTEMPT`, never a bare `return` out of a `HANDLE`, `CLEANUP` before `PROCESS`, an
`ATTEMPT` inside the loop rather than a `CATCH` inside one — are in `AGENTS.md` under
"Error-Handling Protocol", and two of them are enforced by the `error_protocol` test.
## Table 1 — libakgl's own status codes
libakerror reserves statuses 0255 for the host's `errno` values and its own `AKERR_*`
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
libakgl claims a band of six starting there, under the owner string `"libakgl"`:
```c excerpt=include/akgl/error.h
#define AKGL_ERR_OWNER "libakgl"
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
```
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 6`, one past the last
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
arm — it would catch nothing, and if a seventh real code is ever added it would silently
start catching that instead.
| Code | Value | Means | Raised by | What the caller does |
|---|---|---|---|---|
| `AKGL_ERR_SDL` | 256 | An SDL call failed; the message carries `SDL_GetError()` | Around forty sites — anything touching a window, texture, renderer, mixer or the state mutex | Usually fatal at startup. Check the driver and the asset path |
| `AKGL_ERR_REGISTRY` | 257 | A name lookup against a registry failed | **One site only**: `akgl_controller_default`, when `actorname` is not in `AKGL_REGISTRY_ACTOR` | Bind the control map *after* the actor is initialized |
| `AKGL_ERR_HEAP` | 258 | A pool has no free slot | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** See [Chapter 5](05-the-heap.md) before raising `AKGL_MAX_HEAP_*` |
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
| `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) |
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
six: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
`"Logic Interrupt"`, `"Collision Error"`. Those names are what a stack trace prints.
Two rows deserve more than a cell.
**`AKGL_ERR_REGISTRY` is rarer than its name suggests.** The obvious candidates do not
raise it. `akgl_actor_initialize` raises `AKERR_KEY` when the registry write fails, and
`akgl_actor_set_character` raises `AKERR_NULLPOINTER` when the named character is not in
`AKGL_REGISTRY_CHARACTER`. If you are writing a `HANDLE` arm for "that name was not
registered", `AKERR_NULLPOINTER` and `AKERR_KEY` are the statuses you will actually see.
**`AKGL_ERR_LOGICINTERRUPT` only works from one place.** `akgl_physics_simulate` wraps each
actor's step in an `ATTEMPT` and calls `actor->movementlogicfunc` through `CATCH`, with a
`HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT)` arm that does nothing — so a raise from your
movement logic skips that actor's gravity, drag and move for this step and the loop carries
on to the next actor. The backend's own `gravity` and `move` are called through `PASS` in
the same block, and `PASS` returns out of the function. **A backend hook that raises
`AKGL_ERR_LOGICINTERRUPT` aborts the whole physics step**, leaving every remaining actor
unsimulated and `gravity_time` unadvanced. Raise it from `movementlogicfunc` and nowhere
else.
## Table 2 — libakerror statuses libakgl raises, and what they mean here
The statuses are libakerror's; their libakgl meaning is not. This table was built by
grepping every `FAIL_*` and `HANDLE*` site in `src/`, not by reading header prose.
| Status | What it means when a libakgl function raises it |
|---|---|
| `AKERR_NULLPOINTER` | By a wide margin the most common — roughly six of every seven raise sites in `src/`. A required pointer argument was `NULL` — *or* a required field was empty (`akgl_game.name`/`.version`/`.uri`), *or* a name was looked up and not found. `akgl_actor_set_character` and `akgl_get_property` both use it for "absent", and `registry_create` uses it for "SDL could not allocate the property set" |
| `AKERR_KEY` | **A key is absent, or a registry write was refused.** A missing JSON object member (every `akgl_get_json_*_value`), a character with no sprite for a state, a physics backend name that is neither `null` nor `arcade`, a failed `SDL_SetPointerProperty` in `akgl_actor_initialize`. Frequently a `HANDLE` arm rather than a failure |
| `AKERR_TYPE` | **A JSON value was present but the wrong type.** All of `src/json_helpers.c` and `akgl_get_json_tilemap_property`. This is the status that separates "you did not write that key" (`AKERR_KEY`) from "you wrote it as a string and it wants a number" |
| `AKERR_OUTOFBOUNDS` | A value did not fit its destination, or an index ran off the end. `aksl_strncpy` truncation, an array index past the end of a JSON array (`akgl_get_json_array_index_*`), a ninth child on an actor, a tile past `AKGL_TILEMAP_MAX_*`, a control map id out of range |
| `AKERR_VALUE` | A value parsed but was not usable: a version string that is not semver (`akgl_game_load_versioncmp`), two surfaces that differ (`akgl_compare_sdl_surfaces`), a zero-sized destination or an overlapping copy from the `aksl_` wrappers |
| `AKERR_RELATIONSHIP` | **One site**: `akgl_actor_add_child`, when the child already has a parent. Detach it first |
| `AKERR_API` | Two distinct meanings, so read the message. (1) **The function is not implemented** — `akgl_render_2d_draw_mesh` does `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and it is reached by an ordinary-looking call. (2) **A savegame does not match this build** — `akgl_game_load` and `akgl_game_load_versioncmp` |
| `AKERR_IO` | A read or write failed, or a savegame had trailing data after its name tables. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists |
| `AKERR_EOF` | End of input. Sometimes the *desired* outcome: `require_at_eof` in `src/game.c` handles it and treats anything else as a failure |
| `AKERR_INDEX` | **libakgl never raises this.** It appears once, as a `HANDLE_GROUP` arm in `akgl_get_json_with_default`, so that a caller whose *own* code raises it still gets a default. An out-of-range JSON array index arrives as `AKERR_OUTOFBOUNDS` |
Statuses raised by the libraries underneath also reach you unchanged. `aksl_fopen` reports
`ENOENT` and `EACCES` as themselves; `aksl_fclose` reports `ENOSPC` and `EDQUOT`.
`akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`,
`AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`.
[Chapter 22](22-appendix-limits.md) has the per-function cross-reference.
## Table 3 — the exit status trap
This is arithmetic, and it is silent.
| You write | Wait status the shell sees | Why |
|---|---|---|
| `exit(AKGL_ERR_SDL)` | **0 — a clean run** | An exit status is one byte. `AKGL_ERR_SDL` is 256, and 256 & 0xFF is 0 |
| `akerr_exit(status)` | 0 → 0, 1255 → itself, anything else → 125 | 125 is `AKERR_EXIT_STATUS_UNREPRESENTABLE` |
Every suite in `tests/` once reported success on the single most common failure a library
built on SDL can have — a renderer that would not come up. `tests/character.c` aborted at
its second of four tests and was green until 0.5.0.
**You do not normally need to do anything about this.** libakerror's default unhandled-error
handler already calls `akerr_exit()`, so a `main` that ends in `FINISH_NORETURN` exits
correctly. The trap is only live if you replace libakerror's
`akerr_handler_unhandled_error` pointer with your own handler, or call `exit(3)` on a
status yourself. If you do either, call `akerr_exit()`.
## Four traps that are libakgl's, not libakerror's
### `akgl_error_init()` must run before anything that can raise
`akgl_error_init` claims the status band *and* registers the six names. A code raised
before it runs has no name in the registry, so every stack trace carrying it prints
**"Unknown Error"** instead of "Heap Error" — and the status number, 258, means nothing to
a reader.
`akgl_game_init` calls it first, before anything else that can fail — that is why the
mutex creation and the `akgl_game.name` checks sit *after* it. A host with its own startup
path, such as one binding an existing renderer with `akgl_render_2d_bind` instead of
calling `akgl_game_init`, must call `akgl_error_init` itself. Repeat calls are a no-op, so
a program that cannot order its initialization precisely may simply call it more than once.
### The SDL callbacks end in `FINISH_NORETURN`, and that exits the process
`SDL_EnumerateProperties` takes a `void`-returning callback, so a failure inside one has
nowhere to go. All six of libakgl's therefore end in `FINISH_NORETURN`, which logs the
trace and hands the context to libakerror's unhandled-error handler — **and the default
handler exits the process.** They are `akgl_registry_iterate_actor` (`src/actor.c`),
`akgl_character_state_sprites_iterate` (`src/character.c`), and the four savegame name
iterators in `src/game.c`.
A reader meets this the first time a sprite name is wrong: an actor's `renderfunc` raises
`AKERR_KEY`, the callback cannot return it, and the game exits mid-frame with a stack trace
instead of returning an error you could have handled.
Two consequences worth planning for:
- A write error part-way through `akgl_game_save` terminates the game rather than reaching
`akgl_game_save`'s return value. `game.h` says so on `akgl_game_save_actors`.
- If you want a different outcome, point libakerror's `akerr_handler_unhandled_error`
function pointer at your own handler — and call `akerr_exit()` from it, per Table 3.
Note that `akgl_game_update` does **not** go through the callback. It walks
`akgl_heap_actors` directly and propagates through `PASS`, so an error from an actor's
`updatefunc` does reach you. The callback path is the render sweep and the savegame writer.
### `akgl_get_json_with_default` hands back the context you gave it
This is the one libakgl function whose ownership rule you have to know before you write
your first loader. It takes an incoming `akerr_ErrorContext *`, and when it does not handle
that status it returns **the same pointer**, because its `FINISH(e, true)` passes `e` up.
So the context has exactly one owner at a time, and it is never both yours and its. A
`CLEANUP` block that releases your original pointer *as well as* releasing what the call
returned releases the same slot twice, and a double-released context corrupts the failure
instead of reporting it. That is not hypothetical: it is how the first draft of the
`AKERR_OUTOFBOUNDS` test in `tests/json_helpers.c` passed against the unfixed library.
`AGENTS.md` documents the shape under "Testing Guidelines"; `TODO.md` item 18 records the
history.
The correct shape: take the result into a local, `NULL` your own pointer immediately, and
decide from the local.
It defaults on three statuses — `AKERR_KEY`, `AKERR_OUTOFBOUNDS` and `AKERR_INDEX` — so
both a missing object member and a short array give you the default. It does **not** default
on `AKERR_TYPE`: a key that is present with the wrong type is a mistake in the document, not
an absent value, and it propagates.
### libakerror 2.0.1 is a hard floor, checked at compile time
`include/akgl/error.h` carries two `#error` feature tests, because libakerror publishes no
version macro and each probes the narrowest thing that a required release introduced:
- `AKERR_FIRST_CONSUMER_STATUS` — added in 1.0.0 with the private status registry. Without
it, `AKGL_ERR_BASE` does not exist and the failure lands much later, inside `src/heap.c`.
- `AKERR_EXIT_STATUS_UNREPRESENTABLE` — added in 2.0.1 with `akerr_exit()`.
Both failures are an *installed header* being stale, not a build-tree problem. 2.0.0 is an
ABI break — `akerr_next_error()` returns a context that already holds its reference, and
`__akerr_last_ignored` is thread-local — and both expand at libakgl's own call sites through
`IGNORE` and the `FAIL_*` macros. The soname is `libakerror.so.2`, so the *libraries* cannot
be mixed by accident; the header can. Rebuild and reinstall libakerror.
## One worked example
A loader reading an optional integer out of a JSON document. It is the smallest piece of
real libakgl code that exercises both halves of the chapter: a status that means "absent
and that is fine", and the ownership rule on the function that turns it into a default.
```c
#include <jansson.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/json_helpers.h>
/* Read an optional integer out of a loaded document, defaulting to 4. */
akerr_ErrorContext AKERR_NOIGNORE *read_speed(json_t *doc, int *dest)
{
akerr_ErrorContext *keyerr = NULL;
akerr_ErrorContext *unhandled = NULL;
int def = 4;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, doc, AKERR_NULLPOINTER, "NULL document");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination");
keyerr = akgl_get_json_integer_value(doc, "speed", dest);
unhandled = akgl_get_json_with_default(keyerr, &def, dest, sizeof(int));
keyerr = NULL;
if ( unhandled != NULL ) {
return unhandled;
}
SUCCEED_RETURN(errctx);
}
int main(void)
{
json_t *doc = NULL;
int speed = 0;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
doc = json_pack("{s:i}", "width", 32);
CATCH(errctx, read_speed(doc, &speed));
} CLEANUP {
if ( doc != NULL ) {
json_decref(doc);
}
} PROCESS(errctx) {
} FINISH_NORETURN(errctx);
}
```
Four things in there are libakgl-specific and worth naming:
1. **`akgl_error_init()` is the first call**, before anything that can raise.
2. **`keyerr = NULL;` on the line after the call.** Ownership passed either way — handled
or not — and the local is the only surviving reference. Nothing else in the function may
touch `keyerr` again.
3. **A `speed` key that is absent is not an error.** `akgl_get_json_integer_value` raises
`AKERR_KEY`, `akgl_get_json_with_default` handles it and writes 4. A `speed` key that is
present as a string *is* an error: `AKERR_TYPE` is not defaulted and comes back as
`unhandled`.
4. **`main` ends in `FINISH_NORETURN`.** That is what gets you `akerr_exit()` and a
truthful exit status without writing the trap from Table 3.
## Where to go next
- [Chapter 5](05-the-heap.md) — `AKGL_ERR_HEAP`, and why it is almost always a missing
release.
- [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and
`AKERR_NULLPOINTER`.
- [Chapter 22](22-appendix-limits.md) — which function raises what.
- `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion.
- The generated Doxygen reference — every function's own `@throws` list.