Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 56 Namespaces, Types, and Include Paths

Sharp-runtime makes a source-porting promise visible in the filesystem: if a developer knows the .NET fully-qualified type name, the public include spelling should be predictable. The module graph adds a second question that a header alone cannot answer: which component owns that include and which dependencies follow transitively? This chapter establishes both rules, then applies them to representative text, stream, encoding, and task types.

56.1 Two public roots, no umbrella

Exactly 1,032 public headers are reachable below two roots; five further .hpp files are private implementation headers below module src/ trees. System/ contains the literal .NET namespace tree; SharpRuntime/ contains project-specific aliases and source-porting helpers. There is no catch-all header. Private headers below a module’s src/ directory are not added to consumer include paths.

The transliteration rule is mechanical:

1 System.IO.FileStream
2 -> #include "System/IO/FileStream.hpp"
3
4 System.Threading.Tasks.Task
5 -> #include "System/Threading/Tasks/Task.hpp"
6
7 SharpRuntime::intcs
8 -> #include "SharpRuntime/SharpRuntimeHelper.hpp"

One public type per like-named header is the norm. The rule makes a port review tractable: an include that cannot be derived from the type name deserves inspection, and an accidental include from another module’s private tree cannot be hidden behind a universal umbrella.

56.2 Namespaces do not equal components one-for-one

The C++ namespace hierarchy describes API identity; the 44 CMake components describe build ownership. Several namespace families split across components to keep link surfaces narrow. Table 56.1 gives representative mappings used by CNA.

Table 56.1: Representative namespace/include/component mappings.
API family Include example Direct component
System core System/TimeSpan.hpp Core.Base
Collections System/Collections/
Generic/List.hpp
Collections.Core
Object model System/Collections/
ObjectModel/Collection.hpp
Collections.
ObjectModel
Streams System/IO/MemoryStream.hpp IO
Text System/Text/UTF8Encoding.hpp Text
Tasks System/Threading/Tasks/Task.hpp Threading.Tasks
Cryptographic hashes System/Security/
Cryptography/SHA256.hpp
Security.
Cryptography

The direct component is not the full closure. Its public dependencies carry lower-level requirements, just as a C++ header’s own includes do. This is why CNA names SharpRuntime::IO rather than copying IO’s current dependency list into its own CMake files. Chapter 54 covers the adapter and its legacy monolith fallback.

56.3 CNA’s dependency subset

CNA directly names 54 distinct runtime headers. Fifty-two use the System/ root. The two SharpRuntime/ spellings are SharpRuntimeHelper.hpp, which defines the C#-fidelity primitives and limits, and Prop.hpp, which exports property-declaration macros for downstream code. Counting only the latter prefix and concluding that CNA consumes two runtime headers mistakes a namespace prefix for a repository boundary.

SharpRuntimeHelper.hpp is unusually load-bearing: hundreds of public runtime headers include it, and CNA uses aliases such as intcs, bytecs, and Single throughout its public surface. It also exports two unnamespaced macros, CONTAINS and INTERNAL; consumers must treat those as global preprocessor names, not members protected by the SharpRuntime namespace.

Prop.hpp is the opposite kind of seam. The runtime’s own property methods are hand-written, and its modules do not include this macro header. CNA does. The file is therefore an exported downstream facility even though a search limited to the library’s implementation would misleadingly classify it as unused.

56.4 String and StringBuilder: static utility versus mutable buffer

System::String is deliberately not a wrapper class at all — its default constructor and destructor are both explicitly deleted, making it a pure static-method utility operating on ordinary std::string values (IsNullOrEmpty, Compare, the IndexOf / LastIndexOf family, Split, Substring, Trim*, PadLeft / PadRight). Text::StringBuilder, by contrast, is a real, stateful, mutable buffer type — the many-overloaded Append (string, char, int, double, float, bool, and the longcs alias from Chapter 3), AppendLine, Insert, Remove, Replace, and a nested ChunkEnumerator class mirroring real .NET’s own chunked-buffer enumeration shape — real .NET’s own StringBuilder is internally a linked list of separate character chunks, and ChunkEnumerator walks that list without copying; this port’s StringBuilder is backed by a single std::string instead, so its own ChunkEnumerator always yields exactly one chunk holding the entire current buffer — the enumeration shape is preserved faithfully even though the simpler single-buffer implementation underneath never actually produces more than one chunk to walk.

