C API internals
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 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at 009d40f5 only. CNA_BUILD_C_API defaults to OFF and no CI workflow builds the library; no build or C API test was run for this page.
The C surface is not a thin symbol rename of C++ CNA. It is a second ownership model: versioned C structures, canonical UTF-8, string and boolean rules, result codes with a thread-local diagnostic, generation-checked typed handles, child-before-parent destruction gates and a lifecycle-callback adapter. This page traces how modules/c-api implements those rules at the TARGET snapshot, for maintainers who change a C route and for anyone who writes or reviews a language binding, which inherits every one of these choices even when its wrapper looks object-oriented.
Source-level only. Everything here was checked by reading modules/c-api at 009d40f5; nothing was built or executed. The option CNA_BUILD_C_API defaults to OFF, no CI workflow builds the library, and CNA's own release gate for ABI 0.29.0 reads “Not ready”. The user-facing picture (build, package, version history, coverage, gates) is on the Native C API guide; this page does not repeat it.
Build artifact and public boundary
cna_c_api (alias CNA::CApi) is compiled from 59 feature-family translation units, CnaCApi*.cpp, listed explicitly in modules/c-api/CMakeLists.txt; the other files under modules/c-api/src are private *Detail.hpp headers shared between families. The 61 public headers under modules/c-api/include/CNA/C are aggregated by cna.h. modules/c-api/include/CNA/C/abi.h carries the experimental ABI version (0.29.0: CNA_ABI_VERSION_MAJOR 0, _MINOR 29, _PATCH 0, packed by CNA_ABI_VERSION_ENCODE as major<<16 | minor<<8 | patch), the fixed-width CNA_Result (uint32_t, codes 0–14), CNA_Bool (uint8_t), CNA_Handle (uint64_t, CNA_INVALID_HANDLE = 0), the CNA_C_API export macro and cna_get_abi_version().
Two language levels apply and should not be confused. The public headers are held to a C99 consumer floor (the compatibility matrix in tools/c-api/compatibility_matrix.json requires c99/c11/c17 and c++11/14/17 cells; the shipped example is compiled at exactly C99), while CNA's own C test targets compile at C17 with extensions off and -Wall -Wextra -Wpedantic -Werror (/W4 /WX on MSVC).
| Artifact | When | How the surface is controlled |
|---|---|---|
Shared library libcna_c_api | Every non-Emscripten build with CNA_BUILD_C_API=ON | CXX_VISIBILITY_PRESET hidden and VISIBILITY_INLINES_HIDDEN; CNA_C_API marks routes visibility("default") (GCC/Clang) or dllexport/dllimport (Windows). On ELF only (UNIX AND NOT APPLE AND NOT EMSCRIPTEN) the link adds --exclude-libs,ALL and the version script CnaCApiExports.map: cna_* global, everything else local. INSTALL_RPATH "$ORIGIN". |
Static archive CNA::CApiStatic | Option CNA_C_API_BUILD_STATIC, Linux-like hosts with Python 3 | generate_static_archive.py partially links the closure into one object and localizes every non-cna_* symbol; consumers get CNA_C_API_STATIC, which empties the export macro. |
Wasm module cna_c_api_wasm | Emscripten (the library itself is then STATIC) | An ES module factory (cna_c_api.mjs + .wasm, createCnaCApi) whose -sEXPORTED_FUNCTIONS list is generated from the public headers by generate_wasm_exports.py; the headers are declared as inputs so a newly declared route cannot be silently missing. -sASYNCIFY=0: JavaScript owns the loop and calls the one-frame route (cna_game_run_one_frame) from requestAnimationFrame. module_entry.c pins the wasm32 layout of CNA_GameCallbacks and CNA_GameCreateInfo with _Static_asserts. |
The ELF symbol-version node is CNA_C_API_0.1 and is intentionally not the semantic ABI version: already-linked consumers record the node name, so renaming it on every minor would turn each additive release into a hard break (the map file's own comment says it changes only for a major break). These are three distinct final artifacts, not one library that works unchanged everywhere, and a route change has to be checked against each of them.
Route anatomy and the exception barrier
A fallible route follows one shape: validate the C-facing arguments, clear every out_ handle to CNA_INVALID_HANDLE, run the body inside CallWithExceptionBarrier, resolve typed native objects through the handle registry, then translate results back into POD values or caller-owned buffers. The order of the first two steps is itself a contract: a refused creation must leave its output invalid, which is why CnaCApiDetail.hpp tells authors to call ValidateCanonicalBool only after clearing the output.
CallWithExceptionBarrier is a noexcept template whose catch arms are ordered from most derived to most general, so C++ exceptions never cross the ABI. The mapping at TARGET, in arm order:
| Caught (in order) | CNA_Result |
|---|---|
std::bad_alloc | OUT_OF_MEMORY |
std::overflow_error, std::range_error | OVERFLOW |
std::out_of_range, std::invalid_argument | INVALID_ARGUMENT |
other std::logic_error | INVALID_STATE (deliberately not INTERNAL) |
std::ios_base::failure, std::filesystem::filesystem_error, System::IO::IOException, ContentLoadException | IO |
StorageDeviceNotConnectedException, InstancePlayLimitException, gamer-privilege / guide-visible, sensor failure, network-session join failure, network not available | INVALID_STATE (sensor and join failures also record their error id in the thread's diagnostic) |
NoAudioHardwareException, NoMicrophoneConnectedException, gamer services not available, game update required | NOT_SUPPORTED |
other NetworkException, CNA::Platform::PlatformException | PLATFORM |
CNA::Graphics::EngineException (CNAEXT builds only) | NOT_SUPPORTED |
CNA::CNAException, System::InvalidOperationException, DeviceLostException, DeviceNotResetException | INVALID_STATE |
System::ArgumentException | INVALID_ARGUMENT |
System::NotImplementedException, System::NotSupportedException, NoSuitableGraphicsDeviceException | NOT_SUPPORTED |
any other std::exception; ... | INTERNAL |
Because the arms are positional, changing the native exception hierarchy (a new base class, a re-parented exception) can silently move a failure into a different arm; PlatformException, for example, derives from std::runtime_error and must stay above the generic arm. A new native exception type therefore needs a C-ABI behaviour review, and BoundaryDetailTest.cpp (CApi_BoundaryDetail) pins the mapping for representative exceptions (argument, allocation, not-supported, device-lost family, unknown failure) together with the error-copy and string helpers.
Every arm goes through Fail, which writes a thread_local LastError (result, category, message, plus the optional join-error and sensor-error ids) in CnaCApiDetail.cpp. A successful call does not clear it; the three cna_error_* queries in core.h never overwrite it. CNA_ErrorCategory is derived from the result by ErrorCategoryForResult (OVERFLOW and BUFFER_TOO_SMALL share RANGE).
Opaque handle registry and actual ownership
CNA_Handle (uint64_t)
bits 63..32 generation 1 .. UINT32_MAX, bumped on every Release, wraps to 1 (never 0)
bits 31..0 slot index + 1 (0 is never a valid slot; handle 0 = CNA_INVALID_HANDLE)
HandleRegistry::Slot { generation, ObjectKind kind, shared_ptr<void> object,
std::thread::id creationThread, uint64_t userTag }
There is exactly one registry per process: GetRuntimeHandles() returns the HandleRegistry inside the static RuntimeState of CnaCApiRuntime.cpp. Create reuses the first slot whose object is empty (a linear scan) or appends a new one, records the kind, the calling thread and a zero user tag, and encodes the handle from the slot's current generation. Get<T> takes the registry mutex, rejects handle 0, slot 0, generation 0, an out-of-range slot, an empty slot or a generation mismatch (INVALID_HANDLE), then a different ObjectKind (INVALID_HANDLE), then a different creation thread (THREAD), and only then static_pointer_casts the stored shared_ptr<void>. Release performs the same lookup and thread check, moves the object out, clears the slot and bumps the generation; the moved-out shared_ptr is destroyed after the lock scope ends, so a destructor that re-enters the registry cannot deadlock. A stale handle cannot resolve to a reused slot until that slot's 32-bit generation has cycled.
Three consequences matter to binding authors. First, the kind check is the only type guard: a route that names the wrong C++ type for a kind would be undefined behaviour, which is why, for instance, every audio event registration is stored under one base class (AudioRegistrationBase in CnaCApiAudioDetail.hpp). ObjectKind enumerates 181 kinds (1–181) plus Unknown and a test-only value. Second, every handle resolved through Get<T> or released through Release is creation-thread-affine, not only some; only the internal GetKind query skips the thread check. Third, the mutex makes registry bookkeeping coherent; it does not make any CNA object thread-safe, and the per-resource counters below are plain integers protected only by that thread affinity.
What a handle actually owns
A handle's existence does not imply that it owns the native object. The shapes that coexist at TARGET (declared in CnaCApiRuntimeDetail.hpp and the family files):
| Shape | Example | Lifetime rule |
|---|---|---|
| Owned resource with parent token | SoundEffectResource, Texture2DResource | Holds the native object by shared_ptr plus parentGame; destroyed by its own route; counted against cna_game_destroy. |
| Owned child with parent reference | SoundEffectInstanceResource | Holds a shared_ptr to the parent resource and increments the parent's child count; the parent refuses destruction until it is zero. |
| Borrowed game device | BorrowedGraphicsDevice | A raw GraphicsDevice* plus an owner token (the game handle). Created lazily during a lifecycle callback and released when that callback returns. |
| Caller-created device | OwnedGraphicsDevice | A unique_ptr<GraphicsDevice> plus a BorrowedGraphicsDevice view whose owner token is the device's own handle, so resources on two standalone devices are as distinguishable as two games' resources. Not counted against game destruction. |
| Game content-manager borrow | cna_game_get_content_manager_ext | One cached handle over the game's value-member ContentManager (a non-owning aliasing shared_ptr); cna_content_manager_destroy refuses it with INVALID_STATE; released with the game. |
| Callback-scoped borrow | BorrowContentManagerForCallback | A fresh handle over whichever manager performs a .cnb load, wrapped with a no-op deleter and released by its creator before the loader callback returns. |
| Resource-to-resource references | Texture2DResource active batch/font/effect/model/scope counts | cna_texture2d_destroy answers INVALID_STATE while a live sprite batch, font, effect, model or scope still references the texture. |
Resource routes copy the owner token and compare it to reject cross-device use. These wrappers cannot be treated interchangeably by a binding. Before adding a route, decide whether its result is owned, shared, borrowed from a parent, callback-scoped or a copied value, and write the exact destroy and parent rules into the public header. The whole-engine ownership picture is on Ownership and lifetime master map.
Representative binding call: C game creation and one frame
language wrapper → cna_game_create(CNA_GameCreateInfo*, out handle) → CallWithExceptionBarrier → struct_size >= sizeof, struct_version == 1; out handle cleared → canonical is_fixed_time_step, ticks > 0, callbacks table valid or null → window title: valid UTF-8, embedded NUL refused, copied → RuntimeState lock: refuse if hasActiveGame (INVALID_STATE) → make_shared<CGame>(callbacks copied) → timing and title set → HandleRegistry::Create(ObjectKind::Game) → hasActiveGame = true cna_game_run_one_frame(handle) (also cna_game_run, cna_game_tick) → GetCallableGame: kind + creation thread + not inside a callback → Game::RunOneFrame → CGame overrides → C callbacks → returns the recorded callback failure (or SUCCESS) cna_game_destroy(handle) → GetCallableGame → owned-child gates → CGame::Shutdown → adapter/state resets → Release handle → hasActiveGame = false
CGame derives from the native Game and translates its virtuals into the C table CNA_GameCallbacks (load_content, update, draw, unload_content, exiting, one context) declared in runtime.h. Its Update and Draw call the C hook first and then Game::Update / Game::Draw, so game components still tick and draw and FrameworkDispatcher still advances (a comment in the source records that the base call was once missing). The optional frame hooks installed with cna_game_set_frame_hooks_ext (initialize, begin_run, end_run, begin_draw, end_draw) follow the canonical subclass shape: initialize runs before Game::Initialize, which is what ends by calling LoadContent, so a C game sees initialize before load_content as a ported XNA game expects; begin_draw can veto the frame through an out CNA_Bool.
Every callback invocation is itself run inside CallWithExceptionBarrier with isInsideCallback_ set. A non-success return (or a C++ exception from a C++-implemented callback) is recorded by RecordCallbackFailure: the optional CNA_CallbackError message (versioned, UTF-8, borrowed until the callback returns) becomes the thread's diagnostic with CNA_RESULT_CALLBACK, the failure is latched and Exit() is called. No C error code is thrown through native game execution; once latched, later callbacks and the base Update/Draw passes are skipped, and cna_game_run, _run_one_frame and even cna_game_destroy return the latched result.
A lifecycle callback may borrow the graphics device with cna_game_get_graphics_device; outside a callback that route answers INVALID_STATE, and the borrowed handle is released when the callback returns. GetCallableGame refuses to re-enter run, run_one_frame, tick or destroy from inside a callback (INVALID_STATE), while request_exit, clear, set_window_title and set_frame_hooks_ext are allowed there. The process admits one C-owned active game at a time; standalone devices from cna_graphics_device_create are a separate rule and do not need a game.
What cna_game_destroy does, in order
- Resolve the game through
GetCallableGame(kind, thread, not inside a callback). - Refuse with
INVALID_STATEwhile any C-owned game graphics resource, owned content manager, owned audio resource or owned game component is still alive. The four counters live in the process-wideRuntimeState; graphics resources are counted only when their owner token is a game (AddOwnedGraphicsResourceFor). CGame::Shutdown: theexitingcallback (once),unload_contentif content was loaded, thenDispose(), so subscribers still observe the game's ownDisposedevent, the disposal of its components (Disposed) and of the graphics device manager (DisposedandDeviceDisposing); the content manager is disposed too but has no event to observe. It does not dispose the game's canonicalGraphicsDevice:Game::Dispose(true)never does, and the device raisesDisposingonly from its destructor once the game object itself goes away, after the adapter state below has been reset. The source comment after theShutdowncall that says otherwise is inaccurate.- Drop the C-side state that referred into the game: graphics-device adapter state (event subscriptions, recorded sampler bindings), the platform override, game and window event registrations, the graphics device managers retained past their handles, and the borrowed game content-manager handle. The manager retention is deliberate: native
Gamecaches a raw pointer to the graphics device service, so releasing a manager's handle cannot free the manager while the game lives. - Release the game handle, clear
hasActiveGameand return the latched callback result.
A wrapper finalizer that destroys the game before its child handles violates this API even if the host garbage collector later frees the children. The native shutdown sequence the adapter drives is traced on Ownership and shutdown.
Representative child call: SoundEffect and instance
cna_sound_effect_create_pcm16 in CnaCApiAudio.cpp clears its output, then validates the versioned CNA_SoundEffectCreateInfo (size, version 1, zero reserved, sample rate in 1..INT32_MAX, channels exactly mono or stereo), the pointer/count pair through CheckedElementByteCount, a byte count that is nonzero, at most INT32_MAX and a whole number of 16-bit frames, and finally the game handle (ValidateActiveGameHandle, which also enforces the creation thread). It copies the caller's bytes into a native std::vector (so the language buffer need not stay pinned), constructs the native SoundEffect (a NoAudioHardwareException becomes NOT_SUPPORTED), wraps it in SoundEffectResource{value, parentGame, instanceCount = 0}, creates a SoundEffect-kind handle and increments the process's owned-audio count. cna_sound_effect_create_pcm16_range_ext, the route the C#, Java and Python binding pages trace at their pinned revisions, adds signed offset/count/loop checks against the buffer before the same copy and publishes through the shared PublishSoundEffect helper; content-loaded effects are published by the same factory (CreateOwnedSoundEffect), so every path yields the same kind of owned game child.
cna_sound_effect_create_instance resolves the parent, constructs a native SoundEffectInstance, wraps it with a shared_ptr to the parent resource, and increments both the parent's instanceCount and the owned-audio count. cna_sound_effect_destroy refuses with INVALID_STATE while instanceCount is nonzero, otherwise calls native Dispose, releases the handle and decrements the count. cna_sound_effect_instance_destroy disposes, releases, decrements its parent's count (a streaming instance has no parent) and the owned-audio count. This is stronger ordering than “shared_ptr keeps memory alive”: it preserves the public lifecycle contract and feeds the game-destroy gate.
Rules for a binding that wraps this sequence:
- Keep the exact
CNA_Handleas a 64-bit integer; never reinterpret it as a native pointer. - Do not destroy children from an arbitrary garbage-collector or finalizer thread: the route answers
CNA_RESULT_THREADand the handle stays live. - Check the result before reading an output handle; a failed create leaves it
CNA_INVALID_HANDLE. - Map host exceptions from
CNA_Resultplus the current thread's last diagnostic, read immediately. The store isthread_local, so a reader on another host thread does not see the failure, and a later failure on the same thread replaces it. - The message copy is size query followed by caller-owned buffer (
cna_error_get_last_message_size,cna_error_copy_last_message); keep both calls on the failing thread and handleBUFFER_TOO_SMALL, which writes nothing and reports the required size.
The pure-C test that pins this contract is AudioSmoke.c (CApi_AudioSmoke): with an instance alive it expects cna_game_destroy and cna_sound_effect_destroy to answer INVALID_STATE, from a second thread it expects THREAD for a query and for instance destruction, and afterwards child-first destruction, INVALID_HANDLE on a double destroy and on use of the destroyed game. AudioSoundEffectSmoke.c (CApi_AudioSoundEffectSmoke) covers the extended creation routes (range, encoded, asset), process-wide 3D-audio settings and cna_sound_effect_play. The native voice and mixer path behind these handles is on Audio engine internals.
Validation, ABI evolution and tests
Conventions every route enforces
- Versioned structures. The routes read here accept a structure when
struct_size >= sizeof(the current struct) andstruct_version == 1; a smaller prefix is refused withINVALID_ARGUMENT, a larger future struct passes the check. Validators must check both before reading a new field. - Strings.
CNA_StringViewis a borrowed{data, byte_length}span valid for one call, not a NUL-terminated or long-lived string.ValidateStringViewacceptsdata == NULLonly with length 0, rejects overlong encodings, surrogates and code points above U+10FFFF (ENCODING), and optionally rejects embedded NUL;CopyStringViewvalidates before copying. The rules are cross-checked byUtf8OracleTest.cpp(CApi_Utf8Oracle) and a libFuzzer source compiled but not registered as a test. - Booleans. Only
CNA_FALSE(0) andCNA_TRUE(1) are valid;ValidateCanonicalBoolnames the offending parameter. Since ABI 0.3.0 every route taking one refuses other bytes; the generatedCApi_BoolContractSmoketest and theCApiBoolContractCurrentgate keep that true. - Buffers.
CheckedElementByteCountmultiplies element count by element size with overflow checks against bothuint64_tandsize_t(OVERFLOW) before any caller memory is touched.
Changing any of these conventions affects every consumer at once: C programs, the external C#, Java, Python, Rust, TypeScript, Swift, Go and Ruby projects, and wasm callers.
Adding or changing a route
- Decide whether the C++ addition belongs in the C ABI at all (CNA's coverage model records some public C++ symbols as not applicable to C).
- Declare it in the owning public header with its ownership, thread and result contract written out.
- Implement it in the owning
CnaCApi*.cppfamily: output cleared first,CallWithExceptionBarrier, handle kind and thread through the registry, validation as above, child counts if it creates a child. - Apply the ABI version policy in
ABI_VERSIONING.md: while 0.x, an incompatible change needs a minor increment, release notes and a regeneratedabi_baseline.json(the CMake package version is read out ofabi.h). - Audit the exported surface:
CApiDeclaredExports(check_declared_exports.py, declared vs exported in both directions),CApi_Exports(ELF export check), the generated wasm export list, andCApiRouteTestCoverage, whose budget of uncovered routes is 0, so the new route must be named by a test or example. - Add pure-C tests under
tests/pure_cplus focused boundary or handle tests, then run the installed-consumer check. - Only then update each external binding, at a recorded revision.
A route added to the header and C++ but missing from the wasm export list is invisible to browser callers; the generator exists precisely because that failure produces no build diagnostic. Test the final artifacts, not just static compilation. The step-by-step maintainer recipe is I need to update the C API.
Tests and gates to know
| Evidence | What it checks | Where it runs |
|---|---|---|
AbiHeaderC.c, AbiHeaderCpp.cpp | Every public header compiles as C17 and as C++ under strict warnings (object libraries, not CTest entries) | C API builds |
CApi_HandleRegistry, CApi_BoundaryDetail, CApi_Utf8Oracle | Registry user tags, off-thread THREAD refusal, double release (INVALID_HANDLE) and slot reuse under a new generation; representative barrier mappings and the error-copy/string helpers; the UTF-8 oracle (C++ test programs, not GoogleTest) | C API builds |
85 pure-C programs (CApi_*Smoke, CApi_TeardownLifetime_*, CApi_AudioSmoke, CApi_RuntimeGameSmoke, CApi_OwnedGraphicsDeviceSmoke …) | Route behaviour, lifetime and refusal contracts from real C | C API builds |
CApi_InstalledConsumer | Install the CNACApi component, build hello_cna shared and static from outside the tree, run both | Local CTest, Linux/ELF only |
CApiAbiHeaderBaseline, CApiDeclaredExports, CApiRouteTestCoverage, CApiBoolContractCurrent, CApiReleaseGate and the other header/JSON gates in ModuleProbes.cmake | Recorded layouts and constants, export sets, route-test coverage, release verdict | Ordinary build (no C library needed for most) |
None of these was executed for this page; CNA's own records of a ctest -R '^CApi' run are summarised on the C API guide's gates section. The wider test layout is on Test architecture.
Known evidence gaps
- This page establishes the in-tree ABI at source level. It does not establish that the library builds at
009d40f5, nor binary compatibility of any external binding. The public bindings target ABI 0.21.x, while TARGET exports 0.29.0 after two incompatible steps (0.28.0, 0.29.0); see Bindings boundary and the C#, Java and Python binding internals, each pinned to its own revision. - Some routes have callback, thread or borrowed-resource rules beyond this general model (event registrations,
.cnbloader callbacks, streaming audio, storage). Read the route's header and implementation rather than extrapolating from the SoundEffect example. cna_graphics_device_destroyfor a caller-created device disposes and releases it without consulting a child count in its route body;graphics_device.hdocuments that the device's resources are released with it (GraphicsDevice::Disposedisposes its resource list), so destroying the device first is permitted. The pure-C smoke destroys resources first, and no test pins what a child handle answers after its device is gone.
Source reading order
abi.h,core.handruntime.h: wire types, error queries and the game ownership statements before any implementation.CnaCApiDetail.hppandCnaCApiDetail.cpp: exception arms,LastError, string/bool/buffer checks, handle encoding, generation and thread enforcement.CnaCApiRuntime.cppandCnaCApiRuntimeDetail.hpp:CGame, callback latching, borrowed devices, the child counters and the destroy sequence.CnaCApiAudio.cppwithAudioSmoke.candAudioSoundEffectSmoke.c: one owned parent/child family against its C-only tests.CnaCApiContent.cpp(borrowed and callback-scoped content managers) andCnaCApiDisplay.cpp(caller-created devices): the two ownership shapes that differ most from the audio example.c-api CMakeLists.txtandCnaCApiExports.map: the desktop, static and wasm artifacts and the test registrations before changing the exported surface.
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.29.0 reaches, what each release-gate criterion really checks, why the gate measures two unmet criteria at 009d40f5, and how the coverage inventory classifies modules.
- Native C API contract: admission, buffers, retention and route families — What CNA's C ABI 0.29.0 version checks admit, the exact count-then-copy protocol, callback and registration lifetimes, resources that retain others, caller-created devices and all 61 headers by family.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-056: docs/c-api/FEATURE_MATRIX.md and docs/c-api/README.md still present the C ABI as version 0.1.0 while abi.h declares 0.29.0 — The release gate now reads its ABI label from abi.h, but the C API feature matrix is titled '0.1 Feature Matrix' with an ABI row of 'Experimental version 0.1.0', and the C API README describes the library as 0.1.0.
- CNA-BUG-062: MODULE_SCOPE in generate_coverage_inventory.py leaves design, diagnostics and inspector unclassified, so every C API inventory gate stops — The C API coverage generator refuses to run because three public modules are missing from MODULE_SCOPE, so the coverage, limitations, scope-model and release-gate checks fail at this snapshot and the committed coverage n
- CNA-BUG-067: Math argument failures throw a mix of Sharp Runtime and std:: exception types that differs between near-identical functions — BoundingBox throws Sharp Runtime ArgumentException types as XNA does, but BoundingSphere::CreateFromPoints, BoundingFrustum::GetCorners and the perspective builders throw std::invalid_argument or std::out_of_range.
- CNA-BUG-212: cna_graphics_resource_get_graphics_device returns CNA_RESULT_INVALID_HANDLE for every resource created on a caller-created GraphicsDevice — A resource made on a device from cna_graphics_device_create carries that device's handle as its owner token, and the route passes it to the game-only BorrowGameGraphicsDevice, so asking such a valid resource for its devi
- CNA-BUG-213: audio.h and AUDIO.md call cna_dynamic_sound_effect_instance_submit_buffer safe from a producer thread, but every call off the creation thread returns CNA_RESULT_THREAD — The submission routes resolve the instance through HandleRegistry::Get, which refuses any thread other than the handle's creation thread, so the documented canonical producer-thread usage fails; AUDIO.md's own threading
- CNA-BUG-214: docs/c-api/HANDLES.md documents CNA_RESULT_SHUTTING_DOWN for a closing or closed runtime, but no C API route ever returns it — abi.h defines CNA_RESULT_SHUTTING_DOWN and HANDLES.md maps a closing runtime to it, but no route returns it and the registry has no shutdown state, so a stale handle answers CNA_RESULT_INVALID_HANDLE.
- CNA-BUG-218: content_readers.h documentation names cna_get_last_error_message, which does not exist — The Doxygen of cna_object_dictionary_ext_copy_value tells callers to read 'the message cna_get_last_error_message returns', a function declared nowhere; the C API's error accessors are the cna_error_* functions in core.h
- CNA-BUG-220: cna_game_destroy's comment says Shutdown has disposed the canonical graphics device; it has not — A comment in cna_game_destroy claims CGame::Shutdown already disposed the game's canonical GraphicsDevice so C subscribers saw its Disposing event, but Game::Dispose(true) never disposes the Game-owned device.
- CNA-VGAP-053: No CI workflow builds the C API library or runs its tests — No workflow at this snapshot configures CNA_BUILD_C_API=ON; the C API workflows only run generators, inventories, a release gate and a header compile matrix, so the library and its tests are verified only in local builds
- 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.
- Internals
- C# binding internals · Java binding internals · Python binding internals · Ownership and shutdown
- Maintainer workflow
- I need to update the C API · Thread and callback map
- Tests and validation
- C API gates and CI · Test architecture
- Reference
- Public header index · Test target index