C# binding (CNA.NET) 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 c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Binding facts were read at libcna/cna-dotnet 860f92cca30f0862f6eabf7970269ce49af90536 (2026-10-04, when the repository was still named cna-cs), which admits C ABI 0.44.0 only. CNA snapshot c1c316b9 declares 0.46.0; the binding’s later admission of 0.46.0 is recorded in CNA’s Apple campaign (see the warning below) and was not read here. Snapshot facts are limited to its C headers and C API sources. No binding test, probe or build was executed for this page; the binding's own measurements are quoted as records of its runs, not repeated here.
CNA.NET (libcna/cna-dotnet) is the maintained C#/.NET binding of CNA. It reaches CNA only through the experimental C ABI and describes itself as beta and source-first: its public metadata matches the selected XNA 4.0 Windows runtime profile exactly, it is not yet behaviourally complete, and there are no published packages or prebuilt native binaries. This page explains how it is built — its layers, its native-loading gate, its handle and string marshalling, its finalizer-safe release queue, its game-callback trampolines and its game-thread dispatch — for a maintainer who has to update it or reason about a managed/native ownership bug. Everything here was read at cna-dotnet commit 860f92cca30f0862f6eabf7970269ce49af90536 (2026-10-04); it is evidence for that revision of the binding, checked against the C headers of the CNA snapshot these pages document.
A matched pair by review, not by a run of this page. As it was at the audited commit, the binding admits exactly one native generation, C ABI 0.44.0, while CNA snapshot c1c316b9 declares 0.46.0 in modules/c-api/include/CNA/C/abi.h. The audited revision’s consumer baseline is CNA commit ed885a8ae (0.44.0), so that revision refuses a library built from this snapshot by its version rule. CNA’s Apple campaign records the next step, taken as the review below describes: the binding (cna-dotnet 8219a6c, October 2026) compared 0.44.0 with 0.46.0 — 1,419 consumed exports, none absent, no changed prototype, no struct, scalar or string difference, five integer-constant differences each reviewed — and admitted 0.46.0 (never 0.45.0); its fixture gate and the runtime contract then passed against a 0.46.0 macOS library. The export set itself (tools/c-api/abi_baseline.json, 3,210 exports) did not change between 0.44.0 and 0.46.0. Nothing was built or run for this page. Minor versions below 1.0 can break compatibility (see the ABI version history), so the next CNA minor is refused until the binding reviews it.
The four physical layers
CNA.XnaCompat (Microsoft.Xna.Framework public hierarchy) → CNA.Framework (CNA.* managed objects, ownership, pure values) → CNA.Interop (LibraryImport declarations, ABI structs, loader policy) → cna_c_api (C ABI, logical library name "cna-native") → native CNA C++
Three core source projects under src/ carry the layers, and each has one job:
CNA.XnaCompatis the XNA 4.0 compatibility façade (Microsoft.Xna.Framework.*). Preserving XNA's public hierarchy often requires composition rather than inheritance: the compatMicrosoft.Xna.Framework.Audio.SoundEffectis a sealed class that holds aCNA.Audio.SoundEffect, and itsSoundEffectInstanceholds an innerCNA.Audio.SoundEffectInstance. Its public contract is measured against real XNA metadata bytools/api-compat, not derived from CNA's headers; the binding records 256 of 256 types for the strict Windows runtime profile and 75 of 75 for its separate GamerServices, Avatar and Net profile, measured against legally supplied XNA reference assemblies. GamerServices (with Guide and the Avatar types) and Net are the recorded exception to the layering: they callCNA.Interopdirectly, because CNA has no idiomatic gamer-services API worth aCNA.*name, andCNA.Frameworkcontributes only the runningGamethey attach to.CNA.Frameworkowns the managed resource wrappers (NativeResourceHandle), theGamebase class with its callback trampolines and game-thread dispatcher, value types andCnaException, the exception a failing native call becomes.CnaExceptionexposes the failing result only as a string name (NativeResult) because noCNA.Interoptype may appear in a public signature; since C ABI 0.37.0 it also carries the canonical .NET exception type andArgumentExceptionparameter name native recorded for the failure (CanonicalExceptionType,CanonicalParamName), which the façade uses to re-raise XNA's exception types at its boundaries.CNA.Interopis internal. Its raw declarations are one partial class,Native, spread overNative.cs,Native.Devices.cs,Native.GamerServices.csandNative.Net.cs. Its methods use .NET's[LibraryImport], so the interop source generator emits the marshalling stubs during the managed build — but the route inventory, the parameter widths and the ownership decisions are written by hand, not generated from CNA's headers. At the audited commit it declares 1,419 entry points, andCnaAbiTests.RequiredSymbols_AreEveryDeclaredNativeImportpins that literal as a tripwire so that adding an import is a deliberate act. A new C route therefore never appears in C# on its own. (The header comment ofNative.csstill calls it the only file that names the native library; the three partial files do too, and so do two Emscripten main-loop imports inCNA.Framework's browser loop.)
Around them sit assemblies a game references only when it needs them: CNA.PhoneCompat (Windows Phone's Microsoft.Devices and sensors, which are not XNA), CNA.WindowsFormsCompat (a small System.Windows.Forms subset), CNA.BrowserCompat (System.IO.IsolatedStorage for the browser runtime) and the CNA.XnaCompat.Generators source generators. Under src/XnaAssemblies, ten XNA-named assemblies contain only type forwards to CNA.XnaCompat and carry no public key: CNA.NET is source-compatible and does not take Microsoft's assembly identity, so a prebuilt pure-IL library compiled against XNA can be retargeted, but a game's own .exe is not run as a binary.
NativeLibraryResolver sits in CNA.Interop and is registered by a module initializer for the CNA.Interop assembly only. The layers are also a rule: a feature class must not bypass CNA.Interop with its own ad-hoc DllImport, because the loader gate below only covers imports declared in Native.
Native loading is a compatibility gate, not a filename lookup
The resolver binds only the logical name cna-native and answers every other name with “not mine”. Selecting a file and admitting it are two separate stages.
Stage 1: selecting exactly one file
CNA_NATIVE_LIBRARY, if set, must be an absolute path to an existing file. It has the highest precedence, and when it is set no fallback is attempted: a wrong value is an error, not a hint.- Otherwise
CNA_NATIVE_DIR, if set, must be an absolute, existing directory containing a recognized library name; again no fallback when it is set. - Otherwise, on Android, the bare file name
libcna_c_api.so, which only the dynamic loader can find in the APK's library directory; the admission checks below still apply to it. - Otherwise the application's own locations:
AppContext.BaseDirectory, the directory of theCNA.Interopassembly, andruntimes/<RID>/native(the packaged RID-native asset layout).
Recognized file names are cna-native and cna_c_api with the platform's prefix/suffix (libcna_c_api.so, cna_c_api.dll, libcna_c_api.dylib, …). More than one match in the chosen location is refused as a conflict rather than resolved by preference. The resolver deliberately never probes a source checkout or the process-wide loader search path, so a package consumer cannot succeed merely because a developer build happens to be visible. Every failure message names the selection route, the platform/RID and a remediation; CNA_NATIVE_DIAGNOSTICS=1 adds the operating-system loader's own text. In a browser there is no file to select: CNA is linked statically into the WebAssembly module, the resolver steps aside, a symbol missing from the archive fails the link instead, and the module initializer judges only the linked library's version by the same policy.
Stage 2: admission against a reviewed point matrix
After NativeLibrary.Load the resolver, in order: requires the export cna_get_abi_version and calls it; looks the encoded version up in the compiled matrix (CnaNativeAbiPolicy.TryGetProfile); requires every entry point declared by Native to be exported; then runs two side-effect-free runtime canaries — cna_error_get_last_message_size must succeed and write its out-parameter, and cna_touch_capabilities_init must fill a 16-byte, version-1 structure with a canonical body without touching guard bytes on either side. Any failure frees the library and throws DllNotFoundException with the detected and expected ABI. Extra exports are allowed.
At 860f92c the executable matrix is compiled into CnaNativeAbiPolicy (ConsumerVersion = 0.44.0, one profile classified exact); the resolver does not read JSON at run time. The machine-readable record is eng/cna-native-abi-policy.json (policyVersion cna-cs-native-abi/1, consumerAbi 0.44.0, acceptedVersions = [0.44.0], default reject), and the test CnaAbiTests.MachineReadablePolicy_MatchesExecutableMatrix keeps the two in step. The JSON also records the evidence behind the one accepted entry (a consumed-surface baseline diff from 0.43.0 to 0.44.0 at CNA ed885a8ae: 3,210 exports on both sides, no consumed export absent or changed, zero breaking differences — the one change is a rule), the fifteen retired entries 0.6.0, 0.7.0, 0.8.0, 0.19.0, 0.20.0, 0.21.0 and 0.35.0 to 0.43.0 with the reason each was dropped, and twelve named admission fixtures (for example retired-0.43.0, unreviewed-0.45.0, missing-required-symbol, changed-required-signature, structurally-incompatible-0.44.0), all expected to reject except the two exact-0.44.0 cases. At that revision scripts/Verify-NativeAbiCompatibility.sh runs those fixtures on Linux only; CNA’s Apple campaign records the script building them as .dylib on macOS too, and the policy, fixtures (13) and CI pins moving to 0.46.0.
There is no “same major 0.x” range: the policy text says a 0.x minor is “never accepted as a same-major range”. The retired list shows that export availability is not the whole policy either — every route the binding still imports existed in 0.21.0 with the same prototype, yet a 0.21.0 library is refused on the version rule alone, which the retired-0.21.0 fixture proves — so the matrix stays a list of reviewed points instead of growing into a range. CnaAbi.EnsureCompatible, which Game's constructor calls before its first native call, re-asserts the matrix at the public boundary in case an alternate host bypasses the resolver.
A maintainer must not simply change the expected version when CNA moves to a new minor. The migration is a review: compare consumed prototypes, struct layouts, numeric constants, callback contracts, result codes, ownership and behaviour between the accepted generation and the new one (the binding's own tools/coverage/baselinediff.py, with eng/cna-upstream-abi-allowlist.txt, covers the export/layout half), add the new point to both the compiled matrix and the JSON with its evidence, retire the old point, then run the admission fixtures and the native suites against a real library of the new generation. A rule-only CNA bump, which changes no export, still needs that review: 0.44.0 itself is one (Activated and Deactivated handlers may now use the device), and the binding's 0.43.0 → 0.44.0 entry records it as such.
What the snapshot's 0.46.0 surface looks like from this binding
A name-level comparison, done by reading and not executing anything: all 1,419 entry points the binding imports at 860f92c are declared by name in the snapshot's 60 public C headers under CNA/C/, which declare 3,210 routes in all, exactly the export set of 0.44.0: what 0.46.0 changes is its declared integer constants (it adds CNA_NATIVE_WINDOW_SYSTEM_UIKIT), none of them a route or structure the binding imports. That agrees with the binding’s own review, but a name-level comparison is still the input to a review, not its result: a name that exists can have a changed contract, and only a run against a real library tests the canaries and the behaviour. The routes and constants this page relies on — cna_get_abi_version and CNA_RESULT_THREAD (8), CNA_RESULT_CALLBACK (9) and CNA_RESULT_INVALID_STATE (3) (abi.h), cna_game_create, cna_game_set_frame_hooks_ext, cna_game_run_frame_ext, cna_game_set_foreign_thread_calls_ext, cna_game_run_foreign_thread_calls_ext, cna_game_destroy, CNA_GameCallbacks, CNA_GameFrameHooks and CNA_CallbackError (runtime.h), and the cna_sound_effect_* routes (audio.h) — are all present at the snapshot.
Handles, strings and errors on the wire
- Handles.
CnaHandlestores the C ABI'suint64_tCNA_Handleasulong, never as a native pointer, andNativeResourceHandlekeeps that 64-bit value itself. It is deliberately not aSafeHandle: aSafeHandlestores a pointer-width value and could not hold a CNA handle on WebAssembly or any 32-bit target, so the class is aCriticalFinalizerObjectthat keepsSafeHandle's contract (release at most once, fromDisposeor a critical finalizer, never for a borrowed or detached handle) over the real 64-bit value. See the native handle registry for what the 64 bits mean (slot plus generation). - Strings.
CnaStringMarshal.WithStringViewUTF-8-encodes a managed string into astackallocbuffer when it is at most 256 bytes and into a heap array otherwise, pins it only for the duration of the synchronous native call, and passes a pointer-plus-lengthCNA_StringView(a null pointer for the empty string). Native receives a borrowed view for one call, not an owned NUL-terminated string. - Errors.
CnaException.ThrowIfFailedreads the calling thread's last native diagnostic through the two-callcna_error_get_last_message_size/cna_error_copy_last_messageprotocol (CnaError.GetLastErrorMessage, which never throws itself), adds the canonical exception type and parameter name from the 0.37.0 queries, and builds a managed exception. The diagnostic is thread-local on the native side, so it must be read on the thread that made the failing call, immediately afterwards. The C-side rules are in the C API programming model.
Representative call: managed SoundEffect → native track
new CNA.Audio.SoundEffect(byte[] PCM, offset, count, rate, channels, loopStart, loopLength) → managed validation (rate 8000..48000, Mono/Stereo, block alignment, range, loop) → Native.cna_sound_effect_create_pcm16_range_ext(CnaAmbientGame.Current, info, bytes, ..., out CnaHandle) → native C resource + HandleRegistry → C++ SoundEffect (bytes copied into a native vector) → NativeResourceHandle owns the result, releases via cna_sound_effect_destroy SoundEffect.CreateInstance() → CreateNativeInstanceHandle → Native.cna_sound_effect_create_instance(parent handle) → managed SoundEffectInstance owns the child handle (release: cna_sound_effect_instance_destroy) → Play() → cna_sound_effect_instance_play → C++ mixer voice Teardown: child Dispose, then parent Dispose, then Game.Dispose
The constructor validates in XNA's order — format (sample rate 8,000–48,000 Hz, mono or stereo), whole-buffer block alignment, the offset/count range, then the loop region — entirely in managed code, so failures are deterministic even before a native game exists. CnaSoundEffectCreateInfo sets its own struct_size and struct_version (1) in its constructor. The byte[] is pinned with fixed only for the duration of the call; the C route copies the PCM bytes into a native vector before returning, so the managed array need not stay pinned during playback. Creation needs the ambient game handle (CnaAmbientGame.Current, set by Game's constructor) because the C ABI has no parameterless audio route.
CreateNativeInstanceHandle returns one new native child handle. The public CNA.Audio.SoundEffect.CreateInstance wraps it once; the XNA-compat SoundEffect.CreateInstance calls the same internal method and composes a new compat SoundEffectInstance around that same handle. The compat instance strongly roots its parent, while the compat parent keeps only weak references to its children and disposes every still-live child before releasing its own native effect. Creating an intermediate public managed instance and wrapping its handle a second time would give one native handle two owners and a double release.
The ordering is semantic, not memory hygiene. At the snapshot, cna_sound_effect_destroy in CnaCApiAudio.cpp refuses with CNA_RESULT_INVALID_STATE while any instance of that effect is alive, and cna_game_destroy refuses while owned audio resources remain; the native side of this sequence is traced in the C API SoundEffect trace, and the mixer below it in Audio engine internals.
NativeResourceHandle and the owner-thread release queue
NativeResourceHandle carries a per-resource release delegate and an ownsHandle flag. A borrowed resource must be wrapped with ownsHandle: false — the case it was added for is the video player's frame texture, which the C API documents as valid only until the next call on that player; otherwise the critical finalizer would destroy an object its real owner still controls. Detach gives up ownership, returns the value and marks the wrapper closed, so neither Dispose nor the finalizer will release it; ContentManager uses it to hand a SpriteFont atlas texture to the font.
Native handles are creation-thread-affine, and a .NET critical finalizer runs on the finalizer thread, where a direct release would receive CNA_RESULT_THREAD and lose the only copy of the handle. ReleaseHandle therefore releases immediately only when it runs on the owner thread (the managed thread ID captured at construction); off-thread it queues the raw value and release delegate under that owner thread's ID. A failed on-thread release is queued too, so a parent refused because a child still lives is retried later. DrainPendingReleasesForCurrentThread repeats passes over the queue as long as some release succeeds (children first, then the parents they were blocking) and returns anything still failing to the queue instead of dropping it. Counters are exposed through NativeResourceHandle.GetMetrics.
The drain points at the audited commit are all in Game: its constructor, entry to and return from Run(), the direct-call helpers RunOneFrame(), Tick(), ResetElapsedTime() and SuppressDraw(), before and after every browser frame, and Dispose. The per-frame callback trampolines do not drain, so while a desktop game sits inside a long Run() loop, finalizer-queued releases wait until one of those calls, and a wrapper whose owner thread never reaches another drain point leaves a pending native resource. Explicit, deterministic Dispose on the owner thread remains the reliable path. The general thread-affinity rules are collected in the thread and callback map.
Managed Game callback boundary
CNA.Framework.Game's constructor allocates a GCHandle to itself and passes it as the callback context, fills CnaManagedGameCallbacks (load content, update, draw, unload content) inside CnaGameCreateInfo, checks the ABI (CnaAbi.EnsureCompatible), drains pending releases, and calls cna_game_create. It then installs a second table, CnaGameFrameHooks, through cna_game_set_frame_hooks_ext, using its initialize, begin_run and end_run hooks. This mirrors the snapshot's C design, where frame hooks are a separate table so the original callback struct never grows (see runtime.h). If installing the hooks fails, the game is destroyed and the GCHandle freed before the exception is thrown.
Every callback is a static [UnmanagedCallersOnly] trampoline with the cdecl calling convention that resolves the Game from the context, runs the managed method, and catches every exception: a managed exception must never unwind through C++ frames. ReportCallbackFailure keeps the first exception for the managed caller to rethrow after native returns, encodes its message as UTF-8 (shortened until it fits) into a 512-byte buffer allocated once per Game with GC.AllocateArray(..., pinned: true), points CNA_CallbackError.message at it and returns CNA_RESULT_CALLBACK. The C header documents the message as borrowed only until the callback returns; a buffer pinned for the whole game lifetime removes any doubt that it is still rooted when native copies it.
Two newer native routes shape how the run is driven. In a browser a page cannot block, so Run() hands the game to BrowserGameLoop, which calls cna_game_run_frame_ext (C ABI 0.38.0) once per animation frame — or, in a threaded bundle, from Emscripten's main loop on .NET's worker thread — and the frame that finds the game exited delivers Exiting and EndRun as a desktop run does. On every platform, XNA's loading-screen pattern (a worker thread loading content while the game thread draws) meets native's creation-thread rule through GameThread: the binding sends whole operations from other threads to the game thread, where they run at the start of the next Update or Draw while the caller blocks, and its Game constructor turns on native's own queue for the rest (cna_game_set_foreign_thread_calls_ext, 0.39.0). A game thread that waits for such a worker runs both queues between short waits, through a SynchronizationContext that is told of its waits and calls cna_game_run_foreign_thread_calls_ext (0.41.0). XNA ran the worker's calls concurrently; here they run while the game thread waits. Only the XNA façade's Game turns native's queue on; CNA.Framework's own Game leaves native's default, under which such a call is refused with CNA_RESULT_THREAD.
Game.Dispose releases components, microphone subscriptions, media-player state, window and game event registrations (all of which hold native context pointers into GCHandles), then forces GC.Collect / WaitForPendingFinalizers / GC.Collect so that wrappers whose XNA counterparts are not IDisposable (effect techniques and passes are the recorded case) are finalized, drains the queue, and calls cna_game_destroy once. Disposal must not throw, so a refusal is recorded instead: the handle is kept in a static “undestroyed game” slot with the reason. Because native admits one C-owned game at a time, the next Game constructor that fails to create a game collects, drains, retries the recorded destroy and, if that succeeds, retries creation; otherwise its exception names the earlier refusal. The source comments record why the destroy itself is not retried inside Dispose: a second destroy of a handle the first call may already have consumed is undefined. A callback signature or timing change must be checked in all three places: the native callback structure, the CNA.Interop layout and function-pointer type, and the CNA.Framework.Game trampoline. The native half of game creation and teardown is in the C game trace.
Change workflow and evidence
To expose a new CNA API in C#:
- Add and test the native C route first, following I need to update the C API and the checklist in C API validation and evolution.
- Mirror the exact primitive widths, struct layout and ownership in the right
CNA.Interop/Native*.csfile (and bump the 1,419 tripwire deliberately). - Add an idiomatic wrapper in
CNA.Framework; add an XNA-compat façade member only if the method belongs to that public contract. - Update the native ABI policy only after a reviewed ABI diff for the new generation, never before.
- Run managed, native-integration, ownership-stress, ABI-verification and strict public-metadata checks.
If the route returns a borrow, establish the parent's lifetime and use a non-owning handle; if it returns an owned child, make sure disposal happens before its parent and on its creation thread (see the ownership and lifetime map). Exercise non-ASCII strings, buffer count and stride, wrong-thread release, a missing export and a wrong-ABI library. Changing only a high-level class can hide a stale raw import until its first runtime call.
Evidence group at 860f92c | What it covers |
|---|---|
tests/CNA.Framework.Tests | Managed behaviour without a native library, including CnaAbiTests (version decoding, matrix acceptance/rejection, required-symbol list, JSON-vs-code policy agreement). |
tests/CNA.XnaCompat.Tests and tests/CNA.BrowserCompat.Tests | The public XNA façade and the browser storage assembly. |
tests/CNA.Integration.Tests | Real native calls: ResourceIntegrationTests.SoundEffectInstance_MovesThroughItsStates (instance state round trip), GameLifecycleTests (ABI admission, construct/dispose repeatability, lifecycle callback order, timing round trip), CompatWorkerThreadTests (calls from a loading thread) and many more. |
tests/CNA.GamerServices.IntegrationTests | GamerServices, Avatar and Net against the native library, in their own process because the native dispatcher is process-wide, as XNA's is. |
tests/CNA.OwnershipStress | A separate executable, so a native double free kills only that process: 24 create/use/destroy cycles by default (1,000 with CNA_OWNERSHIP_STRESS_DEEP=1, or a count argument), alternating explicit disposal with abandonment plus forced finalization, with a throwing GraphicsDevice.Disposing handler every tenth cycle; it reads the release and destroy-retry metrics. |
tools/abi-verify | C-authority layout, constant and prototype verification of the managed declarations against a CNA include directory, with negative controls. |
tools/native-abi-probe and scripts/Verify-NativeAbiCompatibility.sh | Loader admission in an isolated process (CNA_ABI_PROBE_STATUS=accepted|rejected) and the twelve policy fixtures (Linux only). |
tools/api-compat | Compiled public/protected metadata compared with legally supplied XNA 4.0 Windows runtime assemblies. |
The binding's README records its latest local run, against CNA 8a7e13ff5 (an ancestor of this snapshot whose C headers are the same) on Linux x64 with OPENGLES3 and compiled effects: 653 Framework, 319 XNA-compat, 5 BrowserCompat, 260 native integration and 24 GamerServices/Avatar/Net tests passing in both Debug and Release, and tools/abi-verify measuring 1,137 layout values with no mismatch. These are the binding's own records; none of these suites was run for this page.
A green run is not integration evidence by default. Native integration facts use a [NativeFact] attribute that skips the test when the library cannot be loaded, and an ABI-rejected library surfaces as exactly that load failure, so a passing report proves nothing about a library the policy refused. The binding's release-qualification workflow pins CNA headers at ed885a8ae and runs its native lanes only when a secret-supplied Linux library is configured; its strict-API lane likewise needs secret-supplied XNA reference assemblies. The workflow is not green evidence for this revision either: its latest completed runs, for earlier commits, recorded failing jobs when observed on GitHub on 2026-10-04, so the measured numbers above come from local runs recorded in the binding's documents.
Platform evidence
The binding classifies its own platforms, and the classes are narrower than the platform list suggests. Linux x86_64 is the primary qualified platform, measured on private displays (Xvfb, or a headless Weston/Xwayland compositor on the real GPU), not on an interactive desktop. The browser route (a plain Microsoft.NET.Sdk.WebAssembly project, not Blazor, with CNA's static WebGL 2 archive) is qualified only in headless Chromium with SwiftShader; an interactive browser, a hardware GPU and audio are not. Android is qualified only on the x86_64 emulator (API 35, headless, no audio); an ARM64 package builds but nothing ran on it, and no physical device was used. macOS on Apple silicon (osx-arm64) now has local evidence recorded in CNA’s Apple campaign on a physical Mac mini M4 — Debug and Release builds, the managed suites, ABI verification with 0 mismatches, package acceptance with runtimes/osx-arm64/native, the template, and 84 sample programs started on SOFTWARE and METAL — which the binding records as local evidence, not a shipping claim (no CI leg, signing or notarization); osx-x64 has only a CI ABI-header check. Windows and iOS are not runtime-qualified: the Windows build steps are documented but unverified. Browser and Android heads need .NET 11 and its workloads; the desktop libraries target .NET 8.
Some of the binding's documents lag its code at the audited commit: CnaAbi.cs's doc comment still names 0.6.0 as the generation it was written against, and docs/native-abi-compatibility.md quotes 1,409 and 1,416 imports in older rows. Use the compiled policy, the tests and the current source as the authority. For where this binding sits among the other external projects, see Bindings boundary and C API and bindings architecture; the archived Java and Python pages record the same problems solved with JNI and ctypes.
Reading order at cna-dotnet commit 860f92c
eng/cna-native-abi-policy.json,CnaNativeAbiPolicy.cs,NativeLibraryResolver.csanddocs/native-abi-compatibility.md: determine whether this checkout can load the selected CNA library at all.Native.cs,CnaHandle.cs,CnaStringMarshal.csandCnaException.cs: inspect wire declarations, marshalling and error conversion before any feature wrapper.NativeResourceHandle.cs,Game.cs,GameThread.csandBrowserGameLoop.cs: understand the owner-thread release queue, callback pinning, game-thread dispatch, the browser run and game teardown.CNA.Framework/Audio/SoundEffect.cs,SoundEffectInstance.cs, the compatMicrosoft/Xna/Framework/Audio/SoundEffect.csand the in-tree C route trace: follow one call through every ownership layer.ResourceIntegrationTests.cs,CnaNativeProbe.csandCNA.OwnershipStress: choose live-native and finalizer-order evidence, after ABI admission, and know when a native fact silently skips.docs/migrating-xna-games.mdanddocs/final-compatibility-audit.md: the build steps per platform and the binding's own record of what is and is not qualified.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- C API guide: bindings boundary · ABI version history
- Architecture
- C API and bindings architecture
- Maintainer workflow
- I need to update the C API · Ownership and lifetime master map
- Tests and validation
- Test architecture
- Reference
- Public header index