Add the manual: nineteen chapters and a corrected README
docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
269
docs/04-errors.md
Normal file
269
docs/04-errors.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# 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
|
||||
156 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 0–255 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 five 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 */
|
||||
```
|
||||
|
||||
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 5`, 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 sixth 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_error_init` reserves the band all-or-nothing and registers a name for each of the
|
||||
five: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
|
||||
`"Logic Interrupt"`. 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_physics_arcade_collide` and `akgl_render_2d_draw_mesh` both `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and both are reached by ordinary-looking calls. (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 21](21-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, 1–255 → 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 five 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 21](21-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.
|
||||
Reference in New Issue
Block a user