1 // Worked example: System::String is a pure static utility -- there is no
2 // instance to construct, only free functions operating on std::string.
3 std::vector<std::string> parts = System::String::Split(
4 "Content/Textures/hero.png", ’/’);
5 std::string trimmed = System::String::Trim(parts.back());
6
7 // Text::StringBuilder, by contrast, is a real mutable buffer.
8 System::Text::StringBuilder sb;
9 sb.Append("Score: ").Append(playerScore).AppendLine();
10 sb.Append("Lives: ").Append(livesRemaining);
11
12 for (const std::string& chunk : sb.GetChunks())
13 {
14 // Exactly one iteration, always -- see the ChunkEnumerator note above.
15 RenderHudText(chunk);
16 }

56.5 Stream: a lightweight subset, honest about it

IO::Stream’s own class comment states its scope plainly: “lightweight subset of the .NET Stream API.” Read(buffer, offset, count), Close(), and getLengthProperty() are pure virtual; Write / WriteByte default to throwing NotSupportedException unless a writable stream subclass overrides them — the same shape real .NET’s own read-only stream implementations follow, rather than forcing every stream subclass to implement a full read/write/seek contract regardless of whether it actually supports all three. getCanWriteProperty() defaults to false and getCanReadProperty() defaults to true, matching a read-only stream’s honest self-report without requiring every subclass to override both just to state the obvious.

1 // Worked example: the minimal real subclass this base class actually
2 // requires -- only three overrides, matching a genuinely read-only
3 // stream’s real capabilities (Write/WriteByte are correctly inherited as
4 // NotSupportedException-throwing, never called by a well-behaved reader).
5 class MemoryBlobStream final : public System::IO::Stream
6 {
7 public:
8 explicit MemoryBlobStream(std::vector<SharpRuntime::bytecs> data)
9 : data_(std::move(data)) {}
10
11 SharpRuntime::intcs Read(SharpRuntime::bytecs buffer[],
12 SharpRuntime::intcs offset, SharpRuntime::intcs count) override
13 {
14 if (buffer == nullptr && count != 0)
15 throw System::ArgumentNullException("buffer");
16 if (offset < 0)
17 throw System::ArgumentOutOfRangeException("offset");
18 if (count < 0)
19 throw System::ArgumentOutOfRangeException("count");
20
21 SharpRuntime::intcs n = std::min<SharpRuntime::intcs>(
22 count, static_cast<SharpRuntime::intcs>(data_.size()) - position_);
23 if (n != 0)
24 std::memcpy(buffer + offset, data_.data() + position_, n);
25 position_ += n;
26 return n;
27 }
28
29 void Close() override {}
30
31 [[nodiscard]] SharpRuntime::intcs getLengthProperty() const override
32 {
33 return static_cast<SharpRuntime::intcs>(data_.size());
34 }
35
36 private:
37 std::vector<SharpRuntime::bytecs> data_;
38 SharpRuntime::intcs position_ = 0;
39 };

56.5.1 MemoryStream: writable by default, read-only when requested

System::IO::MemoryStream is the concrete Stream subclass this ecosystem actually uses in practice — CNA’s own ContentManager reaches for it to wrap data loaded from sources like Android APK assets, where a real filesystem path is not available at all. Its default constructor creates an empty, writable buffer, matching real .NET. The buffer-copy constructor now makes the same default explicit while retaining a C++-only way to request a read-only copy:

1 MemoryStream(const bytecs* buffer, intcs size, bool writable = true);

The implementation validates before forming the vector range. A negative size is reported as ArgumentOutOfRangeException; a null pointer with non-zero size is reported as ArgumentNullException. The one deliberate source-language adaptation is nullptr, 0, accepted as an empty range because C++ can express an empty pointer range whereas .NET’s array overload simply rejects a null array. Passing false creates a readable, seekable, non-writable copy whose Write and WriteByte calls throw NotSupportedException; omitting it matches the writable .NET array constructor.

56.5.2 Closing a memory stream closes operations, not the bytes it already owns

The constructor policy above should not be confused with this class’s close/dispose behavior. MemoryStream::Close() changes only its isOpen_ state; it deliberately retains both the vector of bytes and the current position. After close, Read, Write, WriteByte, Seek, Length, and Position correctly reject use with ObjectDisposedException, while CanRead and CanSeek return false. A closed stream created as writable deliberately retains CanWrite == true: .NET’s own property reports the construction-time writable flag even though subsequent writes throw. In further deliberate contrast, ToArray() and GetBuffer() remain usable:

