I need to update the C API
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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Read at 3b43bd86 from modules/c-api (headers, specs, generated routes, sources, tests), tools/c-api, docs/c-api, the c-api-* workflows, CHANGELOG.md and CNA's plan records; nothing was configured, built or run, no workflow builds the C library, and the recorded C API builds refer to other commits.
Use this recipe when a change touches modules/c-api, a public C++ header the C layer adapts, or anything the C ABI records about itself: its version, layout baseline, coverage inventory or release gate. The C layer is a second ownership model over the canonical C++ surface and has more mechanical gates than any other module; most read files rather than a built library, so they register and run in an ordinary tests build. Since the 0.1.0 release its route boundaries are generated: a route is a header declaration plus one spec line, and only the logic of a complex route is written by hand. Everything here was read from the TARGET tree at 3b43bd86; nothing was configured, built or run for this page.
What is and is not established. No workflow at TARGET builds libcna_c_api; the six c-api-*.yml workflows check headers, specs, generated files, JSON and Doxygen output only, and every shipped preset sets CNA_BUILD_C_API=OFF. CNA's own records report C API builds and ctest -R '^CApi' runs on other commits: the plan record plans/plan_capi_smoke_stability.md on an earlier next commit (111 tests, 108 passing, the three failures being generator gates), the earlier commit that set ABI 0.44.0 (ed885a8ae, an ancestor of this snapshot: ^CApi 119/119, C API gates 9/9), CNA’s Apple campaign on a physical Mac mini M4 (every C API test in two renderer trees, the installed-package consumer shared and static, the export and baseline gates read from the Mach-O library) and the 0.1.0 release run, whose complete suite carried CApi_InstalledConsumer as a known failure (an installed consumer could not find wgpu_native; [Unreleased] records the package fix and the test passing for both consumers). Those are local measurements of other commits, not CI and not evidence produced for this page, and all of them predate the generated route boundaries. A C++ change can therefore break the C layer without any automatic signal, and the C API's history contains exactly that.
Find the owner
The C layer lives in one module and is meant to change in the same commit as the C++ it adapts (docs/c-api/README.md). Its pieces and their checks:
| Piece | Where | Checked by |
|---|---|---|
60 public headers (C99 floor), aggregated by cna.h | include/CNA/C | CApi_HeaderAudit (no std::, class, template, namespace, throw; only stdint.h, stddef.h, stdbool.h and CNA/C/*.h may be included), CApiCompatibilityMatrix, CApiHeaderCompatibility |
Route boundaries: one spec line per route in 56 family specs, generated into 56 .inc files that are never edited | spec, src/generated, written by generate_routes.py; the procedure is in GENERATED_ROUTES.md | CApiGeneratedRoutesCurrent (--check), CApiRouteGeneratorTests, the generated CApi_GeneratedRoutesSmoke and the c-api-generated-routes workflow |
Implementation: 59 translation units (each family ends with #include "generated/<Family>.inc" and holds the hand-written helpers and lookups) plus private headers: CnaCApiDetail.hpp (barrier, refusals, registry), CnaCApiOutput.hpp (outputs, size-then-copy), CnaCApiMarshal.hpp (C↔C++ conversions) and the family *Detail.hpp | src, listed explicitly in modules/c-api/CMakeLists.txt (no glob) | The C API build itself (-Wall -Wextra -Werror) and the pure-C tests |
| Route tests | tests/pure_c (94 *Smoke.c programs, one of them generated, plus AbiHeaderC.c, RuntimeComponentDeviceBorrow.c and ServiceClient.c), tests/cpp (registry, exception barrier, owner-thread runtime and UTF-8 oracle, plus the C++ header compile check), tests/fuzz | One add_test per program, under CNA_BUILD_TESTS |
| Recorded layout and exports | abi_baseline.json: 196 struct layouts, 296 scalar types, 1,478 integer constants, 3,212 exports | CApiAbiHeaderBaseline (headers only), CApiAbiBaseline (built library), the c-api-abi-baseline workflow |
| Coverage inventory and release gate | coverage_mappings.json (628 rules), the generated summary, release_gate.json, RELEASE_GATE.md | CApiCoverageMatrix, CApiLimitations, CApiReleaseGate and their workflows |
| Contract prose | docs/c-api | CApiDocExportCounts holds the export counts in four of the documents |
First decide whether the change belongs in the ABI at all. The inventory records 453 public C++ symbols as having no C form (templates over types C cannot name, protected hooks, the platform substrate, renderer implementations behind IGraphicsRenderer). Touching such a symbol needs no route but must leave the inventory consistent. The scope rules are in generate_coverage_inventory.py: MODULE_SCOPE classifies every module, so a new module stops the gate instead of inheriting a default.
Read first
abi.h:CNA_ABI_VERSION_MAJOR/MINOR/PATCH(0, 50, 0),CNA_ABI_VERSION_ENCODE,CNA_Resultcodes 0–14,CNA_Handle.docs/c-api/ABI_VERSIONING.md: the0.xrule, the four evolution paths, the recorded-baseline section.OWNERSHIP.md,HANDLES.mdandCALLBACKS_AND_THREADING.md, then C API internals for how the code implements them; this page does not repeat that trace.CnaCApiDetail.hpp(CallWithExceptionBarrier,HandleRegistry,ObjectKind, the named refusals), then the family’s spec inmodules/c-api/spec, theCnaCApi<Family>.cppthat owns a neighbouring route, and its pure-C smoke test.GENERATED_ROUTES.md: the route kinds (value,get,call,size,copy,release,result), which conversions the generator applies, and when a route is a hand-written helper.
Contracts a route must keep
- Handles are 64-bit: a 1-based slot in the low 32 bits, a generation in the high 32.
HandleRegistry::Getrefuses a stale generation or wrongObjectKind(CNA_RESULT_INVALID_HANDLE) and a foreign creation thread (CNA_RESULT_THREAD);Releaseand the user-tag accessors check the thread too; onlyGetFromAnyThreadskips it, for the sound-effect play lookup (0.43.0). Once a slot's last 32-bit generation is released the slot is retired, not wrapped. There is one process-wide registry. The kind is the only type guard, so resolving a handle under the wrong C++ type is undefined behaviour, not an error. - The exception barrier is positional. Catch arms are ordered most-derived first, so a new or re-parented native exception can land in a different arm;
CApi_BoundaryDetailpins representative mappings only. - Threading is part of the contract. A refusal that
Getraises only because another thread created the handle is noted, and withcna_game_set_foreign_thread_calls_exton (0.39.0, off by default)CallWithExceptionBarrierre-runs the whole route body on the game thread while the caller blocks; the game thread serves the queue at the start of each update and draw and incna_game_run_foreign_thread_calls_ext(0.41.0). A new route that resolves its handles through the registry gets this for free; a route that must itself run on the game thread has to refuse with aTHREADthat is not noted, as driving or destroying the game does. Allowing a route on any thread, or in a new phase (before the run, in an event handler), is a rule change that takes a minor. - A refused creation leaves the output invalid: the generated half of a route clears every handle output to
CNA_INVALID_HANDLEand refuses a null output before a helper runs, and checks faults in one order (a null output, then a non-canonicalCNA_Bool, then a refused argument, then handles in parameter order). - Structures are versioned prefixes (
struct_size >= sizeof,struct_version == 1); fields are appended, never reordered. EveryCNA_Boolroute refuses bytes other than 0 and 1, held by the generatedCApi_BoolContractSmoke. - Ownership is written into the header. OWNERSHIP.md names six categories (owned, borrowed, retained, transferred, callback context, caller buffer) and calls a declaration incomplete until its Doxygen names one. No test enforcing this was found; it is a review obligation.
- Owned children block their parent:
cna_game_destroyanswersCNA_RESULT_INVALID_STATEwhile an owned graphics resource, content manager, audio resource or game component lives. Event registrations are the deliberate exception. A new owned child type must join the relevant counter, not only the registry. - Two language levels. Headers are held to C99 (23 declared matrix cells across 7 toolchains); CNA's C targets compile as C17 with extensions off and warnings as errors.
ABI version discipline
The ABI version is independent of the product version string and of Sharp Runtime's; docs/releasing.md says it “moves when the ABI changes, independently of a product release”. It is defined once, by three macros in abi.h, encoded major<<16 | minor<<8 | patch (0.50.0, this snapshot, is 0x00003200). Everything else derives from or is held against them:
| Consumer of the version | How it stays in step |
|---|---|
CMake package CNAConfigVersion.cmake | The module CMake file regex-reads the macros and writes the version file with SameMajorVersion; the package version is the ABI version. The C++ SDK package (CNA_INSTALL_CXX, InstallCxx.cmake) reads the same macros, so both installed CNA packages carry the C ABI version. |
abi_baseline.json | generate_abi_baseline.py measures the macros from the headers and, with --library, what cna_get_abi_version() reports. |
AbiHeaderC.c | Deliberately carries no version literal: it checks only the encoding. A literal had gone two bumps stale (0.27.0 against 0.29.0) unnoticed and broke the build when the C API was next built. |
| Prose | The identity paragraph and history in ABI_VERSIONING.md; export counts are held by CApiDocExportCounts. |
ELF version node CNA_C_API_0.1 | Intentionally not the ABI version and never bumped with it; it would change only with a major break (CnaCApiExports.map). |
When the version moves
While the ABI is 0.x, an incompatible change needs a minor increment, release notes and a regenerated baseline; from 1.x, only additive changes within a major, and any removal or meaning change needs a new major. CNA also increments the minor for additive generations (0.36.0, 0.38.0, 0.39.0 and 0.41.0 each added one route; 0.50.0 added seven routes and some constants) and for a rule that widens with every signature unchanged, “because a consumer needs a number to require”. ABI 1.0 is a separate decision the release gate says it does not make.
| Change | Class and version | Recorded at TARGET |
|---|---|---|
| New route, struct, appended field, constant or handle kind | Additive; minor by project practice | 0.36.0 (cna_packet_reader_copy_data_ext), 0.37.0 (four exception type and parameter-name queries), 0.38.0 (cna_game_run_frame_ext), 0.39.0 and 0.41.0 (the two foreign-thread call routes), 0.46.0 (the native-window-system constant CNA_NATIVE_WINDOW_SYSTEM_UIKIT), 0.50.0 (skinned-model part colour, CNB model cameras and five renderer features, and the restored CNA_DisplayColorSpace typedef, a uint32_t that changes no binary signature) |
| Route, struct or constant removed or renumbered | Incompatible; minor while 0.x, with notes and a regenerated baseline | 0.30.0 deleted a whole header, engine_layer.h, and with it 842 net exports and 23 layouts; the gamer-services line removed eleven avatar real-rendering routes and CNA_AvatarAppearanceEXT (its 0.33.0) and the routes cna_guide_set_is_visible and cna_guide_set_is_trial_mode (its 0.34.0), and 0.35.0 merged the two lines that had both used 0.33.0 under an unused number; 0.47.0 removed the C surface of the retired modern GPU layer (two capability identities, four renderer features, thirteen limits, three format-usage bits, two structures and five routes, every surviving number kept and the removed ones reserved); 0.49.0 renamed the renderer classification surface from “graphics backend” to “graphics renderer” (eight routes and their typedefs and constants, no alias kept, so the export count did not change) |
| Meaning of an existing route, ownership rule, error rule or callback rule changes | Incompatible; minor while 0.x | 0.3.0 (every CNA_Bool route refuses non-canonical bytes); 0.9.0 (seven documented contract changes, four of which had landed unversioned under 0.8.0); 0.32.0 (Guide and the gamer-services dispatcher answer by configured service availability, and a duplicate initialization answers INVALID_STATE) |
| A call that was refused is now accepted (a thread, phase or handle rule widens); every export, layout and constant unchanged | Rule-only; minor by project practice. It passes every link and baseline check, so only the release notes and the header text show it | 0.40.0 (the twelve cna_graphics_adapter_* routes accept the game handle), 0.42.0 (the device is lent before the run begins), 0.43.0 (sound-effect play on any thread), 0.44.0 (Activated and Deactivated handlers may borrow the device) |
| A renderer identity constant leaves the ABI | Incompatible plus a permanent reservation of its value | Recorded step by step in ABI_VERSIONING.md, most recently 0.48.0, which retired four identities (values 1, 8, 14 and 42) and left ten; CNA_GRAPHICS_RENDERER_MAXIMUM stays 43 (FNA3D), the next new identity takes 52, not MAXIMUM + 1, and scripts/check_renderer_identities.py fails on reuse |
| C++ behaviour changes, every C signature unchanged | Depends on whether a documented C contract moves | Practice is mixed: 0.9.0 versioned such changes, while the plan record of thirteen stale smoke-test expectations (defaults, refusal codes, text formats) after the library stopped compiling corrected two header comments and the tests with no bump. Decide explicitly and say in review whether the old promise was part of the header |
How a version is bumped
Read from the tools and workflows; not executed. No script bumps the version: each step is a manual edit that a gate then holds.
- Edit the macro in
abi.h; repeat the literal nowhere that is not gated. - With the library built:
python3 tools/c-api/generate_abi_baseline.py --write --library <build>/modules/c-api/libcna_c_api.so, and review theabi_baseline.jsondiff. Additions are permitted; a moved field, changed constant or vanished export is named an ABI break. - Write the entry at the top of
ABI_VERSIONING.mdand keep the export counts in the four gated documents equal to the baseline (check_doc_export_counts.py --check). python3 tools/c-api/check_release_gate.py --writeregeneratesRELEASE_GATE.md, whose “Release” line names the ABI version.- Add a
CHANGELOG.mdentry under[Unreleased](the entries that moved the ABI to 0.29.0, 0.31.0 and 0.33.0 name what was removed, the deltas and the deciding plan row). Practice is uneven:ABI_VERSIONING.mdrecords every step from 0.30.0 to 0.50.0, butCHANGELOG.mdnames only some of them ([Unreleased]names 0.48.0 and 0.49.0, nothing names 0.47.0 or 0.50.0), so read the history inABI_VERSIONING.md. - Leave the installed-consumer example's
find_package(CNA 0.1 CONFIG)alone: it requests the version it was written against on purpose. Bindings are updated separately (below).
The coverage inventory and the release gate
generate_coverage_inventory.py parses modules/*/include/{Microsoft,CNA}/**/*.hpp with Doxygen (Internal and Detail path segments excluded) and maps every declaration through the rules. Read the numbers from the generated summary: at TARGET, 466 in-scope headers and 8,134 symbols, of which 7,033 implemented, 15 partial, 633 planned and 453 not applicable, with 406 headers explicitly excluded (content pipeline, design, diagnostics, inspector, phone, platform, renderers, internal paths and the build-time content import and pipeline subtrees, each with a recorded reason). Planned rows by module: graphics 252, content 203, graphics-ext 36, input 32, math 32, gamer-services 28, runtime 19, net 14, audio 11, devices 2, media 2, storage 2.
The release gate reads Not ready: nine criteria are recorded as met, and one, “No public C++ symbol is unaccounted for”, is recorded as not met because of those 633 rows. MODULE_SCOPE at TARGET classifies every module that exists, design, diagnostics and inspector included, so the scope check the inventory runs first has an answer for each; the gate was not re-run for this page. The gate is built to fail in both directions: check_release_gate.py fails when a criterion recorded as met stops being met and when one recorded as blocked has quietly become met, so closing the last planned row also forces a change to release_gate.json. It governs publishing an experimental release only.
| Situation | What fails | What to do |
|---|---|---|
| A new public C++ symbol in a module the C ABI links | CApiCoverageMatrix, then the release gate | Bind it and extend or add a rule (status implemented, a task, a tests text), or record an owner-approved not-applicable/partial. Nothing is reclassified to turn a gate green. |
| Editing a header a rule matches | The rule's approved_symbols disagree with what it matches | Rules are pinned to stable CPP-… IDs; widening one needs --approve-rule-symbols --rule ID --grow-approved-symbols after a human checks the routes cover the new symbols. |
| A new module | validate_module_scope | Classify it in MODULE_SCOPE with an owner decision. |
| Prose about limits is stale | CApiLimitations | Edit limitations.json or the mapping and run generate_limitations.py --write; never hand-edit generated files. |
The inventory tools need Doxygen and the sibling sharp-runtime checkout (the workflows install Doxygen and run scripts/ci/clone_siblings.sh); the ABI-header, route-coverage, bool-contract and compatibility checks need only Python and a C compiler. Two documentation gaps: the per-task open-row counts in plans/plan_binding.md (320 for CBIND-127) are not kept equal to the generated 633, and the gate’s release note still records 654 content-pipeline declarations under an open task although the summary scopes that module out. Trust the generated summary.
Adding a route (procedure, not executed)
The shortest path that satisfies every gate for a new route in an existing family, derived from GENERATED_ROUTES.md, existing routes and tests. A new handle kind grows steps 3–4; a new module grows the scope row above.
- Write the contract first. Owned, borrowed, retained, transferred or copied; which thread; which result codes; whether a child count is involved. Put it in the Doxygen of a declaration that is C99,
CNA_C_API-marked,cna_-prefixed and fixed-width only (nolong,size_t,boolor bit-fields). - Classify the ABI change with the table above.
- Generate it. A mechanically regular route (a property read or write, a command, a value computation, a text size/copy pair, a destroy with nothing to undo) is one line in
modules/c-api/spec/<Family>.toml. A route with real semantics (a creation that publishes and counts, an event, a bulk transfer) is aresultline in the family’s[helpers]group that calls a hand-written helper named after the route in CamelCase, inCnaCApi<Family>.cpp; the generated half supplies the exception barrier, the null-output refusal, the cleared handle outputs and theCNA_Boolcheck, so the helper does not repeat them. Runpython3 tools/c-api/generate_routes.pyand commit the spec, the helper and the regenerated.inctogether. Only thecna_error_*queries andcna_get_abi_versionare written whole by hand; a new fully manual entry point needs a written reason. A new source file must be added to the explicitadd_library(cna_c_api ...)list. - If it owns a resource, add an
ObjectKind, a parent token and, for a game child, the matching owned counter socna_game_destroyrefuses while it lives. The ELF map exportscna_*by pattern, as does the macOS exported-symbols list (_cna_*,CnaCApiExports.darwin.txt), and the wasm export list is generated from the headers (generate_wasm_exports.py); neither is edited. - Write the pure-C test with
<CNA/C/cna.h>andCnaTestReport.h(CNA_TEST_FAIL(n)names the failing stage). Cover success, a refused argument (output staysCNA_INVALID_HANDLE), a stale handle, the wrong thread, and child-before-parent destruction. Register it beside its neighbours:add_executable,C_STANDARD 17,C_EXTENSIONS OFF, linkcna_c_api,cna_c_api_enable_strict_warnings,add_test(NAME CApi_…), andSDL_VIDEODRIVER=dummywhere a window opens. - Route-test coverage.
CApiRouteTestCoveragehas a budget of 0 uncovered routes and counts a route as covered when any file undertests/orexamples/names it, the generatedGeneratedRoutesSmoke.cincluded, so a generated route is “covered” before anyone tests what it computes. If the route takes aCNA_Bool, rungenerate_bool_contract_test.py --writeorCApiBoolContractCurrentfails. - Record layout and exports (
generate_abi_baseline.py --write --library ..., additions only expected), thencheck_declared_exports.py --library ..., which compares declared and exported names both ways. - Update the inventory: mapping rule, then
generate_coverage_inventory.py --writeandgenerate_limitations.py --write; a route that closes a planned row should lower the count. - Bump the version and update
ABI_VERSIONING.md,CHANGELOG.mdand the family document (AUDIO.md,GRAPHICS_DEVICE.md, …). - Run the gates below, and the installed-consumer check if headers, exports or packaging changed.
Tests and gates
| Gate | Needs | Catches |
|---|---|---|
CApiAbiHeaderBaseline, CApiAbiBaselineTests, CApiCompatibilityMatrix, CApiHeaderCompatibility, CApiBoolContractCurrent, CApiRouteTestCoverage, CApiDocExportCounts, CApiGeneratedRoutesCurrent, CApiRouteGeneratorTests | Ordinary tests build with Python 3 (3.11 or later for the route generator); no C library | Moved fields or constants, headers not self-contained at C99, a stale generated bool test or route file, a generator regression, routes with no caller, stale export counts |
CApiCoverageMatrix, CApiCoverageScopeModel, CApiLimitations, CApiReleaseGate | Python 3; the inventory ones also Doxygen and sharp-runtime | An unmapped public symbol, a stale generated summary, a criterion that disagrees with its measurement |
CApiAbiBaseline, CApiDeclaredExports, CApi_Exports | The built library, ELF or Mach-O (registered on macOS too; each tool decides by the file’s magic) | A vanished or unexpected export, a declared route the library does not export, a wrong reported version |
The CApi_*Smoke programs (the generated CApi_GeneratedRoutesSmoke among them), CApi_TeardownLifetime_* (seven modes), CApi_StressSmoke, CApi_HandleRegistry, CApi_BoundaryDetail, CApi_OwnerThreadRuntime | CNA_BUILD_C_API=ON with CNA_ENABLE_NET=ON | Route behaviour, refusals, handle recycling, process-exit teardown |
CApi_InstalledConsumer | C API build, Linux/ELF and macOS, serial, 900 s timeout | An install unusable from outside the tree, shared and static, without an environment variable |
CApi_WasmLinkContract, CApi_WasmModuleSmoke, CApi_WasmBrowserProbe | Emscripten with Node (Playwright for the probe) | A route missing from the browser module |
CNA_BUILD_C_API=ON requires CNA_ENABLE_NET=ON (a configure-time refusal otherwise) and is OFF in every shipped preset. Pair a lifetime change with a sanitizer tree: the process-exit crash in this module was found and proved with AddressSanitizer, and CApiAbiBaseline forwards CNA_SANITIZE so a sanitized tree can still run it. See debug shutdown and lifetime.
What a C++ change costs downstream
| C++ change | Inside the tree |
|---|---|
| Rename, re-type or remove a wrapped public symbol | The C layer stops compiling. The effect collections' operator[](int) returned T* while CnaCApiEffects.cpp still took its address, unnoticed until the library was built again (CHANGELOG, “-DCNA_BUILD_C_API=ON builds again”). |
| Add or remove a renderer identity | Two static_asserts in CnaCApiCoreExt.cpp compare the 10-row identity table with the C++ count and CNA_GRAPHICS_RENDERER_MAXIMUM with the highest published value. The build failure at the first tag was exactly this assertion; removing a constant is also an incompatible ABI step. |
| A virtual stops being pure or a default changes | An adapter override written for the old contract may now be wrong: Effect::Clone() stopped being pure virtual and the C adapter's override silently dropped a compiled effect's runtime, so the override was deleted. |
| A generic template the wrapper reached changes semantics | A route regressed while both test suites were individually right: the raw vertex upload route, repaired by two new public forwarders on DynamicVertexBuffer. |
| A new or re-parented native exception | May move failures between barrier arms, giving a different CNA_Result. |
| A profile or limit tightens (Reach enforcement, depth-stencil refusals) | Smoke tests written against the old default fail: CNA's plan classified thirteen stale expectations after the library stopped compiling. |
The maintained external binding, CNA.NET (cna-dotnet), is a separate repository on its own cadence. It admits exactly one reviewed ABI point, 0.47.0 (admitted at its commit f1f88e1, 13 October 2026, after 0.46.0 at 8219a6c), so it refuses the 0.50.0 library of this snapshot, and every new minor, additive, incompatible or rule-only, needs a reviewed point there before CNA.NET accepts the library. The eight archived bindings (Common Lisp, Go, Java, Python, Ruby, Rust, Swift and TypeScript, kept in CNA Cemetery) are frozen at their final revisions and receive no C API change; they stopped at 0.21–0.23 (Common Lisp) and 0.35.0 (the other seven), and none is qualified against this ABI (per-binding table on the bindings boundary). For a C API change this means: (a) a 0.x minor is not backwards compatible, yet find_package accepts every later 0.x; ABI_VERSIONING.md and CONSUMING.md therefore ask a 0.x consumer to refuse at startup, through cna_get_abi_version(), any minor it was not written against (hello_cna.c shows the check), and a binding that admits a range trusts a rule, not a guarantee; (b) an additive route is invisible to a binding until its own manifest adds it (the C# binding compiles a matrix, the archived Python binding's loader refuses a missing export by name); (c) a rule-only minor changes no export, so only the binding's own refusals, threading and tests reveal it; (d) CNA cannot run the bindings' suites, so the evidence a maintainer can hand over is the regenerated baseline, the header diff and the release notes. The migration shapes are on the C# page (and, as records, on the archived Java and Python pages), and the propagation checklist is in C API and bindings architecture.
Traps
- Green ordinary build, broken C layer. Build
cna_c_apiin aCNA_BUILD_C_API=ONtree before claiming a wrapped-header change is unaffected, or say plainly that you did not. - A defined result code nothing returns.
CNA_RESULT_SHUTTING_DOWN(13) is declared, mapped to an error category and documented in HANDLES.md (“runtime is closing or closed”), yet searchingmodules/c-api/srcfinds no route returning it; a stale handle answersINVALID_HANDLE. Do not document or test behaviour the implementation lacks. - Static destruction. The registry is a function-local static in
CnaCApiRuntime.cppthat owns games; anatexithandler registered once a game exists destroys what is left before the libraries a device loaded are finalized. CNA made the platform bookkeeping it depends on immortal because reverse-order destruction otherwise read freed memory at exit; a new process-lifetime static that aGamedestructor touches must follow that idiom. - A finalizer is not a destroyer. A garbage-collected wrapper releasing children on another thread gets
CNA_RESULT_THREADand loses the handle. - Counting a mention as a test.
CApiRouteTestCoveragepasses on a name appearing in any test source, the generated contract test included; that test proves the boundary rules, not what the route computes, so assert behaviour in the family’s own smoke test. - Editing a generated file.
src/generated/*.incandGeneratedRoutesSmoke.care rewritten by the generator and checked byCApiGeneratedRoutesCurrent; change the spec or the helper instead. - Caller-created devices.
cna_graphics_device_destroydisposes without a child-count check in its route body, and that is the documented contract:graphics_device.hsays resources on a caller-created device belong to it and are released with it (GraphicsDevice::Disposedisposes its resource list), so destroying the device first is permitted. The pure-C tests still destroy resources first, and what a child handle answers after its device is gone is not pinned by a test. Treat that part as an open rule.
Review checklist
- Header: C99 only, ownership category, thread rule and result codes written down.
- Implementation: a spec line where one suffices, otherwise a
resulthelper; generated files regenerated and committed with their sources; kind and thread through the registry, child counter, callback re-entrancy rules kept. - ABI: change class named;
abi.h, baseline,ABI_VERSIONING.md,CHANGELOG.md, release-gate record and prose export counts agree. - Tests: a pure-C test that fails when the route is wrong; the destroy-order and wrong-thread cases; which gates ran, which need a built library, which were not run.
- Inventory: no unreviewed planned row, no rule widened without the approval flag, limitations regenerated.
- Bindings: the ABI point CNA.NET admits and whether the change needs a new one, the fact that the archived bindings are not updated, and any removal, renumbering or rule-only widening called out.
Related: Ownership and lifetime master map, Thread and callback map, Blast radius and readiness, Make a release and Change public XNA behaviour.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- C API evidence, coverage inventory and release gate — How far the evidence for CNA's C ABI 0.50.0 reaches, what each release-gate criterion really checks, what the gate reports for the 633 public C++ declarations still planned for C mapping, and how the coverage inventory classifies modules.
- Native C API contract: admission, buffers, retention and route families — What CNA's C ABI 0.50.0 version checks admit, the exact count-then-copy protocol, callback, registration and thread rules, resources that retain others, caller-created devices and all 60 headers by family.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-055: CHANGELOG.md's v0.1.0-alpha.1 section says 49 renderer identities for a release that had 50 — The C API documents, core_ext.h and ModuleProbes.cmake no longer state obsolete whole-registry renderer counts, but the alpha.1 release notes still say 49 renderer identities where that tag's selection list holds 50; no
- CNA-BUG-056: docs/c-api/README.md still presents the C ABI as its initial 0.1.0 library while abi.h declares 0.50.0 — The release gate reads its ABI label from abi.h and the old feature matrix is gone, but the C API README opens by calling the library its initial 0.1.0 shared library and then lists today's surface.
- CNA-BUG-058: LIMITATIONS.md omits the 633 planned public C++ declarations and RELEASE_GATE.md still attributes 654 declarations to CBIND-117 — The generated C API limitations document partitions 7,501 of the 8,134 declarations it counts and never mentions the 633 planned ones, and the release-gate note still says the Content Pipeline task owns 654 declarations
- CNA-GAP-062: The C ABI has no route for 633 public C++ declarations that the coverage inventory records as planned — COVERAGE.md records 633 in-scope public C++ declarations without a C mapping (graphics 252, content 203, graphics-ext 36 and smaller tails in nine other modules), which is why the C ABI release gate reads Not ready.
- CNA-VGAP-054: Destroying a caller-created GraphicsDevice while resources on it are still live is allowed by cna_graphics_device_destroy but untested — graphics_device.h says resources on a caller-created device are released with it and the destroy route checks no child count, but every C test destroys the resources first, so what later calls on those resource handles d
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Experimental C API: ABI version history · Experimental C API: gates and CI · Experimental C API: bindings boundary
- Architecture
- C API and bindings architecture
- Internals
- C API internals · C# binding internals · Java binding internals (archived) · Python binding internals (archived)
- Maintainer workflow
- Ownership and lifetime master map · Blast radius and readiness · Make a release
- Tests and validation
- Test architecture
- Reference
- Test target index