runtime-internals · git:20261027.bf64f38 · 2026-10-27 · sha256 6f2e10e927986c3d
runtime-internals git:20261027.bf64f38A
Immutable. This exact content never changes and is served at /api/v1/blob/6f2e10e927986c3d.
--- name: runtime-internals description: "How Nino reads and builds a runtime's own objects through their fields instead of the public API - CollectionViews (List, HashSet, Dictionary layouts per runtime), mirror classes and ObjectTypes.Retype, ImmutableDictionary/ImmutableHashSet trees (HashTrees), SortedSet red-black trees, LinkedList rings, ConcurrentDictionary buckets - with startup checks, public-API fallbacks and the FastPaths tests. Use when adding or changing such a path, when a collection reads or writes wrong on Mono, IL2CPP, .NET Framework or 32-bit, or when a startup check fails. Triggers: CollectionViews, HashTrees, SortedSetNodes, ReadLinked, ConcurrentTables, Retype, mirror class, Unsafe.As, FastPaths, tests/Nino.RuntimeTests, Mono abort, 运行时内部, 镜像类, 字段布局." metadata: internal: true --- # Runtime internals ## Reading a runtime's collections through their fields A path through a runtime's internals (`CollectionViews`: the fields of `List<T>`, `HashSet<T>`, `Dictionary<TKey, TValue>`, ...) declares a layout for each implementation (.NET 64-bit and 32-bit, .NET Framework, Mono), checks it against an instance with known contents at startup and uses the public API when none matches. Keep the fallback (the enumerator) out of line. Run the tests on .NET, the `net472` build under Mono and the `linux-arm` and `linux-arm64` builds under qemu (`platforms`). ## Building a runtime's objects without its public API `Nino.Immutable/HashTrees.cs` (an `ImmutableDictionary` or `ImmutableHashSet` built as its tree, instead of one builder `Add` at a time); `SortedSetNodes` in Nino.Core (a `SortedSet<T>`'s red-black tree); `ReadLinked` (a `LinkedList<T>`'s ring of nodes); `ConcurrentTables.Builder` (a `ConcurrentDictionary`'s bucket chains). One way for every runtime where one works, and a separate way per runtime (behind `#if` or a startup check) only where none does. What works everywhere so far: - **Mirror classes** that declare the fields of the real class in its order, read and written through `Unsafe.As`: every runtime lays out two classes with the same field list the same way. Check the field lists of every version of the library users may have (System.Collections.Immutable 1.5 to 10.0 have the same fields), and check the layout at startup against an instance with known contents, through the public API; use the public API when it fails. - **Never call an instance method on a mirrored object** (a `MemberwiseClone` helper in the mirror class): Mono and IL2CPP take a shared generic method's context from `this`'s class, and Mono aborts (an assertion, not an exception, so no `catch` helps). Static code only. - **Allocate a mirror instance with `new` and set its type word** to the real type's (`ObjectTypes.Retype`): the same fields mean the same size and the same references for the GC. Faster than `RuntimeHelpers.GetUninitializedObject` (13 ns on .NET 10 but 77 on .NET 8, FormatterServices 59 on Mono) and needs nothing IL2CPP may lack. The type word sits where the reference points (CoreCLR: method table, one header word; Mono and IL2CPP: vtable or class, then the monitor, two words), found at startup; call `GetType()` on a retyped object out of line (`NoInlining`), or the JIT folds it to the type it allocated (Arm32 did). - **Hash tables filled this way still count collisions** (`audit-security`: `HashTables`, `ConcurrentTables`). ## Proof - Run it on .NET 8 and 10, the `net472` build under Mono (also `MONO_GC_DEBUG=verify-before-collections`), Arm32 under qemu, with a GC stress (forced compacting collections between builds, then every object checked). - CI's "Runtime internals" job runs `tests/Nino.RuntimeTests` on .NET (x64, Arm64, Arm32 under qemu, Windows x86), Mono (x64, Arm64) and .NET Framework 4.8 (64-bit and 32-bit): give a new path a case there with known contents, and its startup flag a row in its `FastPaths`, so a runtime that falls back to the public API fails. Each runtime must write .NET x64's bytes; locally, diff the outputs (CONTRIBUTING.md).