Architecture
How a familiar System.* surface becomes a selective, boundary-validated set of native C++23 components.
Goals and architectural boundaries
Sharp Runtime is a native C++23 compatibility library for source ports that benefit from familiar System.* names and behavior. It makes common transformations less disruptive: a port can keep concepts such as Stream, Nullable, Task, HttpClient, or XElement while replacing the managed execution model with ordinary C++ compilation and ownership.
The architecture optimizes for four things: selective consumption, one physical owner for every public header and production source, visible dependency closure, and behavior verified at the boundary where it is implemented. It does not optimize for binary compatibility with .NET, managed assembly execution, or a universal emulation layer.
A namespace or type name is a porting landmark. It is not proof of every overload, CLR object layout, reflection metadata, garbage-collector behavior, or platform guarantee. The component, API, and limitations pages add that evidence.
The native execution model
System::* and project helpersThere is no intermediate language, JIT, managed heap, or CLR host in this path. Headers are compiled into the consumer; static components are linked into a native executable or library. Templates and inline algorithms therefore follow normal C++ rebuild and ABI rules. Exceptions unwind C++ stack frames, destructors perform cleanup, and threads are native threads or native abstractions built over them.
Namespace organization versus physical ownership
Logical organization and build ownership are related but not identical. Public APIs primarily use familiar nested namespaces such as System::Collections::Generic, System::IO, System::Net::Http, and System::Threading::Tasks. Physical CMake components use concise dotted names such as Collections.Core, IO, Net.Http, and Threading.Tasks.
| Concept | Example | What it controls |
|---|---|---|
| Namespace | System::Net::Http::Headers | Source-level discovery and naming |
| Physical owner | modules/net-http-headers | One include tree, tests, and any implementation sources |
| Selectable component | Net.Http.Headers | CMake closure and production boundary |
| Imported target | SharpRuntime::Net.Http.Headers | Consumer include and link requirements |
| Compatibility target | SharpRuntime::Core | Legacy umbrella, not another physical owner |
The generated API inventory maps every public header path back to exactly one of the 41 physical components at the pinned revision. Namespace breadth is therefore discoverable without pretending one namespace equals one archive.
Physical components and compatibility targets
The selected source registers 41 physical components. A physical component is either STATIC, with production translation units and a static archive, or INTERFACE, whose public implementation is header-only. Each has one target, one include directory, declared dependencies, and optional tests. CMake rejects duplicate registration, a static component without sources, an interface component with sources, and an interface component with private dependencies.
Four broader names exist for compatibility or aggregation:
CoreaggregatesCore.Base,Console,TimeZone, andUri.Collectionsaggregates the four physical collection components.Xml.XPathaliases the physicalXmlarchive because the implementation is not a distinct ownership boundary.Allenables all physical components; only this complete selection provides the raw legacySHARP_RUNTIMEforwarding target.
SharpRuntime::Headers is a common support target that supplies shared include and C++23 requirements. It is not a user-selectable API component.
CMake ownership and dependency closure
The root project requires CMake 3.20 and exact C++23 mode with compiler extensions disabled. A standalone checkout defaults to All; an embedding consumer should set SHARP_RUNTIME_COMPONENTS before add_subdirectory. The enablement engine recursively resolves dependencies, removes duplicates, and materializes only the requested closure.
set(SHARP_RUNTIME_COMPONENTS
Net.Http.Json
IO.Hashing
)
set(SHARP_RUNTIME_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(sharp-runtime)
target_link_libraries(app PRIVATE
SharpRuntime::Net.Http.Json
SharpRuntime::IO.Hashing
)
The consumer names its direct needs. It does not manually repeat Core.Base, Text.Json, Threading.Tasks, or other transitive requirements. At this pin the component catalogue contains 92 direct production edges.
Public, private, and test-only edges
| Edge | Use it when | Consumer effect |
|---|---|---|
PUBLIC_DEPENDENCIES | A public header exposes or includes the dependency | Propagates through the imported target |
PRIVATE_DEPENDENCIES | Only production implementation needs it | Links the owner without exposing its include surface |
TEST_DEPENDENCIES | Only the owning test executable needs it | Never becomes a production edge |
This separation matters. A passing component test may use helpers from another module without licensing that dependency to production. Conversely, a public header that leaks a sibling include needs a public edge or a redesign; a global include path is not an acceptable fix.
Public and private boundaries
Every module owns an include/ tree, optional src/, and optional tests/. Public consumers see the selected targets’ include directories. Implementation headers beneath src/, sibling module internals, test helpers, and vendor implementation details are not part of that surface.
The boundary validator checks unique ownership, declared includes and links, visibility, cycles, registered source coverage, and other graph invariants. Positive selective consumers prove that a narrow target can be configured and linked. Negative fixtures prove that private or unrelated headers remain unavailable. Those tests are architectural tests: a behavior unit test cannot detect an accidentally global include directory.
External dependencies stay scoped
| Owner | External facility | Visibility |
|---|---|---|
IO.Compression | ZLIB | Private |
IO.Compression.Zip | Vendored miniz | Private |
Xml | Vendored tinyxml2 | Public, because public headers expose its types |
Text.Json | Vendored nlohmann JSON headers | Header surface |
Net on Windows | ws2_32 | Private |
Security.Cryptography.Random on Windows | BCrypt | Private |
Storage on Android | Parent-provided SDL3 target | Private |
External libraries do not become an undifferentiated global dependency set. Their owner and visibility are recorded in the generated catalogue so a selective consumer can understand why a package is required.
Native object and value model
Sharp Runtime does not impose one representation on all familiar .NET concepts. Primitive aliases, strings, vectors, nullable values, and many wrappers are ordinary C++ values. Some polymorphic hierarchies opt into System::Object. Interfaces are native abstract base classes. Callbacks are typed C++ callables. This mixed model is intentional.
Object is abstract and provides virtual destruction, default identity equality, address-derived hashing, ToString, RTTI-backed GetType, and a project-specific stable GetTypeName hook. It is not the base of every non-primitive C++ type. Type wraps std::type_info; its names may be mangled, Name and FullName collapse, and its classification predicates are fixed compatibility stubs. See the type-system guide before porting reflection-driven code.
Resource ownership and lifetime
RAII is the baseline. Values and smart pointers carry ownership; references, raw pointers, spans, memory views, event subscriptions, and callbacks borrow according to an explicit lifetime contract. Compatibility-shaped Close and Dispose operations may define observable state, but they do not replace deterministic C++ destruction.
| Representation | Typical role | Main hazard |
|---|---|---|
| Direct value/member | Required value with enclosing lifetime | Copy or move semantics may differ from managed identity |
std::unique_ptr | Exclusive dynamic owner | Borrowed callbacks must not outlive it |
std::shared_ptr/weak_ptr | Genuine shared lifetime and observer | Cycles or accidental lifetime extension |
| Reference/raw pointer | Non-owning synchronous access | Dangling access after owner destruction |
Span/Memory | Contiguous non-owning range | Owner destruction or vector relocation invalidates the view |
Platform abstraction is local, not magical
Platform branches live near the facility they implement: socket code selects Windows or POSIX APIs; random generation attaches the appropriate entropy source; time-zone discovery has Windows and POSIX paths; storage can attach SDL3 on Android. Unsupported operations should either be excluded by a documented build decision or throw a specific PlatformNotSupportedException. A silent no-op is not a platform abstraction.
Some components remain platform-specific. At the selected revision FileSystemWatcher uses Linux inotify, NetworkInterface enumeration is Linux-specific, and process behavior is POSIX-oriented. Emscripten has explicit reductions for DNS and native facilities. The platform matrix separates implementation branches from configure, build, runtime, and test evidence.
Threading and callback implications
Threads, tasks, timers, event handlers, watchers, sockets, and HTTP wrappers all execute under native lifetime and synchronization rules. Capturing this does not keep an object alive. Closing an owner does not automatically cancel every detached operation. A callback may run concurrently with reconfiguration unless the API documents serialization.
The FileSystemWatcher remediation at this pin demonstrates the intended architecture: callback-thread identity is recorded, self-disable uses deferred teardown instead of joining the current thread, reconfiguration from inside a handler is rejected atomically where unsafe, handler exceptions flow through the Error event, and an Error handler’s exception is contained. This is a targeted lifecycle contract, not a general promise that every callback-bearing type is reentrant or thread-safe.
Exception model
The runtime supplies a native C++ exception hierarchy with familiar names, messages, selected HResult values, and causal exception pointers where implemented. Throwing and catching use C++ syntax; stack unwinding runs destructors. Catch exceptions by reference. There is no CLR exception object header, managed stack, universal exception filter, or semantic guarantee that a matching name behaves identically in every edge case.
Component owners translate low-level errors at their boundary: filesystem errno, socket status, parser failures, and platform-not-supported cases should become the most specific established public exception. Contributor tests check precedence as well as type—for example, whether invalid arguments are rejected before state mutation or native I/O.
Build and test architecture
With tests enabled, each physical module that owns test sources receives a separate GoogleTest executable. The SharpRuntimeTests name is an aggregate build target, not one monolithic binary. A complete All configuration also builds one integration executable for genuinely cross-component scenarios. At the selected pin this produces 37 component executables plus one integration executable.
CTest discovers individual typed, parameterized, and ordinary GoogleTest cases from the built executables. That is why source macro counts are not used as a headline. The measured full gate at the exact selected SHA discovered and ran 17,131 cases; its six failed remain documented rather than converted into a green status.
Behavior tests are only one layer. The local gate also includes selective consumers, negative include/link fixtures, graph validation, generated-catalogue drift checks, integration behavior, formatting/build checks, and a Doxygen warning ceiling.
Generated metadata and documentation
cmake/SharpRuntimeModules.cmake and each module registration are the build authority. The runtime’s component-catalogue generator derives physical names, kinds, owners, edges, headers, and external attachments. This website pins that catalogue to a SHA in data/components.json, derives a complete public-header inventory into data/api-inventory.json, and centralizes volatile gate facts in data/site-facts.json.
Validation compares these committed facts against an explicitly supplied checkout. It never silently reads whichever branch happens to be active. Search, canonical metadata, sitemap entries, heading anchors, and this page shell are regenerated deterministically from the canonical content sources.
Deliberate non-goals
- No CLR, JIT, managed assembly loader, or garbage collector.
- No claim of source compatibility: C# language constructs still require translation.
- No universal
Objectinheritance or managed object layout. - No complete metadata reflection, dynamic invocation, P/Invoke, remoting, or BinaryFormatter model.
- No requirement that a compatibility type reproduce unsupported .NET internals when an explicit native contract is safer.
- No platform label derived merely from the existence of a preprocessor branch.
Architectural checklist for a change
- Identify the observable contract and the deliberate reductions.
- Select one physical owner and verify its public/private dependency direction.
- Choose value, unique, shared, weak, or borrowed lifetime explicitly.
- Keep platform code local and make unsupported behavior visible.
- Add normal, invalid, boundary, lifetime, concurrency, and platform tests as applicable.
- Add a selective or negative consumer fixture when the public boundary changes.
- Regenerate the catalogue and API inventory, run the graph validator, and update limitations or conceptual documentation.