From C# to C++: CNA's translation conventions
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. Alias, event and BinaryReader statements were read in sharp-runtime at 41b918c9. The event snippet is an illustrative fragment and was read-checked against the headers, not compiled.
Every CNA header follows a small set of rules for representing a C# concept in C++ without losing the ability to diff the result against the original. This page explains those rules with real declarations from snapshot 009d40f5: why names and member order follow the reference, how properties, primitives, events, interfaces and disposal are translated, where the written rules and the code have drifted apart, and what the per-file porting checklist demands. It is for readers of CNA code, porters of XNA games and contributors who add API.
Match the reference, not taste
CNA's porting rules (CLAUDE.md) name a local FNA source tree as the working behavioural and API reference and say bluntly: do not treat old CNA code or AI-generated stubs as authoritative if they conflict with it. (Where measured Microsoft XNA and FNA disagree, XNA wins; see who decides what XNA does.) Three consequences shape every file:
- Names are exact. Class, struct, enum, method, operator and constant names match XNA and FNA exactly; nothing is renamed to look more like idiomatic C++.
- Behaviour beats preference. Packed layouts, clamping, integer casts, operator behaviour, overload sets, default values and, where practical, exception behaviour follow the reference even when another choice would look cleaner.
Colorstores its channels packed asAABBGGRR:Color.cppdefinesColor::CornflowerBlueas0xffed9564, that is alpha 255, blue 237, green 149, red 100. - Member order mirrors the C# source where practical, so that a reviewer can diff a C++ class against its C# original line by line.
Every other convention below answers the same question: how is a C# concept represented in C++ without breaking that diff?
Two namespace families
| Namespace | Contains |
|---|---|
Microsoft::Xna::Framework::* (and the Windows Phone Microsoft::Devices) | Real XNA types, shaped exactly like the reference |
CNA::* | Project-specific extensions, helpers, renderer and platform contracts, internal machinery |
The rule runs both ways. An original XNA type is never moved into CNA, and something that is not XNA 4.0 API must not sit inside Microsoft::Xna::Framework unmarked: that is the job of the CNAEXT marker, catalogued with its strict compile check in the CNAEXT catalogue.
Properties become accessor pairs
A C# property becomes a fixed pair of methods, never a public field, unless the type has already established a field style (the vector and matrix components such as Vector2::X are plain fields, as in XNA's structs):
// C#: public byte R { get; set; }
[[nodiscard]] bytecs getRProperty() const;
void setRProperty(bytecs value);
The convention does not flatten a setter that does work. XNA's SkinnedEffect.WeightsPerVertex accepts only 1, 2 or 4, and changing it selects a different GPU shader variant. The C++ setter in SkinnedEffect.cpp keeps both behaviours:
void SkinnedEffect::setWeightsPerVertexProperty(int v)
{
if (v != 1 && v != 2 && v != 4)
throw System::ArgumentOutOfRangeException("value");
weightsPerVertex_ = v;
dirtyFlags_ |= DirtyShaderIndex;
}
An invalid value is rejected, not clamped to the nearest valid one, and the dirty flag makes the next Apply() pick the matching shader variant. A mechanical "make it a public field" translation would have lost both. The exception type is the .NET one from sharp-runtime, which is what C# callers of XNA would catch; the property spelling is also why the census classes 1,039 of the 1,040 documented XNA properties as semantic rather than exact matches.
.NET primitives keep their names
Where C# uses a .NET primitive, the XNA-facing C++ surface uses the matching sharp-runtime alias instead of a raw fundamental type, so that the public API keeps a visible, greppable link to its .NET origin. The aliases live in SharpRuntime/SharpRuntimeHelper.hpp (namespace SharpRuntime; checked at sharp-runtime 41b918c9):
| C# | sharp-runtime alias | C++ type |
|---|---|---|
byte / sbyte | bytecs or Byte / sbytecs or SByte | uint8_t / int8_t |
short / ushort | shortcs or Int16 / ushortcs or UInt16 | int16_t / uint16_t |
int / uint | intcs or Int32 / uintcs or UInt32 | int32_t / uint32_t |
long / ulong | longcs or Int64 / ulongcs or UInt64 | int64_t / uint64_t |
float / double | Single / Double | float / double |
string | String | std::string |
char | charcs | char16_t |
IntPtr | IntPtr | std::uintptr_t (unsigned, unlike .NET) |
Two details matter when porting. String is UTF-8 std::string, while charcs is a UTF-16 code unit, so text APIs that index characters need care. And if an alias does not exist yet, the rule is to add it to sharp-runtime first; reaching for a raw C++ type in the XNA surface is the shortcut the rule forbids (see missing dependencies).
Events are System::EventHandler<T> fields
C# events and delegates are modelled uniformly through sharp-runtime's System::EventHandler<TEventArgs>, never through a project-specific callback type. Game declares System::EventHandler<System::EventArgs> Exiting; in Game.hpp, exactly as XNA declares EventHandler<EventArgs> Exiting.
// Subscribe (the handler type is std::function<void(System::Object*, const T&)>)
game.Exiting += [](System::Object* sender, const System::EventArgs& e) { /* ... */ };
// Subscribe and keep a token when the handler must be removed later
auto token = game.Exiting.Add([](System::Object*, const System::EventArgs&) { /* ... */ });
game.Exiting.Remove(token);
// Raise, inside the owning class
Exiting.Raise(this, args); // or Exiting.Invoke(this, args)
The token is the one place the translation shows. C# removes a handler with -= by delegate equality; a std::function has no equality, so sharp-runtime's Add returns a token that Remove takes, and operator+= simply discards it. (Illustrative fragment; it assumes a Game-derived object named game.)
Interfaces, equality and disposal
C# interface relationships become C++ abstract base classes. XNA's Color implements IEquatable<Color> and IPackedVector<uint>; CNA's is struct Color : public Graphics::PackedVector::IPackedVectorT<UInt32>, public System::IEquatable<Color>, with Equals(const Color&) marked override (the example in the instruction file shows only the packed-vector base). Where an exact mapping is not practical the rules ask for equivalent behaviour, with the deviation explained in the change description rather than in a header comment; CHECKLIST.md tabulates the accepted ones (for example GetHashCode() returning std::size_t, and ref/out parameters expressed as value-and-reference overload pairs).
IDisposable maps to System::IDisposable with a public Dispose() override and, where the C# pattern needs it, a protected Dispose(bool disposing); members check an isDisposed_ flag before acting. Here the written rule and the code have drifted: the instruction file still says to throw std::runtime_error on use after disposal, while the XNA-layer code throws the .NET-shaped System::ObjectDisposedException (it appears 69 times in modules/graphics/src/Xna alone, for example in SpriteBatch.cpp). Follow the code: it is what a C# port expects to catch.
Visibility is chosen, not defaulted
C++ has no internal, but that does not make C# internal members public. They become private, protected, move into a detail or internal namespace, or are left out. The instruction file's own example is DebugDisplayString, an internal debugger helper in XNA's math types, which "should not become a public C++ API method". At this snapshot nine of the ten math and geometry types that carry a getDebugDisplayStringProperty() keep it private; Color is the exception, exposing it publicly but marked CNAEXT, so strict mode flags any use of it.
Two exceptions are recorded in CHECKLIST.md. A C# internal set becomes a private setter plus friend class <OneSpecificClass> (the Microsoft::Devices::Sensors reading types friend only their own sensor class), which is narrower than C#'s assembly-wide access. And a C# internal const or internal static readonly field that another translation unit genuinely needs is exposed as a CNAEXT public static constexpr, with a note citing the FNA declaration, rather than duplicated or left unreachable: GamePad::LeftDeadZone, RightDeadZone and TriggerThreshold, and TouchPanel::MAX_TOUCHES (8) and NO_FINGER (-1). Strict mode therefore flags every use of them. (Read in CHECKLIST.md and the two headers at this snapshot; not compiled.)
Named constants and no compatibility shims
C# static readonly fields become static const members declared in the header and defined in the .cpp, which is why Color::CornflowerBlue is one definition in Color.cpp. When correcting an API breaks old code, the old code is fixed; CNA does not keep a convenience alias for outdated call sites. The rules' own wrong/right pair is a free-standing inline const Color CornflowerBlue(...) from an early CNA versus the single source of truth Color::CornflowerBlue.
The C++ dialect: a C++23 floor, a C++17/20 idiom
CMakeLists.txt sets CMAKE_CXX_STANDARD 23 with CMAKE_CXX_STANDARD_REQUIRED ON and CMAKE_CXX_EXTENSIONS OFF, and the shared interface target cna_build_config in modules/CMakeLists.txt adds target_compile_features(… cxx_std_23), so a game that links any CNA module is compiled as C++23 as well. The standard level is a toolchain floor, not a feature list. A text search of modules/ at this snapshot finds no C++23-only library facility (std::expected, std::print, import std, std::flat_map, std::mdspan, std::generator, std::stacktrace, std::move_only_function, std::to_underlying, std::unreachable) and none of the C++23 language additions such as explicit object parameters ("deducing this"), if consteval or [[assume]]; there are no coroutines, no C++ modules and no operator<=>.
What the code does use is C++17 and C++20: std::optional for XNA's nullable values, std::string_view, [[nodiscard]], std::span buffer views (several hundred uses), std::bit_cast (58 places in production sources, none in the math sources, which type-pun through std::memcpy), a few std::ranges algorithms (only in four test and example files, none in production sources), constinit for the four Vector2 constants and std::vformat for the content pipeline's indexed {0} message formatting (ContentBuildLogger.hpp). Concepts are confined to a handful of public headers: the element-type constraints of Texture3D.hpp and TextureCube.hpp, the engine layer's GltfMaterialSourceEXT (GltfMaterialBridge.hpp) and two content-pipeline templates, while ConstantBuffer and ComputeShader constrain their upload templates with requires clauses. Read "CNA uses C++23" as "CNA needs a C++23 compiler"; ported game code needs no C++23 construct.
The error model: exceptions in two families
CNA reports failure the way XNA does: by throwing, or through Boolean results (XNA's own, such as Matrix::Decompose, and a few CNA Try…EXT methods). std::expected is not used, and only the C ABI turns exceptions into result codes, at its barrier. The Game class: one catch clause lists the two families a game meets (standard exceptions, and Sharp Runtime's System::Exception, which derives from std::exception but not from std::runtime_error). Across the tree they split roughly by layer. In production sources (tests and examples excluded) about four fifths of the throw std::runtime_error sites are in modules/renderers, where they mean "this renderer cannot do that", as the default bodies in IGraphicsRenderer.hpp do, although renderers also throw System::NotSupportedException at many sites. The public graphics, audio and content wrappers mostly throw the .NET-shaped types an XNA program expects at the same call. Use after disposal shows both the split and its exception: System::ObjectDisposedException is thrown from over a hundred sites in graphics, graphics-ext, audio, gamer-services, net, devices, media and content and from one renderer site, but the runtime module's Game reports use after disposal as std::runtime_error, which is what the coding rules in CLAUDE.md still prescribe. Catch the XNA type where recovery depends on it, and std::exception at the program boundary.
There is no project-wide noexcept policy; the keyword appears mostly on move operations. The one place the project reasons about it is disposal: CHECKLIST.md keeps VideoPlayer::Dispose() idempotent instead of copying FNA's throw on a second call, because ~VideoPlayer() calls Dispose() again, destructors are implicitly noexcept, and an exception leaving one ends the program (VideoPlayerTest.DisposeIsIdempotent pins the behaviour). The browser build's exception ABI is described on Web build: exception ABI.
Physical modules own a namespace-shaped include tree
Each physical module owns its declarations under modules/<module>/include/ and its implementation under modules/<module>/src/. The include tree reproduces the public namespace path, so a consumer's include spelling is stable API and identical whichever module owns the header; implementation paths use a shorter area convention (src/Xna/ for the XNA API, src/Internal/ for engine parts, src/CnaExt/ for extension surfaces):
modules/graphics/include/Microsoft/Xna/Framework/Graphics/Texture2D.hpp # declaration
modules/graphics/src/Xna/Texture2D.cpp # implementation
# included everywhere as #include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
Namespace directories and module ownership are different axes. The Microsoft/Xna/Framework/Graphics include directory at this snapshot holds 134 headers: 115 directly in the graphics module's directory, 18 in its PackedVector child, and one, PackedVector/IPackedVector.hpp, owned by the math module so that Color can implement the packed-vector interface without math depending on graphics. Non-template implementations do not live in headers, and a production translation unit outside every declared module fails the configure step (the source-partition validator in modules/CMakeLists.txt). The public header index lists every header by module.
Documentation: full Doxygen blocks, or nothing
Every public method, constructor, property accessor, operator and constant in a header carries a /** @brief ... */ block, with @param and @return where they apply; bare /// comments are allowed only inside method bodies. The intent of the C# XML comments (<summary>, <param>, <returns>) is carried over, often verbatim from FNA where the wording fits, but never tagged "taken from FNA": provenance belongs in the porting record, not in the header. The rule is held by review more than by tooling: the root Doxyfile sets WARN_IF_UNDOCUMENTED = YES but WARN_AS_ERROR = NO (see Conventions: how each rule is held).
Missing dependencies: stub correctly, never invent
When a ported file needs a type that does not exist yet, the rule is layered. A .NET type (System.* or a primitive alias) goes into sharp-runtime first. Anything else gets a minimal, correctly named stub in its final XNA namespace, just enough to compile, never a large unrelated system built to satisfy one reference; every stub is reported in the task or change description so it is not later mistaken for a real implementation.
The lifecycle is visible in System::IO::BinaryReader. XNB readers call ReadChar() and ReadDecimal(), and for a while sharp-runtime lacked them, so a CNA checkout that used them could not build. The repair was not an ad-hoc replacement inside CNA but an implementation in sharp-runtime (its history records ReadChar() with UTF-8 decoding arriving on 2026-07-16, commit 4cbf00db). At sharp-runtime 41b918c9, ReadChar() decodes one UTF-8 scalar from the stream and throws System::FormatException for an invalid lead byte, an invalid continuation byte, an overlong encoding or a surrogate or out-of-range scalar. A scalar above 0xFFFF does not throw: the call returns the high surrogate and queues the low surrogate for the next read, so a sequence of ReadChar() calls yields valid UTF-16. (Older descriptions said such a scalar threw; that no longer matches the code.)
The per-file porting checklist
CHECKLIST.md governs porting one FNA source file and is meant to be completed in one pass, not "make it compile now, finish later". Its non-negotiable minimum:
// SPDX-License-Identifier: MS-PLat the top of both the.hppand the.cpp;#include "CNA/CNAHelper.hpp"in any header that usesCNAEXT;- the type's full member list extracted from the Microsoft reference XML and diffed against CNA, not only against FNA;
- every method body compared line by line with FNA, and every intentional deviation explained by a
//comment at the code; - every concrete class that derives from
System::ObjectoverridesGetTypeName(), markedCNAEXT, returning the fully qualified .NET name (Game::GetTypeName()returns"Microsoft.Xna.Framework.Game"); - a unit test for every public method, operator and constant, with
ref/outoverloads tested separately from their value-returning forms, each static factory (CreateFromPoints,CreateMerged,CreateFromSphere) tested on its own, equality tested both ways andGetHashCode()tested for consistency.
Which of these rules a tool enforces and which only review holds is tabulated in Conventions; the test rule, for example, has no per-member coverage gate for the C++ surface.
Why the conventions exist
None of these rules is unusual on its own; alias tables and documentation mandates appear in many C++ style guides. What is unusual is that they all serve one measurable goal: keeping a large multi-renderer, multi-platform codebase auditable against external references (Microsoft's reference assemblies, FNA, the real XNA runtime) at every layer, from namespace and visibility through member order and type alias to a compile flag that can turn "did a non-XNA member leak into this code?" into a build error. When another page says a class "matches FNA" or "matches XNA", these conventions are what make the claim checkable.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- C++ API differences from C# XNA · CNA vs alternatives
- Architecture
- Physical module graph
- Internals
- Core module internals
- Maintainer workflow
- Conventions · Change public XNA behaviour
- Reference
- Public header index
- Deep dives
- CNA and XNA 4.0 · CNAEXT catalogue