Give every exported function a declaration, and check that it stays that way

Closes internal-consistency items 7 through 15. Nineteen non-static functions
were in the ABI with no declaration anywhere, so no consumer could call them
and any consumer could collide with them.

The four gamepad_handle_* functions are the ones that mattered: controller.h
declared akgl_controller_handle_button_down and three siblings that did not
exist, so anything compiled against the header alone failed to link. The
definitions carry the declared names now, which also closes Defects -> Known
and still open item 10, and their documentation moved to the header.

The rest are either declared under a "part of the internal API" block --
akgl_game_save_actors and akgl_game_load_versioncmp, which tests/game.c had to
declare for itself, plus six tilemap loader helpers the untested-loader work
wants to reach -- or static, which is what the four save iterators and
load_objectnamemap should always have been. akgl_path_relative_from is deleted:
declared nowhere, called from nowhere, never wrote its output, and leaked a
pooled string on every call, so it closes Known and still open item 4 and item
40 by ceasing to exist.

scripts/check_api_surface.sh keeps it closed. It reads the built library's
dynamic symbol table and every public header with comments stripped, and fails
on an exported akgl_* symbol that is declared nowhere. Stripping comments is
the whole point -- four of these were mentioned in controller.h prose, which is
how they went unnoticed.

The pool-size ceilings are defined once, in heap.h, so the #ifndef override
hook fires for the first time; actor.h, sprite.h and character.h were defining
the same four unconditionally from headers heap.h includes above its own guard.
tests/header_pool_override.c fails the compile if that regresses.

Also here: (void) rather than () on the twelve no-argument entry points,
AKERR_NOIGNORE only on declarations, static helpers with the akgl_ prefix
dropped, and the six parameter-name mismatches. akgl_get_json_with_default had
its two contexts swapped rather than merely misspelled -- the incoming one was
`err` and its own was `e`, which is the name reserved for an incoming one.

24/24 pass, reindent --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 23:44:38 -04:00
parent 9924d74dcc
commit 3a262bee54
29 changed files with 676 additions and 620 deletions

86
scripts/check_api_surface.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# Every function with external linkage must be declared in a header.
#
# A non-static function that appears in no header is still in the ABI but is
# unreachable by callers, and it collides with any consumer that happens to pick
# the same name. libakgl shipped nineteen of those. The rule is in AGENTS.md
# under "API Surface"; this is what enforces it.
#
# Usage: check_api_surface.sh <libakgl.so> <header-dir> [<header-dir> ...]
#
# Exits 0 when every exported akgl_* symbol is declared, 1 otherwise, and 2 when
# it cannot do its job -- a missing library or no nm.
set -u
if [ "$#" -lt 2 ]; then
echo "usage: $0 <libakgl.so> <header-dir> [<header-dir> ...]" >&2
exit 2
fi
LIBRARY="$1"
shift
if ! command -v nm >/dev/null 2>&1; then
echo "check_api_surface: nm is not available; skipping" >&2
exit 2
fi
if [ ! -f "${LIBRARY}" ]; then
echo "check_api_surface: no such library: ${LIBRARY}" >&2
exit 2
fi
DECLARATIONS="$(mktemp)"
trap 'rm -f "${DECLARATIONS}"' EXIT
# Comments are stripped first. Several of these names appear in prose -- the
# whole point of the check is that being *mentioned* in a header is not the same
# as being declared in one, and that distinction is exactly what went wrong with
# the gamepad_handle_* handlers.
for dir in "$@"; do
[ -d "${dir}" ] || continue
find "${dir}" -name '*.h' -print0 |
xargs -0 --no-run-if-empty cat |
sed -e 's://.*::' |
awk 'BEGIN { incomment = 0 }
{
line = $0
while ( 1 ) {
if ( incomment ) {
idx = index(line, "*/")
if ( idx == 0 ) { line = ""; break }
line = substr(line, idx + 2); incomment = 0
} else {
idx = index(line, "/*")
if ( idx == 0 ) { break }
printf "%s", substr(line, 1, idx - 1)
line = substr(line, idx + 2); incomment = 1
}
}
print line
}' >> "${DECLARATIONS}"
done
UNDECLARED=""
for symbol in $(nm -D --defined-only "${LIBRARY}" | awk '$2 == "T" { print $3 }' | grep '^akgl_' | sort); do
if ! grep -qE "(^|[^A-Za-z0-9_])${symbol}[^A-Za-z0-9_]" "${DECLARATIONS}"; then
UNDECLARED="${UNDECLARED} ${symbol}"
fi
done
if [ -n "${UNDECLARED}" ]; then
echo "check_api_surface: exported but declared in no header:" >&2
for symbol in ${UNDECLARED}; do
echo " ${symbol}" >&2
done
echo "" >&2
echo "Declare each one in the header for its subsystem, or make it static." >&2
echo "A function exposed only so tests can reach it goes under the existing" >&2
echo "\"part of the internal API\" comment block." >&2
exit 1
fi
echo "check_api_surface: every exported akgl_* symbol is declared"
exit 0