Chapter 3 Language, Conventions, and CNAEXT
Every worked example in the rest of this book silently assumes a set of conventions that CNA enforces on itself through its CLAUDE.md porting rules and CHECKLIST.md. This chapter makes those conventions explicit, once, so that later chapters can simply use them.
3.1 The prime directive: match FNA, not taste
CNA’s porting rules name a single authoritative behavioral reference: a local FNA source tree. The instruction is blunt: “Do not treat old CNA code or AI-generated stubs as authoritative if they conflict with the FNA reference API.” Concretely, this means:
-
•
Class names, struct names, enum names, method names, operator names, and constant names must match XNA/FNA exactly. A method is not renamed to be “more C++-like” if that would diverge from the reference.
-
•
Behavior is matched over personal preference: packed value layouts (Color stores its channels as AABBGGRR), clamping behavior, integer cast behavior, operator behavior, method overloads, default values, and exception behavior where practical, all follow FNA even when a different choice would look cleaner in isolation.
-
•
C++ member declaration order mirrors the original C# source order where practical, specifically to keep diff-based review tractable against the reference.
This is the single idea that makes the rest of the house style legible: every convention below exists to answer the same question — how do you represent a C# concept in C++ without losing the ability to diff against the original?
3.2 Two namespaces, two jobs
CNA code lives in exactly one of two namespace families, and the rule for which one is absolute:
| Namespace | Contains |
|---|---|
| Microsoft::Xna::Framework::* | Real XNA 4.0 types, matching the reference exactly |
| CNA::* | Project-specific extensions, helpers, internal backends |
Original XNA types are never moved into the CNA namespace, and the reverse is equally firm: anything that is not part of the real XNA 4.0 API must not silently live inside Microsoft::Xna::Framework unmarked. That is what the CNAEXT marker exists for.
3.3 The CNAEXT tag: a compiler-enforced honesty check
CNAEXT is an empty marker macro, defined in modules/core/include/CNA/CNAHelper.hpp, whose only job is to visibly tag a declaration inside the XNA namespace tree as not part of the original XNA 4.0 API — a rumble-and-gyro extension on GamePad, a GetTypeName() override, a raw joystick API, a haptics subsystem. Two concrete examples from the real codebase:
What makes this more than a documentation convention is a compiler-enforced check described in Chapter 1, precisely: CNAHelper.hpp’s own definition of CNAEXT is conditional on a second, distinct macro, CNA_STRICT_XNA_API — in a normal build CNAEXT expands to nothing at all (it is documentation only); only when CNA_STRICT_XNA_API is defined does it expand to [[deprecated("CNAEXT: not part of the XNA 4.0 API surface")]]. That define is never set for the normal CNA library target — it is scoped to exactly two small standalone CMake targets in cmake/Harnesses.cmake, each compiled with -Werror=deprecated-declarations on top of it, so a stray CNAEXT-tagged call anywhere those targets’ translation units can see turns into a hard build failure rather than a warning that could be scrolled past.
A worked example: the check that verifies the check. A positive-only test — “the real API still compiles clean” — cannot by itself prove the mechanism would actually catch a real leak; it is equally consistent with a check that always passes. CNA’s own tools/devices/StrictXnaApiSurfaceCheck.cpp (compiled as the cna_strict_xna_api_check target, run as the StrictXnaApiSurfaceCheck_Compile_Run CTest) is that positive direction, and its own header comment names every CNAEXT-tagged Accelerometer / Sensors member it deliberately avoids calling. The negative direction — proving a real leak genuinely fails — is a second, separate file, tools/devices/StrictXnaApiSurfaceLeakCheck.cpp, whose entire body is one deliberate violation:
This compiles into an EXCLUDE_FROM_ALL target (cna_strict_xna_api_leak_check), so it never participates in an ordinary build. It is invoked by exactly one CTest: StrictXnaApiSurfaceLeakCheck_MustFailToCompile, whose WILL_FAIL TRUE property inverts the usual pass/fail meaning — the test passes only if the build command it wraps fails. If a future change to the CNAEXT macro or to CNA_STRICT_XNA_API’s wiring ever let this deliberately-broken file compile, this specific test would flip from passing to failing — catching a regression in the safety net itself, not merely in the API surface the net is meant to protect.
3.4 C# properties, translated consistently
C# auto-properties (public byte R { get; set; }) become a fixed pair of C++ methods, never a public field, unless the surrounding type has already established a field-style convention:
Not every property setter is a bare field write, and the convention does not ask one to pretend otherwise: a C# property with real validation logic in its setter keeps that logic, translated directly, rather than being flattened into a public field for convenience. SkinnedEffect::WeightsPerVertex (Chapter 15) is a real instance of this, read directly from SkinnedEffect.cpp:
Two things this one setter does at once, both faithfully carried over from what a validated C# property would do: it rejects an invalid value outright (a real XNA/FNA SkinnedEffect genuinely only supports 1, 2, or 4 bone weights per vertex — there is no silent clamping to the nearest valid value), and it marks a dirty-shader-selection flag as a side effect of the assignment, since a changed weight count means a different GPU shader variant must be selected on the next Apply(). Neither behavior would survive a mechanical “just make it a public field” translation.
3.5 SharpRuntime type aliases: never a raw C++ primitive in the XNA surface
Wherever C# source uses a .NET primitive type name, CNA code uses the corresponding sharp-runtime alias rather than a raw C++ fundamental type, specifically so that the XNA-facing surface keeps a visible, greppable link back to its .NET origin:
| C# type | sharp-runtime alias | Underlying C++ type |
|---|---|---|
| byte | bytecs / Byte | uint8_t |
| sbyte | sbytecs / SByte | int8_t |
| short | shortcs / Int16 | int16_t |
| ushort | ushortcs / UInt16 | uint16_t |
| int | intcs / Int32 | int32_t |
| uint | uintcs / UInt32 | uint32_t |
| long | longcs / Int64 | int64_t |
| ulong | ulongcs / UInt64 | uint64_t |
| float | Single | float |
| string | String | std::string |
| char | charcs | char16_t |
If a needed alias does not yet exist, the rule is to add a minimal stub to sharp-runtime first — never to reach for a raw C++ type directly in the XNA API surface as a shortcut. This is the same missing-dependency discipline covered in §3.12 below.
3.6 Events, as System::EventHandler<T>
C# events and delegates are modeled uniformly through sharp-runtime’s System::EventHandler<T>, never through an ad-hoc project-specific callback type:
3.7 Interfaces as abstract base classes, IDisposable as a pattern
C# interface relationships become C++ abstract base classes — for example Color implementing IEquatable<Color> and IPackedVector in C# becomes struct Color : public Graphics::PackedVector::IPackedVectorT<UInt32> in CNA. Where an exact mapping is not practical, the rule is to implement equivalent behavior and document the intentional deviation in the pull request description — explicitly not as a source comment, keeping the deviation visible to reviewers without cluttering the header for every future reader.
IDisposable follows a fixed pattern backed by sharp-runtime’s System::IDisposable:
with an isDisposed_ guard checked before any operation, throwing std::runtime_error on use-after-dispose.
3.8 Visibility is chosen, not defaulted
C# internal members do not automatically become C++ public just because C++ lacks an internal keyword — they become private, protected, live in a detail/internal namespace, or are omitted from the port entirely. The house style calls out a concrete anti-example: a C# internal DebugDisplayString helper should not become a public C++ API method just because it was easy to leave public.
3.9 Physical modules own the namespace-shaped include surface
Each physical module owns its declarations under modules/<owner>/include/ and its implementation under modules/<owner>/src/. The public include subtree mirrors the namespace, while implementation paths may use a shorter subsystem directory:
Non-template implementations do not live in headers. At the pin, that subtree contains 125 .hpp files: 107 directly in the Microsoft::Xna::Framework::Graphics directory and 18 in its PackedVector child. The directory mirrors both namespace levels; calling all 125 files the flat “Graphics namespace” would erase that real nested boundary.
3.10 No backward-compatibility shims
If correcting an API breaks an old demo, the demo is fixed — CNA does not carry a convenience alias just to keep outdated call sites compiling. The house style gives its own canonical wrong/right pair:
3.11 Documentation: full Doxygen, or nothing
Every public method, constructor, property accessor, operator, and constant in every .hpp file must carry a Doxygen block — bare /// line comments are never acceptable on a public declaration (they remain acceptable only for brief inline notes inside a method body). The intent of the original C# XML doc comments (<summary>, <param>, <returns>) is preserved, often verbatim from FNA where the wording already fits, but never marked with a comment like “taken from FNA” — provenance belongs in the porting record, not scattered through the header.
3.12 Missing dependencies: stub correctly, never invent
When a file being ported references a type that does not exist yet, the rule is layered: if the missing type belongs to the .NET runtime (System.* or a primitive alias), it is added to sharp-runtime first, following the same minimal-stub discipline described in Chapter 2. Otherwise, a minimal, correctly named stub is added directly in CNA, in the correct final namespace, sufficient only to compile — never a large unrelated system built just to satisfy one missing dependency. Every stub introduced this way is reported in the task or PR description, so it is never silently forgotten as “real” once the build goes green.
A worked example: a real missing-dependency gap, from stopgap to proper fix. CNA’s own checked-out history has a concrete instance of exactly the situation this rule exists for. At one point, CNA’s checkout would not build at all because two content-type readers called System::IO::BinaryReader::ReadChar() and ReadDecimal() — and neither method existed yet in sharp-runtime. Rather than inventing an ad-hoc replacement inline in CNA (the exact anti-pattern this section warns against), the documented fix at the time was a diagnostic-quality stopgap patch, explicitly labeled as temporary and preserved in this book’s own tools/cna-screenshot-infra/ materials. sharp-runtime has since implemented both methods for real, in its own BinaryReader.cpp, matching .NET’s actual documented behavior precisely rather than approximately: ReadChar() decodes one UTF-8 code point from the underlying stream (not a raw byte), throwing System::FormatException for an invalid lead byte, an invalid continuation byte, or a code point above 0xFFFF that would require a UTF-16 surrogate pair — the last case chosen deliberately to match real .NET’s own BinaryReader.ReadChar(), whose internal Decoder.GetChars call targets a one-char buffer and throws for exactly the same reason, rather than silently truncating to one half of a surrogate pair. This is the missing-dependency rule’s full lifecycle in one real example: a genuine gap, a documented interim workaround, and a proper fix landing in sharp-runtime once someone did the work — not a stub that quietly became permanent.
3.13 The per-file porting checklist, in miniature
CHECKLIST.md governs the full requirements for porting one FNA source file to CNA; the house rules single out its non-negotiable minimum:
-
•
An SPDX MS-PL license identifier at the top of both the .hpp and the .cpp.
-
•
#include "CNA/CNAHelper.hpp" in the header, whenever CNAEXT is used anywhere in that file.
-
•
Every method body checked line-by-line against the FNA equivalent, with every intentional deviation documented in a source comment (not silently absorbed).
-
•
Every concrete class inheriting System::Object overrides GetTypeName(), tagged CNAEXT, returning the fully-qualified .NET name (e.g. "Microsoft.Xna.Framework.Game").
-
•
A unit test for every public method, operator, and constant — with out-ref overloads tested separately from their value-returning counterparts, and static factory methods (CreateFromPoints, CreateMerged, CreateFromSphere, …) each getting a dedicated test.
A file is “done in one pass” — the house style explicitly rejects a “make-and-forget-partially” workflow where checklist items are deferred to a later, unscheduled pass.
3.14 Why this chapter earns its place in a “bible”
None of the conventions above are unique or clever in isolation — type-alias tables and Doxygen mandates exist in a thousand C++ style guides. What is unusual is how tightly they are all in service of one measurable goal: keeping a large, multi-renderer, multi-platform C++ codebase auditable against a real external reference (FNA, real XNA 4.0, xna4-spec) at every layer — namespace, visibility, member order, type alias, and a compiler flag that turns “did we leak a non-XNA type into the XNA surface” into a build error instead of a hoped-for code-review catch. The rest of this book takes these conventions as read; when a later chapter says a class “matches FNA exactly,” this is the mechanism that claim rests on.