Native C API contract: admission, buffers, retention and route families
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 from modules/c-api (headers, sources, tests) and docs/c-api at 009d40f5. The C library was not built and no route or test was executed; the admission snippet was syntax-checked with gcc -std=c99/-std=c17 -fsyntax-only only. Destroying a caller-created device before its resources and submitting streaming audio from another thread are established by reading, not by any test.
This page is the detailed contract of CNA's experimental native C API (ABI 0.29.0) for people who write a C program or a language binding against it. It collects the rules that decide whether a program works in practice and that the overview pages state only in outline: what each form of version check actually admits, the exact caller-buffer protocol, how callback contexts and registrations live, which resources keep other resources alive, how a caller-created graphics device differs from a game's device, and how the 4,055 routes divide into families. Everything was read from the source at snapshot 009d40f5; the C library was not built and no route was executed, so this is the declared and implemented contract at source level, not a verified binary.
The overview (build options, packaging, ABI history, coverage figures, bindings) is on the Experimental C API guide; the implementation trace (route anatomy, exception barrier, handle registry, cna_game_destroy) is on C API internals; what the gates and tests do and do not establish is on C API evidence, coverage inventory and release gate. A complete first program is Tutorial 129. This page does not repeat those.
Admitting a library: what each version check accepts
cna_get_abi_version() returns the CNA_ABI_VERSION constant the library was compiled with (CnaCApi.cpp); it needs no runtime, game or device, so it is the first call a program should make. The value is encoded by CNA_ABI_VERSION_ENCODE in abi.h as major<<16 | minor<<8 | patch (0.29.0 is 0x00001D00). What a program does with it is a policy choice, and the common choices differ sharply while the ABI is 0.x, because two recent minor steps (0.28.0 and 0.29.0) were incompatible removals.
| Check | Where it appears | Headers 0.21.0, library 0.29.0 | Headers 0.29.0, library 0.28.0 | What it misses |
|---|---|---|---|---|
Exact equality with CNA_ABI_VERSION | the guide's example | refused | refused | Nothing it admits can differ, but it also refuses a patch-level rebuild. |
| Same major, same minor while 0.x, not older | the syntax-checked snippet below | refused | refused | Equivalent to exact equality on the minor; admits a newer patch only. |
| Same major, then not older | hello_cna.c (the shipped example) | admitted | refused | Admits every newer 0.x minor, including the incompatible ones. |
Not older only (cna_get_abi_version() < CNA_ABI_VERSION refuses) | a common shortening of the above | admitted | refused | Also admits a future major version; never write it without the major comparison. |
find_package(CNA 0.1 CONFIG) | the consumer's CMake project | compares the installed package version (read from that package's abi.h) with SameMajorVersion at configure time | Says nothing about the library that is loaded at run time. | |
ELF symbol version CNA_C_API_0.1 | the dynamic linker | identical for every 0.x release; it is deliberately never bumped for a minor | Cannot tell any two 0.x libraries apart. | |
Three separate things decide whether an old program runs against a new library, and a version check covers only the first. The check decides whether the program chooses to run. Linking decides whether every route it calls still exists: 0.29.0 removed cna_sprite_batch_draw_mesh_ext, so a program that references it fails when the dynamic linker binds that symbol, whatever its version check said. Behaviour decides the rest: a documented contract change (0.3.0 made every CNA_Bool route refuse bytes other than 0 and 1; 0.9.0 changed seven documented contracts) passes both the admission check of a program that accepts newer minors and the link, and shows up only as different results. That is why the strict check is the safe default under 0.x, and why the looser one in hello_cna.c, whose comment reasons that "a lower minor lacks routes this program may have been compiled against", is a bet on additive evolution that the version history does not support.
A strict admission check
#include <CNA/C/abi.h>
#include <stdint.h>
#include <stdio.h>
/* Returns 1 when the loaded library may be used by code compiled against these headers. */
static int cna_abi_admits(void)
{
const uint32_t runtime_abi = cna_get_abi_version();
const uint32_t runtime_major = runtime_abi >> 16U;
const uint32_t runtime_minor = (runtime_abi >> 8U) & 0xFFU;
if (runtime_major != CNA_ABI_VERSION_MAJOR) {
return 0; /* a different major is never compatible */
}
if (CNA_ABI_VERSION_MAJOR == 0U && runtime_minor != CNA_ABI_VERSION_MINOR) {
return 0; /* 0.x: any other minor may be incompatible */
}
if (runtime_abi < CNA_ABI_VERSION) {
return 0; /* older patch than the headers */
}
return 1;
}
int main(void)
{
if (!cna_abi_admits()) {
fprintf(stderr, "CNA C ABI 0x%08x does not match the headers (0x%08x)\n",
(unsigned)cna_get_abi_version(), (unsigned)CNA_ABI_VERSION);
return 1;
}
return 0;
}
The snippet was syntax-checked with gcc -std=c99 and -std=c17, -Wall -Wextra -Wpedantic -Werror -fsyntax-only, against the public headers at 009d40f5; it was not linked or run. It needs only abi.h, which includes nothing but the C standard headers. When CNA reaches ABI 1.0 (a decision its release gate explicitly does not make) the second condition drops out by itself and the check becomes "same major, not older", which is the rule the 1.x policy is designed for.
The same reasoning gives the short list that a binding or long-lived C program should follow: require one ABI version and say which; branch on capability and renderer-feature queries rather than renderer names or numbers; treat the ownership, thread and child-order rules below as part of the ABI; and keep a consumer test on every platform you ship, because none of those rules is exercised by CNA's continuous integration (see the evidence ladder).
Caller buffers, strings and arrays
Every variable-length output uses a count-then-copy pair and the caller's memory; the ABI never allocates an output on the caller's behalf. The rules are written in STRINGS_AND_BUFFERS.md and the simplest implementation of them is cna_error_copy_last_message in CnaCApi.cpp, which the table follows case by case.
| Destination | Capacity | Result | Written |
|---|---|---|---|
| any | any, with a null count output | CNA_RESULT_INVALID_ARGUMENT | nothing |
| null | nonzero | CNA_RESULT_INVALID_ARGUMENT | nothing |
| null | zero | CNA_RESULT_BUFFER_TOO_SMALL, or success when the required count is zero | the required count only |
| valid | smaller than required | CNA_RESULT_BUFFER_TOO_SMALL | the required count only; not one byte of data |
| valid | at least the required count | CNA_RESULT_SUCCESS | the count and exactly that many bytes or elements |
A too-small buffer therefore never receives a prefix: the protocol is all-or-nothing, which is stronger than merely not splitting a UTF-8 character, and the full required count is always reported so a caller can allocate once and repeat. The count is in bytes for text and in elements for typed arrays (for example cna_graphics_renderer_copy_available_ext counts CNA_GraphicsRendererType values), and it never includes a terminator: copied text is raw UTF-8 without a trailing NUL, and a NUL-terminating variant exists only where a declaration documents one. Error-message routes are also exempt from overwriting the thread's last diagnostic, so a program can query and copy it without losing it.
Inputs are borrowed. A CNA_StringView is {data, byte_length}, valid only for the call, never read past its length and not NUL-terminated by contract; data == NULL is legal only with length zero. Text must be valid UTF-8, and where the value is a path, asset name, window title or identifier an embedded U+0000 is refused with CNA_RESULT_ENCODING, so a consumer cannot rely on C-string truncation. Arrays are a pointer plus a uint64_t count whose unit the route names; null with a zero count is valid, null with a nonzero count is CNA_RESULT_INVALID_ARGUMENT, and the element-size multiplication is overflow-checked before any caller memory is touched. An array is read during the call and not retained unless the declaration says it copies: the PCM creation routes and cna_dynamic_sound_effect_instance_submit_buffer copy their bytes (the latter's header says the caller's buffer "may be reused or freed the moment this returns"), and cna_graphics_renderer_set_fallback_chain_ext copies its identity array into the process-wide selection before returning.
Callback tables, contexts and registrations
A callback is a typed C function pointer plus a caller-owned void* context; CNA never takes ownership of the context memory. The rules in CALLBACKS_AND_THREADING.md and OWNERSHIP.md, which the runtime and family sources follow:
- Game callbacks.
cna_game_createcopies theCNA_GameCallbackstable, but the functions it points to and its context must stay valid untilcna_game_destroyreturns.CNA_GameTimeis non-null only in the update and draw callbacks; it is null for load, unload and exit. - Event registrations are owned handles. Subscribing returns a registration handle; destroying it removes the callback, and the ownership document says the removal synchronizes with any documented in-flight invocation before the call returns. Destroying the event's source removes every registration and invalidates their handles. The caller's context must remain valid until the registration is destroyed or the source has finished destruction. Registrations are the deliberate exception to the game-child counters: they do not block
cna_game_destroy. - Callback-scoped handles are borrowed. The game handle passed to a lifecycle callback and the device from
cna_game_get_graphics_deviceare valid only for that callback; CNA generation-invalidates the device handle before returning to the loop, so a stored copy later answersCNA_RESULT_INVALID_HANDLE. Objects created from that borrowed device (a texture, a SpriteBatch, a content manager) are owned game children and survive the callback.
No route at 009d40f5 provides a deferred-release or main-thread dispatch queue, and every handle is resolved through the registry, which refuses a thread other than the one that created the handle with CNA_RESULT_THREAD. Streaming audio is no exception in the implementation: the header and AUDIO.md say that copying the submitted buffer makes submission "safe from a producer thread while playback runs", but cna_dynamic_sound_effect_instance_submit_buffer looks its instance up through the same thread-checked registry call as every other route (CnaCApiAudio.cpp, BorrowDynamicInstance), so a call from a producer thread answers CNA_RESULT_THREAD (by reading; no test submits from a second thread; see Known Issues). A binding or program with a producer or finalizer thread must hand the work to the creation thread itself.
Resources that keep other resources alive
Several families hold references between C handles, and a destroy route refuses with CNA_RESULT_INVALID_STATE while such a reference is live. These are separate from the game-child counters described on C API internals.
SpriteBatch submissions retain textures until a successful end
The C SpriteBatch draws only through bulk submissions (cna_sprite_batch_submit_many, cna_sprite_batch_submit_scaled_many) and cna_sprite_batch_draw_string. A bulk submission in CnaCApiGraphics.cpp first resolves every command's texture and refuses the whole submission with CNA_RESULT_INVALID_HANDLE if one belongs to a different game than the batch; only then does it retain each texture (incrementing its activeBatchReferenceCount and keeping a shared reference in the batch) and forward the draws; drawing a string retains the font's texture the same way. cna_texture2d_destroy refuses while that count, or the texture's font, effect, model or render-target-scope count, is nonzero. cna_sprite_batch_end calls the native End first and releases the references only after it returns; if End throws, the barrier converts the failure, the batch stays inside its begin/end interval and the textures stay retained. The explicit recovery is cna_sprite_batch_destroy, which, during an active interval, drops the deferred commands, releases every texture reference, disposes the batch and releases its handle. Immediate-mode commands that already reached the renderer cannot be undone. The consequence for a binding: destroying a texture that was drawn in the current interval is a state error, not a use-after-free, and an exception during End leaves a state that only destroying the batch clears.
Loaded content handles outlive the manager's cache
Each typed load (for example cna_content_manager_load_texture2d in CnaCApiContent.cpp) calls ContentManager::Load<T>, copies the returned value into a new object and publishes it as a separate owned handle whose parent is the game. cna_content_manager_unload only calls the native Unload, which in ContentManager.cpp clears the manager's cache of loaded assets; neither it nor cna_content_manager_destroy touches handles already issued to C. A loaded texture therefore remains a valid handle after its manager is unloaded or destroyed and keeps its own obligation to be destroyed before the game; a second load of the same name after an unload goes back to the storage ladder and yields a new handle. The manager a game owns (cna_game_get_content_manager_ext) is borrowed: destroying it is refused with CNA_RESULT_INVALID_STATE, and it is released with the game. Handle validity after Unload follows from the code; no test uses a loaded resource after unloading its manager. The content routes as a family, and the CNB routes beside them, are described on CNB format: the C ABI surface.
Model parts retain their render bundles
An owned skinned-model handle can retain complete same-device bundles of vertex buffer, index buffer, mesh part and optional texture. The caller still owns each original handle, but its typed destroy and the generic graphics-resource disposal refuse while a live model part retains it; replacing a part by name, removing it, replacing the model or destroying the model releases the retained references. Indexed mesh-part queries return a new owned alias, while the optional texture query returns the existing handle, whose lifetime the model protects (OWNERSHIP.md; the texture-side gate is the model reference count checked by cna_texture2d_destroy).
Routes that consume a handle
OWNERSHIP.md gives every handle and pointer parameter one of six categories: owned (release exactly once before runtime shutdown), borrowed (usable only for the call or callback), retained (CNA keeps an extra reference or a copy), transferred (CNA consumes ownership on a successful return), callback context (the caller keeps the void* valid until unregistration completes) and caller buffer (caller storage for the whole call; CNA stores it only where the route says it copies or retains). Transfer is the rare one, and it is the only category that makes a handle you still hold stop existing.
At 009d40f5 three routes, all in the CNAEXT engine layer, take an object over from a handle: cna_post_process_effect_pass_create_owning (the pass owns the effect), cna_post_process_chain_add_owned_pass (the chain owns the pass) and cna_skybox_set_owned_environment (the skybox owns the cube map). Each releases the caller's handle from the registry on success, so the caller must not destroy it afterwards, and each is a separately named route rather than a flag on the borrowing form (cna_post_process_chain_add_pass, cna_skybox_set_environment). The implementations release the handle only after every check that can fail, so a refusal (an invalid handle, an effect from another game, a pass that is still lending its effect) leaves it valid and still yours: read the result before dropping it. The @param lines of cna_post_process_chain_add_owned_pass and cna_skybox_set_owned_environment say the handle is 'invalid on return whether or not the call succeeded'; the code does not do that and the rest of the same comments contradict it (CNA-BUG-270; by reading, and no test exercises a refused hand-over). Routes that only copy or retain never consume: cna_media_queue_add appends a copy of the song, and cna_effect_material_retain_parameter_texture_ext lets a material keep a texture alive while the caller may still release its own handle. The move routes (cna_skinned_model_ext_create_move, cna_skinned_model_ext_move_assign) leave their source valid but empty. A binding should model 'moved-from on success' for the first three instead of treating every handle as owned until its own destroy call.
Game devices and caller-created devices
The ABI hands out graphics devices in two ways, declared in graphics_device.h, and they have different lifetime rules.
| Property | A game's device | A caller-created device |
|---|---|---|
| Obtained by | cna_game_get_graphics_device, only inside a lifecycle callback | cna_graphics_device_create (adapter index, graphics profile, versioned CNA_PresentationParameters); no game needed, and several may exist at once |
| Handle lifetime | Borrowed; invalidated when the callback returns | Owned; ends at cna_graphics_device_destroy, which refuses any other kind of device handle |
| Owner of its resources | The game: resources count against cna_game_destroy | The device itself; its resources do not gate cna_game_destroy |
| Cross-device use | Refused in both directions: a resource remembers the device that made it, whether the other device is a game's or caller-created | |
| Destroying the device while resources live | Not possible: the game refuses to be destroyed first | Permitted by the header ("released with it"); see below |
cna_graphics_device_destroy (CnaCApiDisplay.cpp) resolves the owned device, calls GraphicsDevice::Dispose() and releases the handle; it checks no child count. By reading, the C++ layer makes that order safe: disposing the device disposes every resource it tracks, and a resource disposed afterwards skips the device once its lifetime token has expired. The resource handles, however, stay registered until the caller destroys each one, and the header does not say whether those destroy calls are still required after the device has gone. No test covers this order: the pure-C smoke tests destroy their resources before the device. Destroying children first is therefore the conservative practice for a caller-created device, not a rule the header imposes; the sequence the header permits is unexercised (see Known Issues). A related asymmetry: cna_graphics_resource_get_graphics_device in CnaCApiGraphicsResource.cpp answers the question through the game-device borrow path, so for a resource made on a caller-created device it does not return that device's handle but fails (see Known Issues).
Choosing and identifying the renderer from C
The selection routes in core_ext.h mirror the C++ selection surface and are listed on Runtime renderer selection. The rules that matter to a C caller:
- Before the first device. The preferred renderer, the fallback chain and automatic fallback are process-wide and can be set only until a renderer has been created. After that
cna_graphics_renderer_get_is_latched_extreportsCNA_TRUEand every setter answersCNA_RESULT_INVALID_STATE.cna_graphics_renderer_set_preferred_extalso refuses an identity that is not compiled into the build when no fallback chain is configured. - Selected is not active.
cna_graphics_renderer_get_selected_extreports what will be attempted first;cna_graphics_renderer_get_active_extreports what was actually created and answersCNA_RESULT_INVALID_STATEuntil something has been, rather than guessing. - Why a renderer was passed over. Each fallback record carries a reason:
CNA_GRAPHICS_RENDERER_FALLBACK_NOT_COMPILED_IN(0),CNA_GRAPHICS_RENDERER_FALLBACK_PROBE_UNAVAILABLE(1),CNA_GRAPHICS_RENDERER_FALLBACK_INITIALIZATION_FAILED(2) andCNA_GRAPHICS_RENDERER_FALLBACK_WINDOW_KIND_CONFLICT(3); the diagnostic text is read through its own count/copy pair because the ABI never puts an unbounded string in a fixed structure. - Compiled-in versus available.
cna_graphics_renderer_get_current_typeis a compile-time constant that needs no device.cna_graphics_renderer_copy_available_extlists the identities compiled into this build (all-or-nothing, as above); an enumerated identity is still not a capability claim.
Identity values are sparse and stable (CNA_GRAPHICS_RENDERER_MAXIMUM is 46 for 25 public identities, and retired values are never reused), which is explained with the value table on the C API guide.
The 4,055 routes by family
The overview lists the eight largest headers. The table groups all 61 headers of CNA/C by subject; the counts are lines beginning with CNA_C_API at 009d40f5 and sum to the 4,055 exported routes. Four headers declare no route of their own: the umbrella cna.h, named_colors.h (constants), math_values.h and graphics3d.h (types).
| Family | Headers (routes) | Total | What it carries |
|---|---|---|---|
| Core, ABI and errors | abi.h (1), core.h (3), core_ext.h (57) | 61 | The version query, the thread's last-error queries, and CNA extensions: logging, platform and operating-system queries, renderer identities, classification and selection. |
| Game runtime | runtime.h (42), runtime_components.h (39), runtime_graphics_manager.h (38), runtime_window.h (20) | 139 | Game creation, callbacks and frame hooks, game components and their events, the service container, GraphicsDeviceManager, the window. |
| Math and value types | vectors.h (137), geometry.h (94), matrix.h (57), math.h (52), curve.h (45), quaternion.h (28), color.h (24), packed_vectors.h (7) | 444 | Value types passed as POD structures with initializer and operation routes: vectors, planes and bounding volumes, matrices, curves, colours, packed vectors. |
| Graphics device, state and resources | graphics_device.h (87), graphics.h (27), vertex_resources.h (24), display.h (22), texture.h (21), texture_volume.h (16), graphics_state.h (14), vertex_values.h (13), graphics_resource.h (12), render_target.h (12), index_resources.h (10), sprite_font.h (9) | 267 | Devices, adapters and presentation parameters, textures (2D, 3D, cube), buffers and declarations, render targets, state objects, SpriteBatch and fonts, the generic resource queries. |
| Effects and models | effects.h (290), models.h (216) | 506 | Stock, skinned, environment-map and PBR effects, custom shader effects, parameters and techniques; models, bones, skinning, animation player, morph targets. |
| CNAEXT engine layer | engine_layer.h (858), graphics_ext.h (28) | 886 | The engine layer and graphics extensions; present in every build and answering CNA_RESULT_NOT_SUPPORTED where the layer is compiled out. |
| Content | cnb.h (272), content_readers.h (62), content.h (32) | 366 | The .cnb container, schemas, cna_cnb_compile_cnj and the loader registry; content managers and typed loads; XNB content readers, reflective readers and the object dictionary. |
| Audio | audio.h (74), xact.h (62) | 136 | PCM sound effects and instances, 3D audio, streaming instances (cna_dynamic_sound_effect_instance_create, a sound-effect-instance handle that also accepts every instance route), index-addressed microphones (cna_microphone_get_count and friends); XACT audio engine, wave and sound banks, categories and cues. |
| Media | media_library.h (148), video.h (42), media_player.h (41), media.h (39) | 270 | The media library catalogue and pictures, songs and the media player, video playback (refusing with CNA_RESULT_NOT_SUPPORTED when no video backend is built). |
| Input | input_gamepad.h (63), input_touch.h (47), input_devices.h (37), input_haptics.h (36), input_joystick.h (30), input_mouse.h (20), input_keyboard.h (17), input_text.h (17), input.h (15), input_cursor.h (6) | 288 | Snapshots and queries for every input device, touch and gestures, text input, haptics, raw joysticks, cursors, host device enumeration. |
| Devices and sensors | sensors.h (144), devices.h (62) | 206 | Accelerometer, gyroscope, compass and motion; the device environment, camera, vibration, dialogs, locale, clipboard and power queries. |
| Services | gamer_services.h (250), net_sessions.h (104), net.h (50), storage.h (49), net_gamers.h (33) | 486 | GamerServices (gamers, achievements, leaderboards, Guide, avatars), network sessions and gamers, storage containers. |
All families follow the same handle, result, thread and buffer rules; a specialised route is sometimes narrower than its C++ counterpart. Fifteen declarations are recorded as approved partial mappings: GameServiceContainer::GetService and RemoveService, for instance, are served by cna_game_services_contains_ext and cna_game_services_remove_ext, which name the two services the runtime registers, because a C caller cannot name a C++ type to key the container with. What the ABI deliberately leaves out (the build-time Content Pipeline, the Windows Phone shell module, the platform substrate, renderer implementations, and Diagnostics and the Inspector, which have no routes) and how the inventory classifies it is on the evidence page.
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
- Maintainer workflow
- I need to update the C API
- Tests and validation
- Test architecture
- Reference
- Public header index: c-api