C API and bindings architecture

CNA snapshot c1c316b9  ·  Development › Architecture Maps  ·  source links pinned to c1c316b9

✓

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 modules/c-api (abi.h, the detail header, CMakeLists.txt), the ABI documents and the module probes; nothing was built. The external repositories are evidence for their own pinned revisions, and the C library was not built or tested for this page.

The CNA repository contains exactly one binding foundation: an explicitly experimental C API, modules/c-api, whose ABI version at this snapshot is 0.46.0. Two language bindings are actively maintained: C (this ABI itself) and C# (CNA.NET), a separate repository that consumes it. Eight bindings are archived: Common Lisp, Go, Java, Python, Ruby, Rust, Swift and TypeScript (which includes JavaScript). They are no longer maintained, not part of the current support policy and without any forward compatibility guarantee, and their source and Git history are kept in CNA Lab. None of the external bindings is generated from CNA, each pins its own ABI generation, and none must be described as automatically compatible with this snapshot. This page states the boundary rules a C-ABI change has to respect and what a maintainer owes the bindings when it moves; the routes, handle registry and per-layer policies are in C API internals.

Native C API

modules/c-api supplies 60 public C headers under CNA/C/, a shared library cna_c_api (alias CNA::CApi; a static library under Emscripten, which is linked into the cna_c_api_wasm module target), an optional relocatable static archive (on Linux-like hosts; under Emscripten it is merged from the wasm module's closure) and a WebAssembly adapter path. It is an opt-in target: CNA_BUILD_C_API defaults to OFF and requires CNA_ENABLE_NET=ON because the library links the GamerServices and Net modules (see the module graph). On ELF a linker version script exports cna_* and nothing else, and the export list for the WebAssembly module is generated from the headers rather than maintained by hand.

  • Opaque, generation-checked handles keep C++ types out of the ABI. A CNA_Handle is a 64-bit value holding a slot index and a generation; a zero, stale, wrong-kind or foreign handle answers CNA_RESULT_INVALID_HANDLE. The registry records the object kind and the creating thread.
  • No exception crosses the boundary. Entry points run inside CallWithExceptionBarrier (CnaCApiDetail.hpp), which maps C++ exceptions to one of the fixed CNA_Result codes (0 success through 14 buffer too small) and records a per-thread last-error message that query routes never overwrite. Callbacks run synchronously on the game's thread with borrowed handles, and a callback that fails stops the game with CNA_RESULT_CALLBACK. A host that owns its loop, such as a browser page, can drive a run one frame per call with cna_game_run_frame_ext (0.38.0; C++ Game::RunFrameEXT), which delivers the same initialize, begin-run, exiting and end-run sequence as cna_game_run.
  • The ABI version is exact, not stable. CNA_ABI_VERSION (0.46.0, in abi.h) is unrelated to the product version, and while the ABI is 0.x an incompatible change takes a minor increment, release notes and a regenerated baseline. Of the seventeen minors from 0.30.0 to 0.46.0, the six from 0.30.0 to 0.35.0 were incompatible, 0.36.0 to 0.39.0 and 0.41.0 were additive, 0.40.0, 0.42.0, 0.43.0 and 0.44.0 changed a rule only (the export set stayed the same, but a call that was refused before is now accepted), 0.45.0 removed enumeration constants and reserved their values, and 0.46.0 appended one native window-system value, CNA_NATIVE_WINDOW_SYSTEM_UIKIT, without changing a route, structure or export. Consumers must therefore validate the exact exported version instead of assuming compatibility. The history is in ABI_VERSIONING.md and summarized on the C API guide.
  • Build status is source-level. No CNA CI workflow builds the C library at this snapshot: the C-API workflows are header, JSON and documentation gates, and CNA's own release gate reports the surface as not ready. Whether the library builds and passes at exactly this commit was not established for this page.

Boundary checklist

  1. State who creates and destroys every handle, and in what order. Owned children are released before their parent, on the creating thread; cna_game_destroy answers CNA_RESULT_INVALID_STATE while owned graphics resources, content managers, audio resources or components exist, and refuses from inside a lifecycle callback. A wrong-thread call answers CNA_RESULT_THREAD, so a foreign garbage collector on another thread cannot be relied on for teardown order.
  2. State which thread every call is made on. The creating-thread rule has three documented exceptions, all opt-in or narrow: with cna_game_set_foreign_thread_calls_ext (0.39.0, off by default) a call refused only because another thread created the handle is queued, the caller blocks, and the game thread runs it at the start of its next update or draw (cna_game_run_foreign_thread_calls_ext, 0.41.0, drains the queue while the game thread waits for such a thread); cna_sound_effect_play and cna_sound_effect_play_with_settings answer on any thread (0.43.0); and standalone devices may be created and destroyed on several threads at once. Driving or destroying the game is still refused off its thread, and a game thread that waits for a thread whose call it must serve deadlocks unless it drains the queue.
  3. Define whether every string and buffer is borrowed, copied or transferred, and for how long. The conventions are UTF-8 CNA_StringView in, count-then-copy out (a too-small buffer is refused with no partial write), CNA_Bool restricted to 0 and 1, and versioned input structs that start with struct_size and struct_version.
  4. Convert every failure to the C error contract before returning; an exception that escapes the barrier is a defect.
  5. Keep the public headers valid C: they are compiled alone and through the umbrella at a C99 floor (with C11 and C17, and C++11 to C++17 as required cells), so do not leak C++ syntax, namespaces, templates or C++-only types into them.
  6. Test double-destroy, null and wrong-type handles, callback re-entry, wrong-thread calls and shutdown order.

The tests live under modules/c-api/tests: 93 pure-C programs, four C++ tests (including HandleRegistryTest.cpp and BoundaryDetailTest.cpp, registered as CApi_HandleRegistry and CApi_BoundaryDetail) and one libFuzzer source. They are built and registered only when -DCNA_BUILD_C_API=ON, and none was executed for this page. The lifetime rules in more detail are CNA's own OWNERSHIP.md, HANDLES.md and CALLBACKS_AND_THREADING.md, and the ownership map places handles beside the C++ lifetimes.

External language repositories

Maintained. Together with C, which is the in-tree ABI above, this is CNA's actively maintained language binding.

BindingRepositoryABI generation targetedWhat this page supports
C# / .NET (CNA.NET)libcna/cna-dotnet (8219a6c, as recorded by CNA)Exactly 0.46.0Beta and source-first, with separate versioning; it admits one reviewed ABI point, not a range, and its README and compatibility data are authoritative

Archived — not maintained. These eight were developed to an advanced state and archived at their final revisions. Seven of them were requalified against C ABI 0.35.0 on 2026-09-30 before they moved into CNA Lab; Common Lisp had stopped earlier, at the 0.21–0.23 generation. They are not part of the current support policy, a native API change is not propagated to them, and there is no forward compatibility guarantee. Their source and Git history are kept under bindings/ in CNA Lab (libcna/cna-lab); where each final revision can be read is listed under archived bindings in the C API guide.

Archived bindingLocation in CNA LabABI generation targetedWhat this page supports
Common Lispbindings/cna-common-lisp0.21.0, 0.22.0 or 0.23.0Record of its final revision only; a selected subset, qualified on SBCL/Linux only
Gobindings/cna-goMinor 35 or later (requalified against 0.35.0)Record of its final revision only; its loader policy is its own
Javabindings/cna-javaExactly 0.35.0Record of its final revision only; its internals page is kept for study
Pythonbindings/cna-pythonExactly minor 35Record of its final revision only; its internals page is kept for study
Rubybindings/cna-rubyExactly 0.35.0Record of its final revision only; its loader admits only its own version set
Rustbindings/cna-rustExactly minor 35Record of its final revision only; its crate and ownership mapping are not updated
Swiftbindings/cna-swiftMinor 35 or later (requalified against 0.35.0)Record of its final revision only; its loader policy is its own
TypeScript and JavaScriptbindings/cna-tsExactly minor 35Record of its final revision only; its Node-API bridge and WebAssembly decisions are its own
⚠

Compatibility is not established by this source revision. This snapshot exports 0.46.0. The maintained C# binding admits exactly 0.46.0: CNA's Apple campaign records that cna-dotnet 8219a6c moved its policy, constants, tests and CI pins from 0.44.0 to 0.46.0 after an export-by-export comparison, and that 0.45.0 was never admitted (AM4-217 in plan_apple_m4.md). That revision was not read for this page, so the admission is CNA's record, not a run. The archived bindings target earlier generations: Common Lisp, Java, Python, Ruby, Rust and TypeScript refuse a 0.46.0 library by an exact-version rule, and Go and Swift would admit it by a minimum-minor rule but were qualified only against 0.35.0. Only the maintained C# binding is expected to migrate. Consumer projects are evidence for their own pinned revision, not for this one. Before changing the native API, read each repository's current compatibility matrix and CI; never claim that the latest binding works with the latest CNA. The per-binding pinned commits and loader behavior, maintained and archived, are tabulated on the C API guide.

Propagating a native API change

  1. Change and test the owning C++ module first.
  2. Decide whether the operation belongs in the experimental C ABI at all. The generated coverage inventory (generate_coverage_inventory.py, output in the C API coverage document) maps every public C++ symbol to implemented, partial, planned or not applicable; the C API guide gives the counts.
  3. Update the C header, the implementation, the symbol and export rules and the C API tests together. The header half of the ABI baseline gate (CApiAbiHeaderBaseline, from generate_abi_baseline.py --check against abi_baseline.json) runs in an ordinary build and names a moved field, a changed constant or a vanished export as a break; CApiAbiBaseline and CApiDeclaredExports additionally need the built library on ELF, and CApiReleaseGate reports the release verdict. Additive changes are re-recorded; an incompatible one requires the version increment above.
  4. For the maintained external binding (C#), pin a compatible native artifact, update its generated declarations and its handwritten ownership and marshaling code, and let its own compatibility policy decide what it accepts. Removing a route (as several of the 0.30.0 to 0.35.0 steps did) breaks a binding at compile or link time, not at run time; a rule-only step (0.40.0, 0.42.0 to 0.44.0) passes every link check but changes which calls succeed, so it needs a review of the binding's own refusals and threading.
  5. Run binding tests under sanitizers where the foreign runtime permits it, and treat the C library's own smoke and sanitizer configurations as the C-side evidence. The eight archived bindings are not updated: a native change is not propagated to them, and nothing in CNA's release process waits for them.

The step-by-step recipe is update the C API. The C# binding page and the archived Java and Python pages describe how those layers apply the ownership rules.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture