sharp-runtime components and how CNA consumes them
Evidence basis: source-verified at the pinned commit. 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. CNA facts were read at 009d40f5; sharp-runtime facts were read at next @ 41b918c9 (2026-09-20), the tip of next when the snapshot was made, which CNA does not pin. Counts were measured from the Git trees with the methods stated. No configure or build was run; the configure failure text for main and develop is derived from reading both repositories.
sharp-runtime is the one sibling repository that every CNA build compiles: the C++23 library that supplies the .NET System.* vocabulary (TimeSpan, IDisposable, exceptions, collections, streams) that CNA's XNA surface is written in. This page explains how that library is cut into CMake components, how an include path maps to a component and a link closure, and how CNA snapshot 009d40f5 selects, links and instruments exactly the components it needs. It is for maintainers who change CNA's CMake or upgrade the sharp-runtime checkout, and for anyone who needs to know which sharp-runtime revision a statement refers to.
Which sharp-runtime a CNA build uses
CNA does not pin sharp-runtime. The root CMakeLists.txt adds whatever checkout sits at CNA_SHARP_RUNTIME_ROOT (default ../sharp-runtime) with add_subdirectory; there is no submodule, no vendored copy, no FetchContent fallback, no lock file and no SHA comparison. A missing CMakeLists.txt at that path stops the configure with a message that names the checkout, says that git submodule update --init will not fetch it and prints a clone command. What a build compiles is therefore decided by the caller's checkout, not by CNA.
At this snapshot the checkout must be sharp-runtime's next branch. CNA's component closure names Resources and Xml.Serialization, and the module directories that register them (modules/resources, added 2026-09-01, and modules/xml-serialization, added 2026-08-31) exist only on next; main (54578590, 2026-08-22) and develop (df1b42ab, 2026-08-28) have 41 module directories and neither component. The failure on those branches happens inside the sibling's own configure, before CNA's shape detection runs: CNA passes its closure in SHARP_RUNTIME_COMPONENTS, sharp-runtime's sharp_runtime_enable_components() reaches a name it has not registered and stops with Unknown Sharp Runtime component 'Resources'. Available components: …, All (read in both repositories; the configure was not executed).
Every sharp-runtime statement on this page and its three neighbours was read at sharp-runtime next @ 41b918c9 (2026-09-20 23:49 +0200). That commit was the tip of next when CNA snapshot 009d40f5 was made (2026-09-24), so it is the revision a git clone -b next of that day produced, but it is evidence for that sharp-runtime revision, not a version CNA promises to work with. The library calls itself version 0.1.0-beta.1 and is MIT-licensed. Descriptions written against the 2026-08-10 modularisation merge (f827a6c5) are 422 commits older. CNA's own CI obtains the sibling two ways, a branch-picking clone script (scripts/ci/clone_siblings.sh) and several workflows pinned to August commits that lack the components above; the list and its consequence are on Platforms: CI.
What the library contains at 41b918c9
sharp-runtime describes itself as a practical subset of .NET's System.* for native ports "without attempting to implement a CLR, JIT, garbage collector, or the complete .NET platform". The subset is wide: collections (generic, immutable, frozen, concurrent, object-model), text with regular expressions, JSON and XML (including LINQ to XML and XPath), streams, compression, ZIP, hashing and isolated storage, sockets, HTTP, MIME and WebSockets, threads, tasks, channels and timers, numerics, and non-encryption cryptography. CNA links a small part of it. The counts below were measured from the Git tree at 41b918c9:
| Measure | Value | How it was counted |
|---|---|---|
| Module directories | 44 | the ordered list in cmake/SharpRuntimeModules.cmake |
| Registered component names | 47: 33 STATIC, 13 INTERFACE, 1 alias | registration calls in modules/*/CMakeLists.txt |
| Direct production dependency edges | 108 | the generated docs/ComponentCatalog.md |
| Public headers | 1,097: 1,090 below System/, 7 below SharpRuntime/ | files under modules/*/include/ |
| Private headers | 9 | .hpp files under modules/*/src/ |
| Implementation files | 230 | .cpp files under modules/*/src/ |
| Flat top-level types | 225 | headers directly in System/ (Object, String, TimeSpan, Guid, Uri, Span, …) |
| Test sources | 527 in modules/*/tests/, 12 in tests/ | .cpp files |
| Static test definitions | 17,432 | TEST, TEST_F, TEST_P, TYPED_TEST, TYPED_TEST_P macros (not registered or executed cases) |
| Physical lines | 191,545 production, 187,414 test | every line of include/+src/ versus tests/, comments and blank lines included |
Three of these numbers are often confused. Directories, registered names and dependency edges measure different things (see the registry). The README at the same commit still says "41 independently selectable CMake components" and quotes a 2026-08-22 Linux baseline of 17,840 tests across 38 executables, while its own generated catalogue reports 44 physical modules; the continuation log NEXT.md records later suites. A count from sharp-runtime is only meaningful with its date and method, and a static macro count is not a pass count. How the project measures itself is on parity and verification.
Two include roots and the transliteration rule
Every public header sits below one of two roots. System/ is the literal .NET namespace tree; SharpRuntime/ holds the project's own helpers: SharpRuntimeHelper.hpp (the primitive aliases such as intcs and bytecs, their limits and the 128-bit capability macro), Prop.hpp (property macros), Experimental/Property.hpp and Experimental/ReadonlyProperty.hpp, PortableFromChars.hpp, PortableScan.hpp and Storage/StoragePaths.hpp. There is no umbrella header: no SharpRuntime.hpp, no "include everything". The include spelling is a mechanical transliteration of the fully qualified .NET name, and one public type per like-named header is the norm:
| .NET name | Include | Owning component |
|---|---|---|
System.TimeSpan | System/TimeSpan.hpp | Core.Base |
System.Collections.Generic.List | System/Collections/Generic/List.hpp | Collections.Core |
System.Collections.ObjectModel.Collection | System/Collections/ObjectModel/Collection.hpp | Collections.Core (the Collections.ObjectModel component owns ObservableCollection and its neighbours) |
System.IO.MemoryStream | System/IO/MemoryStream.hpp | IO |
System.Text.UTF8Encoding | System/Text/UTF8Encoding.hpp | Text |
System.Threading.Tasks.Task | System/Threading/Tasks/Task.hpp | Threading.Tasks |
System.Security.Cryptography.SHA256 | System/Security/Cryptography/SHA256.hpp | Security.Cryptography |
System.Resources.ResourceManager | System/Resources/ResourceManager.hpp | Resources |
SharpRuntime::intcs (not .NET) | SharpRuntime/SharpRuntimeHelper.hpp | Core.Base |
The rule makes a port reviewable: an include that cannot be derived from the type name deserves a second look. Private headers under a module's src/ are unreachable to consumers because registration sets the component's include directory to that module's include/ only. The row for Collection.hpp shows why the namespace alone does not name the component: the file lives in the collections module although its namespace is ObjectModel.
How the component registry works
A module's CMakeLists.txt is a declaration, not a standalone project, and it never calls add_library. It calls sharp_runtime_register_module(NAME … TARGET … TYPE STATIC|INTERFACE PUBLIC_DEPENDENCIES … PRIVATE_DEPENDENCIES … TEST_DEPENDENCIES … SETUP …) from cmake/SharpRuntimeComponents.cmake. The registry globs src/*.cpp for a STATIC module and tests/*.cpp for every module, and enforces the shape: a compiled component without sources, a header-only component with sources or with private dependencies, a component registered twice, and an alias that specifies anything besides ALIAS_OF are all fatal configure errors. Targets are materialised only when a component is enabled.
Three names are not modules. Core (over Core.Base, Console, Uri and TimeZone) and Collections (over Collections.Core, Collections.Blocking, Collections.Async and Collections.ObjectModel) are compatibility umbrellas registered with sharp_runtime_register_component, and Xml.XPath is an alias of Xml, whose implementation boundary it shares. That is why 44 directories register 47 names, and why neither number is the 108 dependency edges. Consumers link the namespaced form, SharpRuntime::Core.Base, never an archive file name.
Enabling a component enables its dependencies first, depth first; a dependency cycle stops the configure (Cyclic Sharp Runtime component dependency), and so does an unknown name, with the list of registered names. Requesting All enables every physical component and only then creates SharpRuntime::All and the legacy SHARP_RUNTIME interface target that forwards to it; the comment beside that code says it is deliberately not unconditional, because it would instantiate every optional component and external dependency.
Ownership leaks are made visible before anything compiles. sharp_runtime_validate_source_partition() requires every modules/*/src/*.cpp file to belong to exactly one registered component and lists duplicates, orphans and registered paths that do not exist. The dependency allowlist, cmake/SharpRuntimeModuleDependencyAllowlist.json, is empty ("dependencies": []), so there is no standing list of tolerated exceptions. scripts/validate_module_boundaries.py checks ownership, include resolution, declared visibility, stale edges, duplicate public paths and cycles, and scripts/generate_component_catalog.py --check rejects a catalogue that no longer matches the registrations.
Standalone selection and CNA's embedded selection
When sharp-runtime is the top-level project it creates a SHARP_RUNTIME_COMPONENTS cache entry defaulting to All. When it is added as a subdirectory it creates no cache entry, but it still honours a SHARP_RUNTIME_COMPONENTS value the parent has set; only an undefined or empty value falls back to All.
CNA sets that value. Before add_subdirectory, the root CMakeLists.txt includes cmake/SharpRuntimeConsumption.cmake, writes CNA_SHARP_RUNTIME_DEFAULT_COMPONENTS into SHARP_RUNTIME_COMPONENTS when the caller has not set it, and otherwise merges the closure into the caller's list and forces the cache entry. The merge exists for incremental builds: a build directory configured before a component joined the closure (the Xml component was added that way) picks it up on the next reconfigure instead of failing at link time. The comment beside it gives the reason for not accepting All: it instantiates unrelated optional components such as System.Diagnostics.Process, which wastes build time and does not compile against Apple's iOS SDK.
So a normal CNA build configures and compiles CNA's closure and its transitive dependencies, not the whole library, and SharpRuntime::All never exists. Because many older CNA examples and tests still name SHARP_RUNTIME, the root file recreates that name as an INTERFACE target over CNA's closure after shape detection. A game that needs another component, for example Text.Json, sets SHARP_RUNTIME_COMPONENTS before adding CNA and links the narrow target; CNA appends its own closure to that list (Tutorial 89 walks through it). CNA also forces SHARP_RUNTIME_BUILD_TESTS off, so the sibling's component test executables are never part of a CNA build.
Older descriptions of this seam say that the embedded sibling falls through to All and compiles every component, with CNA's narrow lists affecting only link interfaces. That was true of an earlier CNA; at this snapshot the narrow closure also limits what is configured and compiled.
CNA's closure and per-module declarations
CNA_SHARP_RUNTIME_DEFAULT_COMPONENTS is Core.Base, IO, Collections.Core, Collections.ObjectModel, Runtime, Threading, Text, Globalization, ComponentModel, Storage, Security.Cryptography, Xml and Resources, plus Xml.Serialization on every target except Windows. Two entries carry their reason in the file. Resources is listed because ResourceContentManager's documented constructor names System::Resources::ResourceManager and nothing else in the list depends on it publicly, so it would otherwise never be instantiated. Xml.Serialization is listed because the math module's XmlSerializationEXT.hpp includes one of its headers; it is header-only, so selecting it costs nothing until a translation unit includes it. Lower components such as Uri, TimeZone and Buffers arrive through public edges (IO and ComponentModel depend on Uri, Threading on TimeZone, Text on Buffers) and are deliberately not repeated: copying the transitive closure into CNA would duplicate the sibling's registry and break on its next change.
Every CNA target links sharp-runtime through cna_link_sharp_runtime(<target> <PUBLIC|PRIVATE|INTERFACE> [<Component>…]). Against a modular checkout it prefixes each name with SharpRuntime::; with no names it uses the default closure; against a monolithic checkout it links the single SHARP_RUNTIME archive. cna_detect_sharp_runtime_shape() decides between the two by testing whether the target SharpRuntime::Core.Base exists, not by reading a version string, and prints either CNA: sharp-runtime is MODULAR -- CNA modules link specific SharpRuntime:: components or the monolithic message. Nineteen module CMakeLists.txt files make 21 explicit calls:
| CNA module (target) | Components named |
|---|---|
core, math, devices, devices-ext, platform, graphics-ext | Core.Base |
video-ffmpeg | Core.Base (private) |
input | Core.Base, Collections.Core |
design | ComponentModel |
phone | Core.Base, Collections.Core, Uri |
media | Core.Base, IO, Uri |
graphics (cna_graphics_core) | Core.Base, IO, Collections.Core, Text |
audio | Core.Base, IO, Runtime |
runtime | Core.Base, Globalization, IO |
storage | Core.Base, IO, Runtime, Threading; private Storage |
content | Core.Base, IO, Resources; private Security.Cryptography (the build manifest's SHA-256) |
content-pipeline | Core.Base, IO, Collections.Core, Xml |
net (CNA_Net) | Core.Base, IO, Collections.Core, Runtime, Threading |
gamer-services | the same plus Globalization |
| every renderer family target | the default closure, public, through cna_renderer_common_setup() in modules/renderers/CMakeLists.txt |
| tools, harnesses and test hosts | mostly the default closure, private |
Small modules really are narrow: math needs only Core.Base. A narrow list documents and enforces a target's link interface; it is the durable part of the contract, because it keeps working whatever the sibling's internal dependency graph becomes.
What CNA includes directly
At this snapshot CNA's production sources (modules/*/include and modules/*/src) include 81 distinct sharp-runtime headers: 78 below System/ and three below SharpRuntime/ (SharpRuntimeHelper.hpp, Prop.hpp and Storage/StoragePaths.hpp); with tests, examples and tools the figure is 89. CNA owns no System/ header of its own. All 81 resolve at 41b918c9 and belong to 14 components: Core.Base (33 headers), Collections.Core (11), IO (11), ComponentModel (6), Xml (6), Threading (3), Runtime (3), Globalization (2) and one each from Storage, Resources, Security.Cryptography, Text, Uri and Xml.Serialization. Counting only the SharpRuntime/ prefix and concluding that CNA uses "two runtime headers" mistakes a namespace prefix for a repository boundary.
Some of those includes reach into sharp-runtime's detail namespaces: NetworkSessionProperties.hpp holds a System::Collections::detail::MutationCounter so that its enumerator fails fast like sharp-runtime's own collections (why that type exists). A change to such a header in the sibling is a CNA source change even though it looks internal from the sibling's side.
Global macros that arrive with the helper headers
SharpRuntimeHelper.hpp is included by 389 of the library's public headers, and it defines two unnamespaced macros besides the aliases: CONTAINS(STRING, SUBSTR) and an empty INTERNAL. Prop.hpp is a pure macro header that defines 31 names, from the property generators DEF_PROP, DDATA, DGETTER, IDATA and IGETTER to their token-pasting helpers, several of which are ordinary words: YES, NO, nothing, ref1, static0, getter1. Inside sharp-runtime no production header includes it (the two Experimental property headers only mention it in comments and define their own DEF_PROP_AUTO and IMPL_PROP_* macros; the only in-repository includer at 41b918c9 is the integration test tests/integration/Task41Tests.cpp), so its consumer is CNA, whose public Viewport.hpp declares X, Y, Width, Height, MinDepth and MaxDepth with DEF_PROP, as does ContentManager.hpp for RootDirectory and the sensor reading headers. Every translation unit that includes those headers therefore sees all of the macros, and a game identifier spelled nothing, YES or NO after such an include is rewritten by the preprocessor (see Known Issues). Treat them as global preprocessor names, not as members protected by a namespace.
What the property macros generate. DEF_PROP(type, name, getter, setter, member, static, const-return, ref-return, const-method) declares, inside the class it is written in, an optional private backing field name_, a [[nodiscard]] accessor getNameProperty() and two setters, setNameProperty(const type&) and setNameProperty(type&&); the getter1/getter0, setter1/setter0 and member1/member0 arguments switch each piece on or off. DDATA (field, getter and setter), DGETTER (field and getter) and DGETTERSTATIC (a static getter) are presets of it, and IDATA, IGETTER and IGETTERSTATIC are their counterparts for the .cpp file, which define the accessor bodies; IMPL_PROP is the low-level form. CNA's Viewport.hpp uses the low-level form with member0: the class keeps its own X_, Y_ and similar fields, the header declares the accessors, and Viewport.cpp defines them with IMPL_PROP. Read from Prop.hpp at 41b918c9 and CNA at 009d40f5; not compiled.
Build settings CNA pushes into the sibling
Several CNA decisions have to reach sharp-runtime's own targets, and the order of the root file is arranged for that.
- Instrumentation and debug information. After shape detection,
cna_apply_sharp_runtime_build_support()incmake/BuildPerformance.cmakeasks the sibling for its enabled set withsharp_runtime_get_enabled_components(), resolves aliases to their real targets and linkscna_instrumentation,cna_debug_info_optionsandcna_emscripten_exception_abiprivately into each one. A sanitizer build of CNA therefore instruments the sharp-runtime code it links, and a stack in aSystem::function is as trustworthy as one in CNA's own code. - Position-independent code and caching.
CNA_SHARED_LIBRARY(native ELF with GNU or Clang, CMake 3.27 or newer) andCNA_BUILD_C_APIswitch on position-independent code before the sibling is added, because the static archives that go into the shared library need it;CNA_USE_CCACHEis configured before it too, so the launcher covers the sibling's translation units. - Threaded WebAssembly. With
CNA_ENABLE_EMSCRIPTEN_THREADS, CNA forcesSHARP_RUNTIME_ENABLE_EMSCRIPTEN_THREADSon:-pthreadchanges the WebAssembly memory ABI, so every object in the executable must be compiled for it. sharp-runtime itself rejects that option outside an Emscripten toolchain and requires a positiveSHARP_RUNTIME_EMSCRIPTEN_PTHREAD_POOL_SIZE. - MSVC. The sibling promotes warnings to errors; CNA disables only warning C4456 (
/wd4456) on the five date and time translation units ofsharp_runtime_core_base(DateOnly,DateTime,DateTimeOffset,TimeOnly,TimeSpan), whose shared parser declares a harmless inner variable. - Windows and Xml.Serialization. On a Windows target
CNA_SHARP_RUNTIME_HAS_XML_SERIALIZATIONis off, the component is left out of the closure, andcmake/UnitTests.cmakeremoves the math and inputXmlSerializationEXTTestssources. The stated reason is that the component'sXmldependency privately depends onDiagnostics, whoseProcess.cppincluded<poll.h>outside its POSIX guard. sharp-runtime moved that include inside the guard on 2026-09-14 (88c12f15), which41b918c9contains, but the exclusion and its "remove this condition once sharp-runtime guards that include" comment remain at this snapshot, so theXmlSerializationEXTsurface is absent from Windows builds even with the requirednextcheckout. - Android storage. The
Storagecomponent's setup function linksSDL3::SDL3(orSDL3::SDL3-static) privately on Android only if that target already exists, becauseStoragePathsasks SDL forSDL_GetAndroidInternalStoragePath(). CNA creates its SDL targets before adding the sibling, which is what makes the edge appear; it is a conditional, platform-specific edge, not a general SDL dependency of sharp-runtime.
The seam as one picture
CNA source file CNA CMake sharp-runtime CMake
--------------- --------- -------------------
#include "System/IO/MemoryStream.hpp"
| SHARP_RUNTIME_COMPONENTS =
v CNA closure (+ caller extras)
owning CNA target --- cna_link_sharp_runtime(cna_content |
(e.g. cna_content) PUBLIC Core.Base IO Resources) --------+ v
| registry: 44 modules,
| 47 names (33 static,
| 13 interface, 1 alias)
| |
v v
SharpRuntime::IO ----> enabled set = closure + public deps
(Uri, TimeZone, Buffers ...)
|
v
final CNA program (static archives or libcna.so)
Not created in a CNA build: SharpRuntime::All. SHARP_RUNTIME = CNA's own INTERFACE
over the closure, kept for older examples and tests.
Reading a dependency from source
A reliable dependency review works in three steps and in this order:
- Derive the include from the type's fully qualified name (
System.Xml.XmlReaderbecomesSystem/Xml/XmlReader.hpp). - Find the module whose
include/directory owns that file (git ls-tree -r --name-only <rev> modules/ | grep System/Xml/XmlReader.hppgivesmodules/xml). - Read that component's registration, or its row in
docs/ComponentCatalog.md, for its public, private and test-only dependencies instead of inferring a closure from nested namespaces.
Namespace depth, directory depth and link depth usually agree; the umbrellas, the Xml.XPath alias and cases like Collection.hpp are where they do not, and there the registry is the authority. The method also marks the limit of what an include proves. Threading.Tasks is not in CNA's closure and no CNA module includes a task header, so a game that wants System::Threading::Tasks::Task must name that component itself. And a header that resolves proves only that a type can be named: UTF8Encoding's malformed-input handling is implemented and tested, while TaskT<TResult> has continuations but no generic multi-task combinators (both on streams, text and tasks).
Upgrading the seam
- Record both revisions, the CNA commit and the sharp-runtime commit, in any report. CNA will not record the second one for you.
- Point
CNA_SHARP_RUNTIME_ROOTat a different checkout (for example a read-only worktree of a pinned commit) instead of moving sibling directories to satisfy the default path. - Read the configure output. The modular message is expected; the monolithic one means an old checkout and a fallback;
Unknown Sharp Runtime componentmeans a branch that lacks part of CNA's closure. - Keep each CNA module's component list narrow and let public dependencies close transitively. Add a component to
CNA_SHARP_RUNTIME_DEFAULT_COMPONENTSonly when no listed component reaches it publicly, asResourceshad to be. - When the sibling's registry changes, run its own boundary gates in the sibling (
scripts/validate_module_boundaries.py,scripts/check_selective_components.sh,scripts/generate_component_catalog.py --check), then CNA's normal build and tests with the sibling's tests still off. - Keep "these components were configured" apart from "this target links only its declared closure" in any verification report; at this snapshot the first is CNA's closure plus dependencies, not the whole library.
- After an upstream fix, revisit the CNA workarounds that cite it: the Windows
Xml.Serializationexclusion above names a condition sharp-runtime has already removed.
The CNA-side mechanics of every sibling, including easy-gl and free-direct, are compared on third-party dependencies; the roles of the repositories are on the CNA ecosystem; clone commands and configure-failure recipes are on Building CNA.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Architecture: required sibling repositories · Build architecture: configure order · Physical module graph
- Maintainer workflow
- Change the build configuration
- Reference
- CMake option index