1 System::IO::MemoryStream stream;
2 const SharpRuntime::bytecs payload[] = {1, 2, 3, 4};
3 stream.Write(payload, 0, 4);
4 stream.Close();
5
6 std::vector<SharpRuntime::bytecs> saved = stream.ToArray(); // still {1,2,3,4}
7 // stream.getLengthProperty(); // throws ObjectDisposedException
8 // stream.Read(buffer, 0, 1); // throws ObjectDisposedException

That asymmetry is intentional, not a use-after-dispose loophole. Real .NET explicitly preserves the buffer in its own dispose implementation, MemoryStream::Dispose(bool), so post-disposal TryGetBuffer, GetBuffer, and ToArray can still retrieve data; the runtime’s ensureNotClosed() guard follows the same rule by guarding operations but not the two extraction methods. CNA callers can therefore finish producing an in-memory content blob, close the stream to catch accidental later I/O, and still hand a stable copy to a consumer. The distinction between the methods matters: ToArray() returns an independent vector, safe across later stream writes, whereas GetBuffer() is a reference to the live vector and may be invalidated by a reallocation.

The same implementation also has two less obvious safety properties whose tests prevent a “memory only” stream from becoming an unchecked byte container. Null buffers produce ArgumentNullException; negative offsets or counts produce ArgumentOutOfRangeException. Neither case is mistaken for an EOF return of zero. And a caller may legally set Position past the current end, but a later write computes position + count in int64_t before resizing. If that sum exceeds the runtime’s signed intcs range it throws IO::IOException (“Stream was too long.”) instead of wrapping negative and writing beyond the vector. This was not defensive theorizing: the former narrow-integer calculation had the same signed-overflow shape already reproduced under UBSan in a related slicing path, and StreamTests.cpp now exercises Position = 2147483647 followed by a ten-byte write specifically to require the exception rather than an allocation or memory corruption.

56.6 UTF8Encoding: conformance validation and fallback evidence

Text::UTF8Encoding’s own class comment states a real, specific guarantee that is easy to assume any UTF-8 encoder provides and that this port genuinely earns rather than skips: GetBytes() / GetString() validate that input is well-formed UTF-8 — not merely pass bytes through as opaque char values — and route any ill-formed byte at each invalid position through the configured fallback before resuming. Reading UTF8Encoding.cpp’s own local wellFormedUtf8Length() helper directly shows this is real conformance checking, not a length-only byte count: a leading byte is walked against the real UTF-8 grammar (1/2/3/4-byte forms, each continuation byte checked for the 10xxxxxx pattern), and even a structurally well-formed sequence is rejected if the code point it decodes to is an overlong encoding (a multi-byte sequence encoding a code point that a shorter form could already represent — a real, historically security-relevant UTF-8 attack shape, since a naive decoder that accepts overlong encodings lets an attacker smuggle a byte value like 0x2F (’/’) past a filter checking only the raw byte stream) or a UTF-16 surrogate code point (U+D800U+DFFF, which UTF-8 must never directly encode, since surrogates exist only as a UTF-16 encoding artifact).

1 // From wellFormedUtf8Length() -- the 3-byte case, real conformance logic,
2 // not just "are there two continuation bytes after this lead byte":
3 if ((c0 & 0xF0) == 0xE0 && /* two real continuation bytes follow */) {
4 uint32_t cp = /* decode the 16-bit code point from all three bytes */;
5 return (cp >= 0x800 && !(cp >= 0xD800 && cp <= 0xDFFF)) ? 3 : 0;
6 // ^ rejects overlong (cp < 0x800 has a shorter valid encoding)
7 // ^ rejects UTF-16 surrogates directly encoded in UTF-8
8 }

The constructor also earns a real, specific correction over the naive default: real .NET’s own UTF8Encoding.SetDefaultFallbacks() substitutes the Unicode replacement character U+FFFD for invalid input, not the generic Encoding base class’s plain "?" default (correct only for single-byte code pages, where every valid character already has an unambiguous byte representation to fall back to). This port’s constructor reproduces that distinction precisely:

1 UTF8Encoding::UTF8Encoding() {
2 // Real UTF8Encoding.SetDefaultFallbacks() uses a U+FFFD replacement
3 // fallback, not the generic Encoding base class’s "?" default.
4 setEncoderFallbackProperty(
5 std::make_shared<EncoderReplacementFallback>("\xEF\xBF\xBD"));
6 setDecoderFallbackProperty(
7 std::make_shared<DecoderReplacementFallback>("\xEF\xBF\xBD"));
8 }

