C API evidence, coverage inventory and release gate
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Read from tools/c-api, docs/c-api, modules/c-api/CMakeLists.txt, cmake/Tests/ModuleProbes.cmake and the workflows at c1c316b9. The gate verdict was executed once, in a read-only copy of the previous snapshot (009d40f5), for the Known Issues verification; the C library itself was not built and no C test was run.
CNA's experimental C ABI carries more self-checking machinery than any other part of the repository: a recorded layout baseline, a toolchain matrix, a generated coverage inventory, a limitations document and a release gate that measures itself. This page explains what each of those instruments actually measures, what the gate reports at snapshot c1c316b9, how the coverage inventory decides what is in scope, and how far the evidence for the C library reaches. It is for anyone who has to quote a C API status, maintain the gates, or decide how much to trust the ABI.
The committed RELEASE_GATE.md reads "Not ready. 1 criteria are unmet": 631 public C++ symbols in runtime scope have no C mapping. Nine of the ten criteria are recorded and reported as met. The gate was not re-run for this page; the scope table that decides what the generator counts was read at the snapshot and classifies every module that publishes headers (see below).
What is established, rung by rung
The site's evidence vocabulary (source exists, configures, builds, links, unit tested, integration tested, runtime observed) applies to the C library as it does to everything else. For the C API at c1c316b9 the rungs stand as follows. Nothing in this table was built or run for these pages; where a rung is marked as measured, the measurement is CNA’s own local record (its Apple campaign on a physical Mac mini M4, plan_apple_m4.md AM4-209 to AM4-214), made on commits of this branch whose modules/c-api and tools/c-api sources are identical to c1c316b9.
| Rung | State at c1c316b9 | Basis |
|---|---|---|
| Source exists | 60 public headers, 3,210 declared routes, 59 implementation units | Read from modules/c-api; the declared route names equal the export list recorded in abi_baseline.json, with an empty difference both ways. |
| Configures | Opt-in: CNA_BUILD_C_API defaults to OFF and is OFF in every shipped preset; CNA_ENABLE_NET=OFF with the option on is a configure-time error naming it | CMakeLists.txt, CMakePresets.json. The alpha.1-era late failure on a missing GamerServices include is now this deliberate refusal. |
| Compiles | Measured locally, not in CI: CNA’s Apple campaign built the library on macOS (arm64) in the SOFTWARE and SDL_RENDERER configurations. The alpha.1 blocker (a renderer-map count assertion) is gone in source: 14 table rows against 14 canonical identities, and CNA_GRAPHICS_RENDERER_MAXIMUM equals the highest published value (43) | CnaCApiCoreExt.cpp by inspection: both static_asserts tie the table to the canonical enumeration. A syntax-only compile of that translation unit was run once, at an earlier snapshot, for the site's Known Issues verification and was not repeated for this one; it would show that the assertions hold for a compiler, not that the library builds; see the history of the blocker. |
| Links and exports | Measured locally on macOS, not in CI | The baseline's "runtime": 11776 field (0.46.0) comes from a built library. CNA’s Apple campaign ran check_declared_exports.py and generate_abi_baseline.py --check against the built Mach-O library on a physical Mac mini M4: the declared and exported sets agree exactly (3,210 routes; the dylib exports nothing else) and the recorded layouts hold on arm64 macOS (AM4-209, AM4-212). |
| Unit tested | 93 pure-C programs, 4 C++ test programs and 1 fuzz source exist; no CI job builds or runs them | modules/c-api/tests; every c-api-*.yml workflow is build-free. CNA's own record of a ctest -R '^CApi' run (111 tests, 108 passing, the 3 failures being generator gates; Debug HEADLESS tree, 2026-09-18) is in plan_capi_smoke_stability.md; the commit messages of the 0.40.0 to 0.44.0 changes (2026-10-02 and 2026-10-03) report local runs of 119 of 119. All concern earlier commits, and none names its configuration in a form this page could reproduce. For this branch, CNA’s Apple campaign records every C API test passing on macOS, 119 of 119 in each of the SOFTWARE and SDL_RENDERER trees (AM4-213). |
| Integration tested | Local CTest only | CApi_InstalledConsumer installs the package and builds and runs hello_cna shared and static, on Linux/ELF and on macOS, where a developer enables the option; on macOS it passed in CNA’s Apple campaign and also checks the installed dylib’s LC_RPATH @loader_path (AM4-211, AM4-213). |
| Consumers | Evidence for their own revisions only | CNA.NET (libcna/cna-dotnet), the maintained C# binding, admits exactly ABI 0.46.0 after a recorded compatibility review of the 0.44.0 to 0.46.0 difference, and records its own local evidence on Linux and on osx-arm64 (the latter against a 0.46.0 library of this branch). The eight archived bindings kept in CNA Lab (Common Lisp, Go, Java, Python, Ruby, Rust, Swift and TypeScript) follow no newer ABI: six refuse a 0.46.0 library by an exact-version rule, Go and Swift accept any minor from 0.35 on, and none was qualified against anything after 0.35.0 (bindings boundary). |
“Built and tested in CNA’s own local runs (Linux; macOS on Apple silicon), not by CI and not by these pages” is therefore the accurate summary of the library itself. The gate machinery below is different: most of it is build-free by design and can be run by anyone, which is exactly why its measured result at this snapshot can be stated.
Toolchain cells and run configurations
compatibility_matrix.json declares two things, rendered into COMPATIBILITY.md: the compilers and language modes the headers must support, and the renderer configurations the C tests are meant to run in. They have very different evidence behind them.
Header cells: 23 across 7 toolchains
| Toolchain | Modes | Role |
|---|---|---|
cc | c99, c11, c17 | required |
c++ | c++11, c++14, c++17 | required |
gcc, clang | c99, c11, c17, c23 each | optional |
g++, clang++ | c++17, c++20, c++23 each | optional |
x86_64-w64-mingw32-gcc | c99, c11, c17 | optional; headers only, no Windows binary is linked or run |
For every cell, generate_compatibility_matrix.py --run compiles each public header on its own and the umbrella header; an installed toolchain that rejects a header fails, an absent optional toolchain is reported as skipped with its reason, and an absent required one fails. The c-api-compat-matrix.yml workflow installs Clang, GCC 14 and the MinGW cross-compiler on Ubuntu 24.04 so that the optional cells can run there too, and runs both --check and --run. Together with the header half of the ABI baseline, this is what CI establishes about the C API: the headers are self-contained and parse in the declared C and C++ modes, and their layouts match the record. The floor is C99, not C17, because the headers must stay consumable by the oldest declared mode; CNA's own C test programs are compiled at C17 with extensions off.
Four run configurations, declared rather than enforced
| Configuration | Renderer | CNA_DEVICES | What it can establish |
|---|---|---|---|
headless | HEADLESS | OFF | Handles, results, threads, lifetimes and every route that needs no window or GPU; every route that does is proven to refuse. No pixels. |
sdlrenderer | SDL_RENDERER | ON | 2D drawing through SDL's renderer under the dummy video driver, with the devices-ext half compiled in; 3D resource routes a 2D renderer cannot serve answer CNA_RESULT_NOT_SUPPORTED. |
software | SOFTWARE | OFF | A CPU rasteriser with 3D support: a third set of capability answers, and the compiled-out devices half. |
asan | SOFTWARE | ON | AddressSanitizer and UndefinedBehaviorSanitizer with leak detection: handles that outlive their owner, operations nobody released. |
The same pure-C sources are compiled unchanged into whichever tree is configured, and they are written to accept the renderer's answer: 35 of the programs under tests/pure_c handle CNA_RESULT_NOT_SUPPORTED, and the teardown test treats a renderer that cannot create a 2D texture as covered by its game-only modes. The registrations in modules/c-api/CMakeLists.txt choose the display per test: in an SDL_RENDERER tree most smoke tests get SDL_VIDEODRIVER=dummy, while tests that need an OpenGL context or a real window (CApi_Draw3DSmoke, CApi_OwnedGraphicsDeviceSmoke, CApi_GameSecondaryGraphicsDeviceContext and the seven CApi_TeardownLifetime_* modes) get SDL_VIDEODRIVER=x11 and DISPLAY=${CNA_TEST_DISPLAY} under the display policy of TestDisplayPolicy.cmake.
The four configurations are a declaration. No tool reads them except the renderer of the document, and no preset or workflow at c1c316b9 configures any of those four trees with CNA_BUILD_C_API=ON. The sentence in COMPATIBILITY.md that every smoke program "runs in all four configurations" is CNA's description of its campaign practice; the only dated run record in the tree is the HEADLESS one cited above. The other 11 renderer identities share the same C surface but are not in the matrix at all.
The release gate: a record and a measurement
check_release_gate.py reads release_gate.json, where each criterion carries a recorded state (met, not met or an owner-decision state that measures as blocked), and runs one check function per criterion to measure the current state. Its three modes:
--runprints each criterion's measured state and "READY" or "NOT READY"; it always exits 0.--writeregeneratesRELEASE_GATE.mdfrom the measurement; the "Release" line is filled fromabi.h, so the version cannot go stale in prose.--check(theCApiReleaseGateCTest and thec-api-release-gate.ymlworkflow) fails when any criterion's measurement disagrees with its record in either direction, a met criterion that regressed or a blocked one that quietly became met, and otherwise fails if the committed document differs from what--writewould produce.
A "Not ready" verdict is therefore not a failing test: --check passes while the record and the measurement agree that a criterion is unmet. What the check functions measure is less uniform than the criteria's wording suggests.
What each criterion's check actually inspects
Criterion (id) | Recorded | What the check does | Kind of evidence |
|---|---|---|---|
Compatibility matrix (compatibility-matrix) | met | Runs generate_compatibility_matrix.py --check, which verifies that COMPATIBILITY.md matches the declaration, then counts the declared cells | Document currency; the compiling is done by --run (CApiHeaderCompatibility, the workflow), not by the gate |
ABI baseline (abi-baseline) | met | Runs generate_abi_baseline.py --check without --library: compiles a generated probe and compares every struct layout, scalar width and constant with the baseline; the export and runtime-version sections are reported as skipped | Measured from the headers with a C compiler; the export half needs a built library |
Coverage closed (coverage-closed) | not met | Runs generate_coverage_inventory.py --check; if that passes, parses the planned count from the generated coverage summary and requires zero, then requires every partial rule to carry an approval | Measured (Doxygen over the public C++ headers) |
Limitations matrix (limitations-matrix) | met | Runs generate_limitations.py --check | Measured from the same in-memory inventory |
Real C application (real-c-application) | met | Checks that hello_cna.c and its project exist, that the project calls find_package(CNA, and that the module's CMake file contains the text CApi_InstalledConsumer | Presence of a registration; the consumer is not built or run |
Installable (installable) | met | Checks for CNAConfig.cmake.in and for three strings in the module's CMake file (configure_package_config_file, write_basic_package_version_file, COMPONENT CNACApi) | Presence of package rules |
Documentation (documentation) | met | Each of 13 named documents exists and is at least 512 bytes | Presence |
Safety evidence (safety-evidence) | met | CApi_StressSmoke and CApi_Utf8Oracle are registered and their sources exist; the string-view fuzz source exists | Presence of tests; none is run |
Packaged dependencies (packaging-dependencies) | met | The install rules name libSDL3*.so* with INSTALL_RPATH "$ORIGIN" on Linux and libSDL3*.dylib with @loader_path on macOS, and the consumer script passes no -rpath-link or LD_LIBRARY_PATH | Text of build scripts |
Static configuration (static-configuration) | met | The archive generator localizes symbols (it contains --keep-global-symbols and its survivor check), the build and the example name the static target, and the package config offers it | Text of build scripts |
Six of the ten criteria are satisfied by the presence of a file, a registration or a string; they say that the machinery exists, and the real evidence for them arrives only when a developer builds the C API and runs ctest -R '^CApi'. That is a reasonable design for a build-free gate, but it means the gate's green rows are not results of running the library. The record's own history in release_gate.json shows why the gate measures instead of remembering: the coverage criterion was closed and reopened by repeated merges, once after a rule that matched a whole header with .* had let 121 new symbols inherit "implemented and tested" without any C route existing.
What the gate reports at this snapshot
The committed RELEASE_GATE.md names "CNA C ABI 0.46.0, experimental" and reads "Not ready": one criterion is unmet. The gate was not executed for this page; the following was checked by reading the scripts, the record and the generated documents at the snapshot.
- The scope table is total: every module that publishes an include tree has a classification (the table below), including
design,diagnosticsandinspector, which are out of scope with recorded reasons. By reading,validate_module_scopetherefore letsgenerate_coverage_inventory.py --checkproceed to Doxygen. - Coverage closed is recorded not met and reported not met: 631 public C++ declarations in runtime scope have no C mapping.
- The limitations matrix is recorded met, and the committed document is reported current: 15 partially mapped symbols in 9 groups, 443 unmapped in 86 groups. The other seven criteria are recorded and reported met, among them 23 declared cells across 7 toolchains and a baseline of 197 struct layouts and 3,210 exported symbols.
Four CTests carry the generator checks: CApiCoverageMatrix, CApiLimitations, CApiReleaseGate and CApiCoverageScopeModel (whose fixture file test_coverage_scope.py includes test_the_repository_classifies_every_publishing_module, which reads the real tree). They are registered in ModuleProbes.cmake in every native tests build that finds Python, deliberately outside if(CNA_BUILD_C_API), so an ordinary build carries them; the known-failure list of the unfiltered general test workflow (general-tests-ci.yml) names only one EasyGL stencil test. No workflow run was inspected for these pages, and whether the committed summary, LIMITATIONS.md and RELEASE_GATE.md match a fresh regeneration of this tree was not measured: the figures below are the committed generated snapshot. The 0.46.0 ABI is a large experimental surface; it is not a release-ready coverage closure. The coverage, limitations and release-gate CTests also depend on the Doxygen series: they measure only with the 1.9.x series their pins were recorded with (CNA_C_API_DOXYGEN or $CNA_DOXYGEN names it) and report Skipped, naming the version, under any other Doxygen; a direct invocation of the tools still fails (AM4-160).
How the coverage inventory decides scope
generate_coverage_inventory.py derives every public C++ declaration from modules/*/include/{Microsoft,CNA}/**/*.hpp through Doxygen XML, drops any path with an Internal or Detail segment in any capitalization, and maps each remaining symbol through the rules in coverage_mappings.json. Which modules count at all is decided by MODULE_SCOPE, a table that must be total over the modules that publish an include/ tree.
| Classification at c1c316b9 | Modules |
|---|---|
| Runtime scope (14) | audio, content, core, devices, devices-ext, gamer-services, graphics, graphics-ext, input, math, media, net, runtime, storage |
| Out of scope, with a recorded reason (7) | c-api (the ABI itself), platform (the substrate the ABI is built on; owner decision CBIND-047), content-pipeline (a build-time tool the C library does not link; CBIND-117), phone (the Windows Phone shell and notification API, not XNA 4.0; CBIND-117), design (design-time type converters the runtime C ABI does not link), diagnostics (the optional profiler interface, which has no C routes), inspector (the optional inspector agent and protocol, which have no C routes) |
| Out of scope by prefix | renderers/: implementations hidden behind the renderer interface; a C caller selects by identity |
| No entry needed | video-ffmpeg, which publishes no include tree |
validate_module_scope fails when a publishing module has no classification, when a table entry names a module that no longer publishes headers, when a prefix entry covers nothing, when a nested module is classified as runtime scope (the scanner would attribute its symbols to the parent), when an exclusion has no reason, or when an out-of-scope subtree rule points at a non-runtime module or a missing directory. The table is total on purpose. The comment beside it records the incident that made it so: when the Content Pipeline became its own module, nothing classified it, and 2,554 of its declarations fell through to a completed task and were reported as missing runtime C bindings for about a fortnight. A total table cannot fail that way; a new module stops the gate and asks for an owner decision per module, not a reclassification that only makes the gate green. The design, diagnostics and inspector entries are such decisions: none of the three has C routes, and each exclusion says why.
The inventory's four statuses are defined in the generated coverage summary under docs/c-api: implemented means an exact current C mapping with C-only evidence named (not a behaviour oracle), partial an approved subset with its boundary recorded, planned no complete mapping and an unfinished task that owns the work, not applicable a declaration with no callable C++ behaviour or one belonging to a declared substrate boundary. No symbol counts as implemented because a related C operation exists. The committed summary, per module:
| Module | Symbols | Implemented | Partial | Planned | N/A |
|---|---|---|---|---|---|
audio | 321 | 217 | 0 | 10 | 94 |
content | 801 | 563 | 10 | 204 | 24 |
core | 132 | 126 | 0 | 0 | 6 |
devices | 217 | 187 | 0 | 2 | 28 |
devices-ext | 84 | 79 | 0 | 0 | 5 |
gamer-services | 689 | 618 | 0 | 26 | 45 |
graphics | 2,948 | 2,626 | 0 | 255 | 67 |
graphics-ext | 110 | 68 | 0 | 40 | 2 |
input | 897 | 836 | 0 | 32 | 29 |
math | 954 | 927 | 0 | 26 | 1 |
media | 338 | 285 | 0 | 1 | 52 |
net | 285 | 253 | 1 | 14 | 17 |
runtime | 322 | 226 | 4 | 19 | 73 |
storage | 44 | 42 | 0 | 2 | 0 |
| Total (469 headers) | 8,142 | 7,053 | 15 | 631 | 443 |
451 further headers are excluded by the scope rules (renderers 115, content pipeline 107, platform 29, design 16, the build-time import and pipeline subtrees of content 15, phone 9, inspector 3, diagnostics 2, and 155 Internal/Detail paths of runtime modules). The figures are not comparable with the alpha.1 inventory (6,712 rows from 421 headers) because the scope model changed in between.
The planned backlog and the limitations document
The 631 planned rows are the only declarations in runtime scope that a C caller cannot reach; the C++ API provides them. The inventory names an open task as the owner of every planned row (owner_task in generate_coverage_inventory.py), and the tasks are described in plan_binding.md:
| Task | Rows it owns | What it decides |
|---|---|---|
| CBIND-127 | Every planned runtime row that no other task names: the catch-all owner | C routes for the runtime C++ surface that grew after the binding campaign closed its slices |
| CBIND-117 | The CNB Model-v2 data and codec, CNB source-import and model-from-CNJ headers | Whether a C consumer gets the CNB Model-v2 CPU API |
| CBIND-122 | 5 named symbols | Untyped XNB loading seams |
| CBIND-125 | 5 named symbols | Inheritance-aware reflective-reader customisations |
| CBIND-120 | 3 named symbols | Texture profile-format queries |
| CBIND-121 | 1 named symbol | A reflective shared-resource field |
The committed summary counts the planned rows per module (the Planned column above: graphics 255, content 204, graphics-ext 40, input 32, gamer-services 26, math 26, runtime 19, net 14, audio 10, devices 2, storage 2, media 1), not per task. The task table in the plan still carries the per-task counts of an earlier inventory (CBIND-127 320, CBIND-117 134 and the four small tasks, 468 in all), which no longer sum to the current figure; read the size of the backlog from the summary.
LIMITATIONS.md, generated by generate_limitations.py and described in the gate record as the document a consumer reads to decide whether to adopt the ABI, states the 8,142 total and then partitions only 7,511 of them (7,053 fully mapped, 15 partially mapped, 443 with no C form): its generator keeps only partial and not-applicable rows, so the 631 planned declarations, which have no C route either, do not appear in it. The release gate's note still attributes 654 build-time declarations to CBIND-117 although the plan records 134 after the Content Pipeline was scoped out, and it names neither CBIND-125 nor CBIND-127. Read the planned backlog from the coverage summary and the plan, not from the limitations document (see Known Issues). The 15 partial rows are the entries a working program can be surprised by: a route exists under the expected name but does less than the C++ member, and the document says what to call instead.
Quoting C API status correctly
- Say "ABI 0.46.0, experimental; built and tested in CNA's own local runs (Linux; macOS on Apple silicon), not by CI" for the library; say that no CI workflow builds it, and that a minor version below 1.0 may break compatibility.
- Quote coverage as "the committed inventory (469 headers, 8,142 symbols: 7,053 implemented, 15 partial, 631 planned, 443 not applicable)".
- Quote the gate as committed: "Not ready, one criterion unmet: 631 public symbols are still unmapped", and say that it was not re-run unless it was. The gate's “symbols” are planned public C++ declarations without a C mapping, not missing exports of the 3,210 declared C routes.
- Treat a binding's result as evidence for that binding's pinned revision and its ABI generation, never for CNA at c1c316b9.
- Keep the alpha.1 compile blocker as history: it is gone in source, and whether the library builds at this commit is still open.
The maintainer procedure for changing routes, mappings and the gate record is I need to update the C API; the contract a consumer programs against is on Native C API contract.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- C API and bindings architecture
- Internals
- C API internals: tests and gates
- Maintainer workflow
- I need to update the C API: inventory and release gate
- Tests and validation
- Test architecture
- Reference
- Test target index