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 <akerror.h> 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) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:20:28 -04:00
parent b014eb2360
commit 9b124f2e27
19 changed files with 205 additions and 15 deletions

View File

@@ -39,10 +39,8 @@ add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
# Reserve error-name table entries for libakgl-specific status codes. # libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
target_compile_definitions(akerror PUBLIC # libakgl claims its status codes at runtime in akgl_heap_init() instead.
AKERR_MAX_ERR_VALUE=256
)
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE) set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
@@ -102,6 +100,7 @@ add_library(akgl SHARED
src/assets.c src/assets.c
src/character.c src/character.c
src/draw.c src/draw.c
src/error.c
src/game.c src/game.c
src/controller.c src/controller.c
src/heap.c src/heap.c
@@ -128,6 +127,7 @@ set(AKGL_TEST_SUITES
bitmasks bitmasks
character character
controller controller
error
game game
heap heap
json_helpers json_helpers

View File

@@ -6,6 +6,27 @@
#ifndef _ERROR_H_ #ifndef _ERROR_H_
#define _ERROR_H_ #define _ERROR_H_
#include <akerror.h>
/*
* 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. // 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. // 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 // 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 \ #define RESTORE_GCC_WARNINGS \
_Pragma("GCC diagnostic pop") _Pragma("GCC diagnostic pop")
#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18) // libakerror reserves statuses 0-255 for the host's errno values and its own
#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19) // AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20) // These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21) // so that a libc which grows an errno cannot move them out from under us.
#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22) //
// 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_ #endif // _ERROR_H_

24
src/error.c Normal file
View File

@@ -0,0 +1,24 @@
/**
* @file error.c
* @brief Implements the error subsystem: claims and names the libakgl status band.
*/
#include <akerror.h>
#include <akgl/error.h>
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);
}

View File

@@ -21,6 +21,7 @@
#include <akgl/staticstring.h> #include <akgl/staticstring.h>
#include <akgl/iterator.h> #include <akgl/iterator.h>
#include <akgl/physics.h> #include <akgl/physics.h>
#include <akgl/error.h>
#include <akgl/SDL_GameControllerDB.h> #include <akgl/SDL_GameControllerDB.h>
SDL_Window *window = NULL; SDL_Window *window = NULL;
@@ -55,6 +56,10 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
int i = 0; int i = 0;
PREPARE_ERROR(e); 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); strncpy((char *)&game.libversion, AKGL_VERSION, 32);
game.gameStartTime = SDL_GetTicksNS(); game.gameStartTime = SDL_GetTicksNS();
game.lastIterTime = game.gameStartTime; game.lastIterTime = game.gameStartTime;

View File

@@ -24,11 +24,6 @@ akerr_ErrorContext *akgl_heap_init()
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int i = 0; 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()); PASS(errctx, akgl_heap_init_actor());
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) { for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite)); memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));

View File

@@ -903,6 +903,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_registry_init_actor()); CATCH(errctx, akgl_registry_init_actor());
CATCH(errctx, akgl_registry_init_sprite()); CATCH(errctx, akgl_registry_init_sprite());
CATCH(errctx, akgl_registry_init_spritesheet()); CATCH(errctx, akgl_registry_init_spritesheet());

View File

@@ -195,6 +195,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {

View File

@@ -532,6 +532,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
FAIL_ZERO_BREAK( FAIL_ZERO_BREAK(
errctx, errctx,
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD), SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),

102
tests/error.c Normal file
View File

@@ -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 <string.h>
#include <akerror.h>
#include <akgl/error.h>
#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);
}

View File

@@ -402,6 +402,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());

View File

@@ -361,6 +361,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());

View File

@@ -347,6 +347,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, load_fixture()); CATCH(errctx, load_fixture());

View File

@@ -722,6 +722,7 @@ int main(void)
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, akgl_heap_init()); CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init()); CATCH(errctx, akgl_registry_init());
CATCH(errctx, akgl_registry_init_properties()); CATCH(errctx, akgl_registry_init_properties());

View File

@@ -87,6 +87,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_registry_init_creation_failures()); CATCH(errctx, test_akgl_registry_init_creation_failures());
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {

View File

@@ -187,6 +187,7 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

View File

@@ -133,6 +133,7 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
printf("test_fresh_heap_gives_string ....\n"); printf("test_fresh_heap_gives_string ....\n");
test_fresh_heap_gives_strings(); test_fresh_heap_gives_strings();
reset_string_heap(); reset_string_heap();

View File

@@ -414,6 +414,7 @@ int main(void)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
gamemap = &_akgl_gamemap; gamemap = &_akgl_gamemap;
renderer = &_akgl_renderer; renderer = &_akgl_renderer;
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest"); SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");

View File

@@ -310,6 +310,7 @@ int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
ATTEMPT { ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_rectangle_points_nullpointers()); CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math()); CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers()); CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());