A worked example makes the whole mechanism concrete — three real byte sequences, one well-formed and two deliberately malformed in different ways, through the same GetString() call:

1 System::Text::UTF8Encoding utf8;
2
3 // Well-formed: a real 3-byte sequence for U+20AC (EURO SIGN).
4 const SharpRuntime::bytecs euroSign[] = {0xE2, 0x82, 0xAC};
5 std::string ok = utf8.GetString(euroSign, 0, 3); // EURO SIGN, unchanged
6
7 // Overlong encoding of U+002F (’/’) using the 2-byte form -- structurally
8 // valid continuation-byte shape, but wellFormedUtf8Length() rejects it
9 // because cp (0x2F) is below the 2-byte form’s own 0x80 minimum.
10 const SharpRuntime::bytecs overlongSlash[] = {0xC0, 0xAF};
11 std::string rejected = utf8.GetString(overlongSlash, 0, 2); // two U+FFFD
12
13 // A lone continuation byte with no lead byte before it -- fails every one
14 // of wellFormedUtf8Length()’s four branches, falls through to the final
15 // "return 0" unconditionally.
16 const SharpRuntime::bytecs loneContinuation[] = {0x80};
17 std::string alsoRejected = utf8.GetString(loneContinuation, 0, 1); // one U+FFFD

The overlong and lone-continuation cases each substitute one U+FFFD per invalid byte, not per attempted sequence — overlongSlash’s two bytes both fail validation independently (the loop advances one byte at a time through the fallback path, per UTF8Encoding.cpp’s own ++i in the non-well-formed branch), so a 2-byte overlong sequence becomes two replacement characters, not one. This matches real .NET’s own observable behavior for the identical input, confirmed by working through the .NET runtime’s own documented per-byte fallback-and-resynchronize algorithm for UTF-8 decoding rather than assumed by analogy.

This behavior now has direct regression evidence. UTF8EncodingTests covers malformed continuations, overlong input, truncated sequences, directly encoded surrogates and exception fallbacks, including the one-replacement-per-byte resynchronization used above. The separate Utf8Tests family validates overlong sequences, surrogates, code points above U+10FFFF, truncation and lone continuation bytes through the span-oriented validator. The source read explains the algorithm; these two distinct test surfaces pin both the encoding facade’s observable substitutions and the lower-level validity classification.

56.7 Generic continuations exist; generic combinators do not

Both non-generic Task and generic-result TaskT<TResult> have real ContinueWith implementations. Their shared-state shapes each contain a mutex, condition variable, and callback list; registered callbacks execute inline on the thread that completes the antecedent, or synchronously on the caller when it is already complete. TaskT<TResult> provides both an action overload returning Task and a result-producing overload returning TaskT<TNewResult>. The tests cover success, fault, cancellation inspection, filtered options, result chaining, empty callables, and release of captured state. Both implementations capture the antecedent state weakly to avoid a cycle from that state’s own continuation list back to itself.

The narrower missing surface is aggregation over generic tasks. Static WhenAll and WhenAny accept only std::vector<Task>; there is no corresponding overload for std::vector<TaskT<TResult>> that produces a result array or a winning generic task. Thus callers can attach a continuation to one generic result, but cannot express .NET’s generic multi-task combinators through this API. A second minor boundary is that the lightweight antecedent reconstructed for a callback shares terminal status/result/exception state but not the original separately-held cancellation-token object, so its token property reports None.

56.8 Reading a dependency from source

A reliable dependency review uses three layers in order. First derive the public include from the type’s fully-qualified name. Then locate the module whose public include directory owns the file. Finally inspect that component’s declared public dependencies rather than inferring a closure from nested namespaces. Namespace depth, directory depth, and link depth often align, but the component registry — particularly compatibility umbrellas and the Xml.XPath alias — is the authority when they do not.

The representative types in this chapter also show why include availability is weaker than semantic parity. UTF8Encoding’s malformed-input behavior is implemented and directly tested; TaskT<TResult> has generic continuations but not generic multi-task combinators. A mechanically correct path proves that a type can be named. It does not prove that every .NET behavior behind that name exists.

Type at least three characters. Results are ranked by how often and where the words occur.