Pull latest libakerror: static TLS fix landed
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m25s
akbasic CI Build / sanitizers (push) Successful in 4m48s
akbasic CI Build / coverage (push) Successful in 4m18s
akbasic CI Build / akgl_build (push) Successful in 8m6s
akbasic CI Build / mutation_test (push) Successful in 16m49s

libakerror main now includes the fix for issue #37 (per-TU static TLS
akerr_last_ignored -> single extern definition in src/error.c, one copy
per thread instead of one per translation unit) and issue #38 (project()
now stamps 2.0.3, matching the release notes).

Bump deps/libakerror to origin/main (9249f8c) and update MAINTENANCE.md:
drop the now-stale 'IGNORE change costs' subsection and the 2.0.2/2.0.3
version-disagreement note, since both are resolved upstream.

Verified: build/basic has no TLS segment and zero -Wunused-variable
warnings for akerr_last_ignored; 114/114 tests pass.

Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-08-06 07:25:14 -04:00
parent 13f1df03ef
commit 9a22fde5ee
2 changed files with 14 additions and 38 deletions

View File

@@ -163,7 +163,7 @@ guards every dependency with `if(NOT TARGET ...)`, so a top-level build must def
`akerror::akerror` and `akstdlib::akstdlib` from `deps/libakerror` and `deps/libakstdlib`
**before** `add_subdirectory(deps/libakgl)`, or the targets are declared twice.
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.2**, whose 2.0.0
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.3**, whose 2.0.0
was a source and ABI break carrying an soname (`libakerror.so.2`). `libakstdlib` and `libakgl`
must be compiled against that header, not a 1.x one, and an installed `libakerror.so.1` must
not be picked up. The break is quiet if you get it wrong: the context behind `IGNORE` became
@@ -174,46 +174,22 @@ a mixed build leaks pool slots or frees one twice rather than failing to link.
| Submodule | Version | soname | ABI rule | Version API |
|---|---|---|---|---|
| `deps/libakerror` | 2.0.2 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
| `deps/libakerror` | 2.0.3 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
| `deps/libakstdlib` | 0.2.0 | `libakstdlib.so.0.2` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` |
| `deps/libakgl` | 0.9.0 | `libakgl.so.0.9` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
**The `libakerror` row is what `project(akerror VERSION ...)` declares, and it disagrees with
that library's own release notes**, which carry a "Release 2.0.3" section describing CI-only
changes. The version the build stamps into the soname and into
`akerrorConfigVersion.cmake` is 2.0.2; nothing consumers can observe says 2.0.3. Read the
table as the ABI and the notes as the changelog, and do not "correct" this row to 2.0.3
without the upstream `project()` bump to go with it. Reported as `libakerror` issue #38.
`project(akerror VERSION ...)` now stamps 2.0.3, matching that library's own "Release 2.0.3"
release notes. It previously disagreed — the soname and `akerrorConfigVersion.cmake` still
said 2.0.2 while the notes described 2.0.3 — but that was `libakerror` issue #38, which is
closed; the table and the notes agree again.
#### What 2.0.2's `IGNORE` change costs, measured
`IGNORE` now takes a *copy* of the context so the pool slot can be released, which fixes a
real leak. The copy lives in the public header as a file-scope `static`:
```c norun
static AKERR_THREAD_LOCAL akerr_ErrorContext akerr_last_ignored;
```
`static` means one per translation unit, and `AKERR_THREAD_LOCAL` means each one is
thread-local. `sizeof(akerr_ErrorContext)` is **37,296 bytes**, so:
| Build | TLS segment of `build/basic` |
|---|---|
| libakerror 2.0.1 | none at all |
| libakerror 2.0.2 | 1,417,248 bytes — **1.35 MiB**, per thread |
38 copies × 37,296 accounts for the segment exactly; `readelf -sW build/basic` shows all 38.
It scales with translation-unit count, not with anything about the program, and every thread
a host game spawns pays it again — which is worth knowing in a tree that spent a release
taking `akbasic_Runtime` from 10.75 MiB to 2.40 MiB.
It also emits **84 `-Wunused-variable` warnings**, one per TU that includes `akerror.h`
without using `IGNORE`. Nothing here builds with `-Werror`, so it is noise rather than a
failure — but a consumer that does cannot include the header.
**Both are `libakerror` issue #37 and neither is worked around here**, per the rule above: a
one-line `extern` in the header and a definition in `src/error.c` fixes it upstream, and
patching our copy would fork the submodule. Delete this subsection when it lands.
`IGNORE` still takes a *copy* of the ignored context so the pool slot can be released, which
fixes a real leak, but the copy no longer costs TLS per translation unit. It was a file-scope
`static` in the public header — one copy per TU, 37,296 bytes each, 1.35 MiB of thread-local
storage in `build/basic` alone, plus 84 `-Wunused-variable` warnings for every TU that never
called `IGNORE`. `libakerror` issue #37 moved it to an `extern` declaration in the header with
a single definition in `src/error.c`, which restores one copy per thread and silences the
warning. Issue #37 is closed and this subsection is gone accordingly.
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are
*different* ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible