docs/22-ui.md covers the clay-backed UI: what clay owns against what libakgl owns, bring-up and the arena refusal contract, the frame bracket and where it sits in a real frame, the consumed-event rules (with the one-frame-stale hit test stated plainly), the three widgets, menus across keyboard, gamepad and mouse, writing raw CLAY() with an application-owned press edge, images and styles, and how layout errors surface from frame_end with a captured trace. Every listing is a checked block: one linked-and-run example with pinned output, eight excerpts quoting examples/uidemo and examples/jrpg/textbox.c, and the chapter figure is a frame out of uidemo via docs_game_figures. The chapter owes and pays the comparison the tutorials earn: the dialog widget's default style reproduces the JRPG textbox palette, the two listings sit side by side, and the trade is stated -- 125 lines you own completely against one call that costs you the subsystem. Neither is deprecated; chapter 21 keeps teaching the hand-rolled panel on purpose. The appendix moves to 23 (links in chapters 4, 5, 6, 7, 13 and the TOC updated) and gains the ui.h status cross-reference, the AKGL_UI_* limits table, and the new draw primitives' rows. Its status-band figures were stale at COUNT 5 / LIMIT 261 while the band already held six codes; now correct at 7 / 263. Chapter 4 gains AKGL_ERR_UI (262) in the excerpt, the table and the name list. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
17 KiB
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 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 seven starting there, under the owner string "libakgl":
#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 */
#define AKGL_ERR_UI (AKGL_ERR_BASE + 6) /**< The UI subsystem refused, or clay reported a layout error; the message says which */
AKGL_ERR_LIMIT is not a status code. It is AKGL_ERR_BASE + 7, 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 an eighth 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 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 |
AKGL_ERR_UI |
262 | The UI subsystem refused, or clay reported a layout error | akgl_ui_* — lifecycle misuse (init twice, a widget outside the frame bracket), an arena or table at its ceiling, and layout errors surfacing from akgl_ui_frame_end |
The message says which; the ceilings are in Chapter 23. See Chapter 22 |
akgl_error_init reserves the band all-or-nothing and registers a name for each of the
seven: "SDL Error", "Registry Error", "Heap Error", "Behavior Error",
"Logic Interrupt", "Collision Error", "UI 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 23 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 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_saveterminates the game rather than reachingakgl_game_save's return value.game.hsays so onakgl_game_save_actors. - If you want a different outcome, point libakerror's
akerr_handler_unhandled_errorfunction pointer at your own handler — and callakerr_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_BASEdoes not exist and the failure lands much later, insidesrc/heap.c.AKERR_EXIT_STATUS_UNREPRESENTABLE— added in 2.0.1 withakerr_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.
#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:
akgl_error_init()is the first call, before anything that can raise.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 touchkeyerragain.- A
speedkey that is absent is not an error.akgl_get_json_integer_valueraisesAKERR_KEY,akgl_get_json_with_defaulthandles it and writes 4. Aspeedkey that is present as a string is an error:AKERR_TYPEis not defaulted and comes back asunhandled. mainends inFINISH_NORETURN. That is what gets youakerr_exit()and a truthful exit status without writing the trap from Table 3.
Where to go next
- Chapter 5 —
AKGL_ERR_HEAP, and why it is almost always a missing release. - Chapter 6 — the name lookups behind
AKERR_KEYandAKERR_NULLPOINTER. - Chapter 23 — which function raises what.
deps/libakerror/README.md— the protocol itself, and the exit-status discussion.- The generated Doxygen reference — every function's own
@throwslist.