C# tables

September 8, 2026 ยท View on GitHub

Generate with schema generate --lang cs --out generated <schema-directory>. The generated unit targets the pinned .NET SDK in .github/dotnet-version and uses unsafe for native rows, regions, cooks and builders. The packet surface continues to use the same generated files.

Compile each generated *Table.cs together with its unit's *Region.cs files. The region runtime owns the shared announcement and vocabulary definitions used by managed tables as well as native storage.

Choosing storage

Generated classes are convenient managed authoring values. They carry variable graphs, shared pointers, blobs, lists and maps, and support file, message, JSON and cook operations. Their variable read path allocates managed values.

Generated <Table>Row structs describe native storage. File and message LoadMeasure overloads size a caller-owned region; native loads fill it, and native measure/save walk it directly. Data and node attribution can be supplied separately. Preserve attribution for retain-unknown; it is unnecessary for an ordinary relocated region's measure/save. A negative measure is a refusal and must be checked before allocating or loading. LoadVerdict and TableReport distinguish damage, evolution events and named refusals.

Message sizing reports expanded storage without allocating it, including known constant list elements that occupy zero bits on the wire. Check the measured size against the caller's capacity budget; native load refuses insufficient capacity before filling the region. Managed authoring reads allocate their values.

Native offsets are relative to their slots. Move the entire data region as a unit, then reacquire its root and derived pointers. A loaded collection's spare word is padding, never authoring capacity. CopyFrom imports a native region into a mutable builder and reconstructs its collection storage.

Building and packing

Each generated <Table>Builder owns its arena. Dispose it when finished. GetRoot() returns its mutable native root. Alloc<T>() creates generated row storage with declared defaults; AllocBytes and AllocString create blob storage. Set pointer slots with TableArena.SetReference and follow them with TableArena.At<T>. Check allocation results before dereferencing.

CreateWorker() supplies an exclusive worker for one filling thread. Workers carve ordinary nodes from stable slabs; large nodes use separate spans. Finish all workers before saving, cooking, locking or disposing. List and map handles come from the worker and the generated field descriptor. Capacity is distinct from live count, and collection growth preserves existing element addresses.

Measure/Save and CookMeasure/Cook accept the mutable graph. Lock() measures and packs reachable storage, verifies that packing consumes each measured extent, then releases the slabs. It preserves sharing and refuses cycles. After successful locking, GetRoot() returns null, workers stop allocating and AsConst()/Region expose the packed root. Locking is one-way and idempotent. DataBytes and AttributionBytes describe the two packed parts. A failed lock leaves the builder available for correction or disposal.

Load on a builder fills its mutable graph directly. Check the returned bool; a failed builder read requires discarding the partial value. In particular, exceeding a list or map's int32 storage cap adds no report event of its own.

TableAllocator accepts a matched Allocate/Free function-pointer pair and Context. Custom allocation must return zeroed, at least 16-aligned storage, or null on failure. The default uses NativeMemory. The pair owns native arena and packing allocations and native numbering scratch; it does not replace the managed authoring classes' allocations. The generated block builder has its own paired allocator contract.

Wire and validation

The surface includes form-1 files, announcements and bitpacked form-2 message batches, both cook byte orders, block build/open, JSON, reflection and UnitView. All field shapes use the same descriptors: optional and keyed arrays, general union arms, wide scalars, bounded UTF-16 strings, maps and unbounded lists. *wstring remains outside the compiler's current table vocabulary; the native UTF-16 allocation helper does not make it a supported wire pointer type.

Retain-unknown uses caller-owned byte and ID stores plus node attribution. Load reports capacity losses; save reports retained records whose paths or identities can no longer be placed. Retaining a fixed root and saving retained state as a message are refused explicitly. Save each retained message region as a file.

make test-cs runs the integration, listing, differential fuzz, allocation, negative-control and cook-open gates. The managed/native file and builder arms use independent oracle paths; --builder checks builder completion without a region preflight. test/cs-tables/src/BuilderChecks.cs, RegionChecks.cs and RetainChecks.cs are executable ownership and lifetime examples. The shared conformance runner compares canonical wire, JSON, messages and cooks with the reference corpus. Benchmark output from a shared interactive machine is a pairing check, not a release performance claim.