From 9b124f2e27d14635a73791566316b8aee688dbdd Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Thu, 30 Jul 2026 22:20:28 -0400 Subject: [PATCH] Migrate to the libakerror 1.0.0 status registry libakerror 1.0.0 replaced the consumer-sized status-name array with a private registry and made status-code ownership explicit and enforced. AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, and the registry entry points raise akerr_ErrorContext * instead of returning int. See deps/libakerror/UPGRADING.md. The break was not only source-level. libakgl's codes sat at AKERR_LAST_ERRNO_VALUE + 18 through + 22, and 1.0.0 claimed exactly those five offsets for its own AKERR_STATUS_* registry codes, so every AKGL_ERR_* was aliasing a libakerror status. HANDLE(e, AKGL_ERR_LOGICINTERRUPT) at physics.c:222 would have swallowed a foreign-name refusal. - Move the band to AKERR_FIRST_CONSUMER_STATUS (256) as fixed offsets, so a libc that grows an errno cannot move the codes, and add AKGL_ERR_OWNER, AKGL_ERR_LIMIT and AKGL_ERR_COUNT to describe it. - Reserve the range and register the names through the owned entry points, PASS-ing each: these are AKERR_NOIGNORE, and the old akerr_name_for_status calls discarded failure silently. - Drop the AKERR_MAX_ERR_VALUE=256 compile definition. - Guard on AKERR_FIRST_CONSUMER_STATUS in include/akgl/error.h, which now includes so the guard is reliable. The embedded build is fine, but the find_package path can pick up a stale installed header, and 1.0.0 has an soname, so that pairing is an ABI mismatch rather than a compile problem. Same guard libakstdlib already carries. Registration also moves out of akgl_heap_init into a new akgl_error_init in src/error.c. It was in the heap pool's initializer only because that was the first thing akgl_game_init called, and the upgrade turned five fire-and-forget name calls into a library-wide ownership claim that can fail. That placement was hiding a defect: game.c raises AKGL_ERR_SDL when SDL_CreateMutex fails, five lines before akgl_heap_init ran, so the earliest error path in the library was guaranteed to print "Unknown Error". akgl_error_init is now the first statement in akgl_game_init. Callers that drive subsystems directly must call akgl_error_init first; it is idempotent, so ordering it precisely is not required. The eleven test suites that relied on akgl_heap_init to name their statuses now call it explicitly, or their failure messages would have degraded to "Unknown Error". Add tests/error.c: assert every code reads back its registered name, that the name table and AKGL_ERR_COUNT agree, that a foreign owner is refused with AKERR_STATUS_NAME_FOREIGN and AKERR_STATUS_RANGE_OVERLAP, and that repeating the init is a no-op. That last one is a live constraint, not a triviality -- libakerror treats only an identical reservation as a repeat, so a subset or superset raises. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 8 ++-- deps/libakerror | 2 +- include/akgl/error.h | 62 +++++++++++++++++++++++--- src/error.c | 24 ++++++++++ src/game.c | 5 +++ src/heap.c | 5 --- tests/actor.c | 1 + tests/character.c | 1 + tests/controller.c | 1 + tests/error.c | 102 +++++++++++++++++++++++++++++++++++++++++++ tests/game.c | 1 + tests/heap.c | 1 + tests/json_helpers.c | 1 + tests/physics.c | 1 + tests/registry.c | 1 + tests/sprite.c | 1 + tests/staticstring.c | 1 + tests/tilemap.c | 1 + tests/util.c | 1 + 19 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 src/error.c create mode 100644 tests/error.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e9dbcf2..b34a016 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,10 +39,8 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL) -# Reserve error-name table entries for libakgl-specific status codes. -target_compile_definitions(akerror PUBLIC - AKERR_MAX_ERR_VALUE=256 -) +# libakerror 1.0.0 sizes its own status-name registry; consumers no longer do. +# libakgl claims its status codes at runtime in akgl_heap_init() instead. set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE) @@ -102,6 +100,7 @@ add_library(akgl SHARED src/assets.c src/character.c src/draw.c + src/error.c src/game.c src/controller.c src/heap.c @@ -128,6 +127,7 @@ set(AKGL_TEST_SUITES bitmasks character controller + error game heap json_helpers diff --git a/deps/libakerror b/deps/libakerror index 0c0d812..5ff8790 160000 --- a/deps/libakerror +++ b/deps/libakerror @@ -1 +1 @@ -Subproject commit 0c0d81249fc6dcb35c6126dfb8265beeea697d69 +Subproject commit 5ff87908e7b68ab2dc328b55ea89585272411ff9 diff --git a/include/akgl/error.h b/include/akgl/error.h index 73df5eb..c2c081c 100644 --- a/include/akgl/error.h +++ b/include/akgl/error.h @@ -6,6 +6,27 @@ #ifndef _ERROR_H_ #define _ERROR_H_ +#include + +/* + * libakerror 1.0.0 is the floor. That release moved the status-name table into a + * private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the + * registry entry points raise akerr_ErrorContext * instead of returning int, and + * the library gained an soname -- so a translation unit that pairs this header + * with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem. + * + * libakerror publishes no version macro, so this feature-tests on + * AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a + * version number that does not exist. Without the guard an embedded build is + * fine but a stale installed header fails much further in, on the AKGL_ERR_* + * codes below and again inside src/heap.c. + * + * See deps/libakerror/UPGRADING.md. + */ +#ifndef AKERR_FIRST_CONSUMER_STATUS +#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror." +#endif + // This macro is used to silence warnings on string concatenation operations that may fail. // e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH. // We have to draw a line in the sand somewhere or we will just let our buffers grow forever @@ -17,10 +38,41 @@ #define RESTORE_GCC_WARNINGS \ _Pragma("GCC diagnostic pop") -#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18) -#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19) -#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20) -#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21) -#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22) +// libakerror reserves statuses 0-255 for the host's errno values and its own +// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward. +// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE +// so that a libc which grows an errno cannot move them out from under us. +// +// akgl_error_init() reserves this whole band in one call and registers a name +// for every code below. Add a code here and you must name it there, or it +// prints as "Unknown Error" in every stack trace that carries it. +#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 */ + +// One past the last libakgl status. The reservation is all-or-nothing -- a +// subset or superset of an existing one is refused -- so this must stay one +// past the highest code above. +#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5) +#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE) + +/** + * @brief Claim the libakgl status band and register a name for every code in it. + * + * Call this before anything else in libakgl. Every other subsystem raises + * AKGL_ERR_* codes, and a code raised before this runs carries no name into its + * stack trace. Repeating the call is a no-op, so a program that cannot order its + * initialization precisely may call it more than once. + * + * @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band. + * @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left. + * @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full. + */ +akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void); #endif // _ERROR_H_ diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..fcc85dd --- /dev/null +++ b/src/error.c @@ -0,0 +1,24 @@ +/** + * @file error.c + * @brief Implements the error subsystem: claims and names the libakgl status band. + */ + +#include + +#include + +akerr_ErrorContext *akgl_error_init(void) +{ + PREPARE_ERROR(errctx); + // Claim the whole band before naming anything in it: libakerror refuses a + // name for a status we do not own. Any collision propagates to the caller + // -- another component owning part of our range is an initialization + // failure, not a warning. + PASS(errctx, akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, AKGL_ERR_OWNER)); + PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_SDL, "SDL Error")); + PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_REGISTRY, "Registry Error")); + PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_HEAP, "Heap Error")); + PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error")); + PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt")); + SUCCEED_RETURN(errctx); +} diff --git a/src/game.c b/src/game.c index ea36527..71733c3 100644 --- a/src/game.c +++ b/src/game.c @@ -21,6 +21,7 @@ #include #include #include +#include #include SDL_Window *window = NULL; @@ -55,6 +56,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init() int i = 0; PREPARE_ERROR(e); + // First, before anything that can raise: everything below reports through + // AKGL_ERR_* codes, and a code raised before its name is registered prints + // as "Unknown Error" in the stack trace the caller is left holding. + PASS(e, akgl_error_init()); strncpy((char *)&game.libversion, AKGL_VERSION, 32); game.gameStartTime = SDL_GetTicksNS(); game.lastIterTime = game.gameStartTime; diff --git a/src/heap.c b/src/heap.c index 87a4794..001ab9a 100644 --- a/src/heap.c +++ b/src/heap.c @@ -24,11 +24,6 @@ akerr_ErrorContext *akgl_heap_init() { PREPARE_ERROR(errctx); int i = 0; - akerr_name_for_status(AKGL_ERR_SDL, "SDL Error"); - akerr_name_for_status(AKGL_ERR_REGISTRY, "Registry Error"); - akerr_name_for_status(AKGL_ERR_HEAP, "Heap Error"); - akerr_name_for_status(AKGL_ERR_BEHAVIOR, "Behavior Error"); - akerr_name_for_status(AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"); PASS(errctx, akgl_heap_init_actor()); for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) { memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite)); diff --git a/tests/actor.c b/tests/actor.c index 085ea34..2bdcc95 100644 --- a/tests/actor.c +++ b/tests/actor.c @@ -903,6 +903,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, akgl_registry_init_actor()); CATCH(errctx, akgl_registry_init_sprite()); CATCH(errctx, akgl_registry_init_spritesheet()); diff --git a/tests/character.c b/tests/character.c index 2cb4412..528f893 100644 --- a/tests/character.c +++ b/tests/character.c @@ -195,6 +195,7 @@ int main(void) { PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { diff --git a/tests/controller.c b/tests/controller.c index d51bf0b..4a3e0e7 100644 --- a/tests/controller.c +++ b/tests/controller.c @@ -532,6 +532,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); FAIL_ZERO_BREAK( errctx, SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD), diff --git a/tests/error.c b/tests/error.c new file mode 100644 index 0000000..2e255c3 --- /dev/null +++ b/tests/error.c @@ -0,0 +1,102 @@ +/** + * @file error.c + * @brief Unit tests for the libakgl status band: reservation, ownership and names. + * + * The libakerror registry is process-global, so these tests assert against + * whatever akgl_error_init() left behind rather than building their own state. + */ + +#include +#include + +#include + +#include "testutil.h" + +/** + * @brief akgl_error_init() must own the libakgl status band and name every code in it. + * + * A code whose name never registered degrades to "Unknown Error" in every stack + * trace that carries it, and a band we never reserved is one another component + * can name out from under us. Both stay silent until something has already gone + * wrong, so assert them directly rather than waiting to read a useless trace. + */ +akerr_ErrorContext *test_error_init_owns_the_status_band(void) +{ + PREPARE_ERROR(e); + static const struct { + int status; + const char *name; + } expected[] = { + { AKGL_ERR_SDL, "SDL Error" }, + { AKGL_ERR_REGISTRY, "Registry Error" }, + { AKGL_ERR_HEAP, "Heap Error" }, + { AKGL_ERR_BEHAVIOR, "Behavior Error" }, + { AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" } + }; + bool named = true; + int i = 0; + + ATTEMPT { + CATCH(e, akgl_error_init()); + + TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT, + "the libakgl status band holds %d codes but %d are named here", + AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0]))); + + for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) { + TEST_ASSERT_FLAG(named, + strcmp(akerr_name_for_status(expected[i].status, NULL), + expected[i].name) == 0); + } + TEST_ASSERT(e, named, + "akgl_error_init did not register the expected name for every AKGL_ERR_* code"); + + // The reservation is what makes those names ours. Without it the + // registrations above would still succeed for anyone who asked. + TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN, + akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"), + "a foreign owner was allowed to rename a libakgl status"); + TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP, + akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"), + "a foreign owner was allowed to reserve the libakgl status band"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +/** + * @brief Calling akgl_error_init() twice must be a no-op, not a self-collision. + * + * Nothing in libakgl orders initialization for an embedding program, so a second + * call has to be harmless. libakerror only treats an *identical* reservation as + * a repeat -- a subset or superset raises -- which makes this a real constraint + * on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality. + */ +akerr_ErrorContext *test_error_init_is_idempotent(void) +{ + PREPARE_ERROR(e); + + ATTEMPT { + TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed"); + TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed"); + TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0, + "re-running akgl_error_init lost the name for AKGL_ERR_SDL"); + } CLEANUP { + } PROCESS(e) { + } FINISH(e, true); + SUCCEED_RETURN(e); +} + +int main(void) +{ + PREPARE_ERROR(errctx); + + ATTEMPT { + CATCH(errctx, test_error_init_owns_the_status_band()); + CATCH(errctx, test_error_init_is_idempotent()); + } CLEANUP { + } PROCESS(errctx) { + } FINISH_NORETURN(errctx); +} diff --git a/tests/game.c b/tests/game.c index c98941a..ddaaccc 100644 --- a/tests/game.c +++ b/tests/game.c @@ -402,6 +402,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init()); diff --git a/tests/heap.c b/tests/heap.c index 042aa65..5a98635 100644 --- a/tests/heap.c +++ b/tests/heap.c @@ -361,6 +361,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init()); diff --git a/tests/json_helpers.c b/tests/json_helpers.c index b3cc2fa..5828e77 100644 --- a/tests/json_helpers.c +++ b/tests/json_helpers.c @@ -347,6 +347,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init()); CATCH(errctx, load_fixture()); diff --git a/tests/physics.c b/tests/physics.c index 2087c96..5f5372f 100644 --- a/tests/physics.c +++ b/tests/physics.c @@ -722,6 +722,7 @@ int main(void) SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init_properties()); diff --git a/tests/registry.c b/tests/registry.c index 44b353d..0849630 100644 --- a/tests/registry.c +++ b/tests/registry.c @@ -87,6 +87,7 @@ int main(void) { PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, test_akgl_registry_init_creation_failures()); } CLEANUP { } PROCESS(errctx) { diff --git a/tests/sprite.c b/tests/sprite.c index d03f935..d04a866 100644 --- a/tests/sprite.c +++ b/tests/sprite.c @@ -187,6 +187,7 @@ int main(void) PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); renderer = &_akgl_renderer; SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); diff --git a/tests/staticstring.c b/tests/staticstring.c index da94cca..cb017e9 100644 --- a/tests/staticstring.c +++ b/tests/staticstring.c @@ -133,6 +133,7 @@ int main(void) PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); printf("test_fresh_heap_gives_string ....\n"); test_fresh_heap_gives_strings(); reset_string_heap(); diff --git a/tests/tilemap.c b/tests/tilemap.c index 5ba93b9..2b2af2e 100644 --- a/tests/tilemap.c +++ b/tests/tilemap.c @@ -414,6 +414,7 @@ int main(void) PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); gamemap = &_akgl_gamemap; renderer = &_akgl_renderer; SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); diff --git a/tests/util.c b/tests/util.c index ff9f6b7..ed931cf 100644 --- a/tests/util.c +++ b/tests/util.c @@ -310,6 +310,7 @@ int main(void) { PREPARE_ERROR(errctx); ATTEMPT { + CATCH(errctx, akgl_error_init()); CATCH(errctx, test_akgl_rectangle_points_nullpointers()); CATCH(errctx, test_akgl_rectangle_points_math()); CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());