Releases: DevAM-Tools/ZeroAlloc
Releases · DevAM-Tools/ZeroAlloc
Release list
0.4.0
- **HIGH — TempStringBuilder/TempBytesBuilder heap-fallback grow corrupted ThreadStatic
buffer** (H01): When a second `TempStringBuilder` or `TempBytesBuilder` was created
while one was already active on the same thread (heap-fallback mode,
`_IsThreadStatic = false`), any buffer growth in the inner builder incorrectly called
`ZeroAllocHelper.GrowCharBuffer` / `GrowByteBuffer`. Those helpers replace the
ThreadStatic buffer shared with the outer builder, silently invalidating its internal
`_Array`/`_Span` reference and producing corrupt content or crashes in the outer
builder after the inner builder grew. Fix: `EnsureCapacity`, `TryEnsureCapacity`, and
the `AppendFormat` growth loops in both `TempStringBuilder` and `TempBytesBuilder` now
branch on `_IsThreadStatic`. The heap-fallback path allocates a new private array,
copies existing content, and reassigns `_Array`/`_Span` without touching the
ThreadStatic field, making nested-builder grow fully isolated.
- **Generator: missing blank lines in generated `TryParse` code** (BinaryParsableGenerator):
`AppendLine()` calls were absent after the bounds-check block and after each
`LengthBytes` case arm in all three variable-length string decode paths
(`Parse`/`TryParse` for UTF-8, UTF-16, and ASCII variants). The missing line breaks
caused consecutive statements to run together without blank-line separation in the
emitted source, degrading generated-code readability. Fix: `AppendLine()` inserted
after every affected block boundary in `BinaryParsableGenerator`.
- **Package metadata update**: `Authors`, `Company`, and `Copyright` in
`Directory.Build.props` updated from the placeholder "ZeroAlloc Contributors" to the
canonical "DevAM" identity and copyright year.
- **`ContinuousIntegrationBuild` for Release**: Added
`<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>` under a
`'$(Configuration)' == 'Release'` condition in `Directory.Build.props`. This enables
deterministic builds and source-link embedding for NuGet packages produced in Release
configuration.
- **Roslyn dependency downgrade — 5.3.0-preview → 4.14.0**: `Microsoft.CodeAnalysis.Analyzers`
and `Microsoft.CodeAnalysis.CSharp` downgraded from the unstable pre-release
`5.3.0-2.25625.1` / `5.3.0` to the stable `4.14.0` in `Directory.Packages.props`,
restoring build reproducibility on standard SDK installations.
- **Test runner configuration**: Added `"test": { "runner": "Microsoft.Testing.Platform" }`
to `global.json`, aligning the dotnet test host with the TUnit / Microsoft.Testing.Platform
runner used by the test suite.
- **Regression tests for nested-builder grow** (`BuilderTests.cs`): Two new tests cover
the H01 fix end-to-end.
`TempStringBuilder_NestedBuilder_GrowsBeyondDefaultBuffer_InnerContentCorrect` creates
a nested `TempStringBuilder` in heap-fallback mode, appends 3 MiB of content to force
a grow past the 2 MiB default, and asserts that both inner content and outer content
remain correct. The symmetric `TempBytesBuilder_NestedBuilder_GrowsBeyondDefaultBuffer_InnerContentCorrect`
test applies the same scenario to `TempBytesBuilder`.
- **`.gitignore` casing fix**: Entry for the local commit-message file corrected from
`CommitMessage.md` to `commit_message.md` to match the actual filename.
0.3.0
- **BitReader: `Try*` read API**: Added a complete non-throwing counterpart for every read and skip operation: `TryReadBits`, `TryReadBit1`–`TryReadBit6`, `TryReadNibble`, `TryReadByte`, `TryReadUInt16`–`TryReadUInt128`, `TryReadInt8`–`TryReadInt128`, `TryReadBytes`, and `TrySkipBits`. Each returns `false` without advancing the reader position when insufficient bits remain, enabling buffer parsing without exception overhead in incremental or streaming scenarios. - **BitWriter: `Try*` write API**: Symmetric non-throwing counterparts for every write and skip operation: `TryWriteBits`, `TryWriteByte`, `TryWriteUInt16`–`TryWriteUInt64`, `TryWriteInt16`–`TryWriteInt64`, `TrySkipBits`, and `TryWriteBytes`. Each returns `false` without modifying the buffer when capacity is insufficient. - **BitReader/BitWriter: 128-bit integer support**: Added `ReadUInt128`, `ReadInt128`, `WriteUInt128`, and `WriteInt128`. All four methods use big-endian bit order (upper 64 bits first), composing two 64-bit `ReadBits`/`WriteBits` calls. - **HIGH — StringWrappers null-path partial-write** (H01): All 15 fixed-length encoding wrapper types (`Utf8FixBE`, `Utf16LEFixLE`, `AsciiFixBE`, `Latin1FixLE`, and their variants) wrote a zero-length prefix into the destination span before verifying that the span was large enough to hold it. On an undersized destination this produced a partial, corrupt write before returning `false` — violating the `TryFormat` contract that nothing is written on failure. Fix: three `EncodingHelper` helpers (`TryWriteZeroLengthPrefix32BE`, `TryWriteZeroLengthPrefix32LE`, `TryWriteZeroLengthPrefix16LE`) perform the capacity check first; every affected null branch now delegates to one of them so the operation is truly atomic. - **HIGH — SpanBytesBuilder.Advance() silent position corruption** (H02): `Advance(int count)` accepted negative values and counts beyond `Remaining`, silently corrupting internal position state without any diagnostic. Fix: unsigned-comparison pattern `(uint)count > (uint)Remaining` catches both negative and over-limit values in a single branch and throws `ArgumentOutOfRangeException` with a descriptive message identifying the requested advance and the actual remaining capacity. - **HIGH — SpanBytesBuilder throwing Append* relied on late BCL exceptions** (H03): All throwing `Append*` methods detected overflow only via downstream BCL exceptions (`ArgumentException` from `Span.CopyTo`, `IndexOutOfRangeException` from index access, `ArgumentOutOfRangeException` from `BinaryPrimitives`), with some paths producing partial writes before failing. Fix: all variants route through their `TryAppend*` counterparts; on `false` a shared `_ThrowBufferTooSmall` helper (marked `[MethodImpl(NoInlining)]` to keep the hot path lean) throws `InvalidOperationException` with the operation name. Covers `Append(byte)`, `Append(ReadOnlySpan<byte>)`, `Append(byte[]?)`, all 16 integer BE/LE variants, all 6 float BE/LE variants, `AppendVarInt`, `AppendHex2/4/8/16`, and `AppendBinary8/16/32/64`. - **HIGH — SpanBytesBuilder compound UTF-8 partial state** (H04): `AppendUtf8WithVarIntPrefix`, `AppendUtf8WithLengthPrefixBE`, and `AppendUtf8WithLengthPrefixLE` wrote a length prefix (or partial content) before detecting insufficient space, leaving the builder position advanced past those bytes with no clean recovery. Fix: all compound UTF-8 operations calculate the total required byte count up front; if `Remaining < requiredBytes` the method fails atomically without advancing the position. `AppendUtf8` and `AppendUtf8NullTerminated` (throwing variants) likewise route through their `Try*` counterparts. - **HIGH — BitReader missing bounds checks** (H05): `ReadBit1()`, `ReadByte()`, `SkipBits()`, and `ReadBytes()` did not validate remaining capacity before accessing the buffer, producing context-free `IndexOutOfRangeException` on overflow. Fix: all four methods now throw `InvalidOperationException` on overflow, or `ArgumentOutOfRangeException` for negative arguments, with messages stating exactly how many bits/bytes were requested vs. available. - **HIGH — BitWriter missing bounds checks** (H06): Same structural gap as BitReader. `WriteBits()`, `WriteByte()`, `SkipBits()`, and `WriteBytes()` lacked capacity validation. Fix: identical treatment — `InvalidOperationException` on overflow, `ArgumentOutOfRangeException` for negative counts. - **MEDIUM — SpanStringBuilder throwing Append* relied on late BCL exceptions** (M01): Identical structural problem to SpanBytesBuilder. Fix: `Append(string?)`, `Append(ReadOnlySpan<char>)`, `Append(char)`, `Append(bool)`, all `AppendHex*`, and all `AppendBinary*` methods route through their `TryAppend*` counterparts and throw `InvalidOperationException` on overflow. - **MEDIUM — SpanStringBuilder.AppendLine partial advance** (M02): `AppendLine(string?)` and `AppendLine(ReadOnlySpan<char>)` could write the value content and then fail on the trailing newline, leaving the builder advanced by the value length while producing unterminated output. Fix: both overloads pre-check `Remaining < value.Length + NewLine.Length` before touching the buffer and fail atomically. - **Exception contract clarification**: All overflow and capacity failures in both builder types now surface consistently as `InvalidOperationException`. The previous mix of `ArgumentException` (from `Span.CopyTo`), `IndexOutOfRangeException` (from span index access), and incidental `ArgumentOutOfRangeException` (from `BinaryPrimitives`) is gone. `ArgumentOutOfRangeException` is reserved exclusively for `Advance(count)`, where `count` itself is the out-of-range argument. - **SpanBytesBuilder: `_GetVarIntByteCount` extraction**: The 7-branch variable-integer byte-count calculation was duplicated between `AppendVarInt` and `TryAppendVarInt`. Extracted to a shared private `_GetVarIntByteCount(ulong)` method. - **BitReader/BitWriter: `ReadBitsCore`/`WriteBitsCore` extraction**: Hot-path bit-manipulation logic factored into private `[AggressiveInlining]` methods shared between throwing and non-throwing call sites, eliminating duplication while maintaining inlining for the fast path. - **Generator: `ParsableMemberKind.Float`/`Double` removed**: The two dedicated enum members were redundant; float and double members are handled uniformly via `PrimitiveInteger`. Removing them eliminates a dead code path and reduces the risk of divergence between generator branches. - **Generator: `Parse()` exception type corrected**: Documentation (and generated code comments) previously stated `Parse()` throws `FormatException` on failure; the implementation throws `InvalidOperationException`. Doc updated to match. - **Generator: code generation quality improvements**: File header and namespace declarations in `BinaryParsableGenerator` and `BinaryWritableGenerator` now use raw string literals for cleaner, indent-preserving output. Copyright header in generated files updated to match the repository standard. - **NuGet packaging fix — generator DLL inclusion**: The `Condition`-based `<None Include=...>` approach silently omitted the generator DLL when building in a configuration where the output path did not yet exist. Replaced with an MSBuild `Target` (`_IncludeGeneratorInPackage`) that resolves the generator's actual output path at pack time via `GetTargetPath`, guaranteeing correct inclusion regardless of build order. - **Test framework migration — xunit.v3 → TUnit 1.45.8**: All test classes and methods migrated from xunit.v3 (`[Fact]`, `Assert.Equal`) to TUnit (`[Test]`, `await Assert.That(...).IsEqualTo(...)`). All test classes changed to `sealed`. `Microsoft.NET.Test.Sdk`, `xunit.v3`, and `xunit.runner.visualstudio` removed from `Directory.Packages.props`; replaced with `TUnit 1.45.8`. Ref-struct workarounds (snapshotting `Length` before `await`) applied where CS4007 applies. - **Copyright header standardization**: Full 26-line MIT license block replaced with the single-line short form from the new `COPYRIGHT` file across all 60+ source files. `COPYRIGHT` file added to the repository root. - **SDK version pinning**: `global.json` added, pinning the build to .NET SDK 10.0.100 with `rollForward: latestMinor`. - **ZeroAllocBaseTests** (new test file, 433 lines): Covers `ZeroAllocBase` fallback-method contracts (`String`, `TryString`, `Utf8`, `TryUtf8`, `Bytes`, `TryBytes`) and all attribute types defined in `Attributes.cs`. - **BitReader/BitWriter tests expanded**: Full TUnit coverage for all new `Try*` methods, 128-bit read/write, bounds-check error paths, and `SkipBits` validation in both `BitReaderTests.cs` and `BitWriterTests.cs`. - **SpanBuilder/FormattedWrapper regression tests**: Coverage for all buffer-safety fixes. `SpanBuilderTests.cs`: four tests renamed to reflect the new exception contract; three new regression tests added (Advance past remaining, compound UTF-8 partial state, AppendLine partial advance). `FormattedWrapperTests.cs`: two new tests for the fixed-length wrapper null-path bug, plus a parameterised theory covering all 15 wrapper types. `NegativeAndEdgeCaseTests.cs`: two assertions updated from `IndexOutOfRangeException`/`ArgumentException` to `InvalidOperationException`. - **Total tests**: 893 (up from 647 in 0.2.0)
0.2.0
- **LazyString**: Deferred string evaluation that captures format state and only materializes when needed. Use `ZA.Lazy()` for simple deferred values and `ZA.LazyInterpolated()` for interpolated strings. Implements `ISpanFormattable` and `IUtf8SpanFormattable` for zero-allocation formatting when consumed by TempString/TempBytes builders. Includes `IDeferredFormat<TState>` interface and `DeferredFormat<TState>` base class for custom deferred formatting logic. Generator support for `ZA.Lazy()` and `ZA.LazyInterpolated()` added to `ZeroAllocGenerator`. - **BinaryWritable Generator** (`[BinaryWritable]`): New source generator that produces `IBinarySerializable` implementations for structs, enabling zero- allocation binary serialization. This is the write counterpart to the existing `[BinaryParsable]` generator. Supports all the same member types: endian wrappers, bit fields, VarInt, nested serializable structs, fixed/dynamic byte arrays, and string encodings. Diagnostics ZA3001–ZA3013 for write-side validation. - **ZeroAllocBase**: New base class for ZeroAlloc helper types, providing a foundation for `ZA` to inherit from while enabling generator detection of types that use ZeroAlloc APIs internally. - **Central Package Management**: Introduced `Directory.Build.props` and `Directory.Packages.props` for centralized build configuration and NuGet package version management across all projects. - **CRITICAL — BitReader.RemainingBytes operator precedence** (C01): Fixed `_Buffer.Length << 3 - _BitOffset` to `(_Buffer.Length << 3) - _BitOffset`. The subtraction operator has lower precedence than the left-shift operator, causing the shift amount to be computed as `3 - _BitOffset` instead of shifting first and then subtracting. This produced incorrect remaining-byte counts and could cause downstream index errors. - **HIGH — BitReader.ReadBits missing bounds check** (H01): Added explicit validation that `_BitOffset + bitCount <= totalBits` before buffer access. Previously, reading past the end of the buffer produced a generic `IndexOutOfRangeException`; now throws a descriptive `InvalidOperationException` stating how many bits remain. - **HIGH — Utf8Formatted\<T\>.ToString() heap allocation** (H02): Replaced the disproportionate 256B → 4KB → 65KB fallback chain with 256B stackalloc → 4KB `ArrayPool<byte>.Shared.Rent()` with try/finally → `_Value.ToString()` fallback. Similarly updated `Formatted<T>.ToString()` to use `ArrayPool<char>` for the 4KB path. This eliminates unnecessary large heap allocations while maintaining correctness for values that exceed the stack buffer. - **HIGH — Infinite loop risk in AppendFormattable** (H03): Added growth-loop safety guards in `TempStringBuilder` and `TempBytesBuilder`. All `AppendFormattable`, `TryAppendFormattable`, `Append<IBinarySerializable>`, and `AppendUtf8Formattable` loops now detect when the buffer fails to grow (capacity unchanged after `GrowBuffer`), preventing infinite loops from buggy `ISpanFormattable` implementations. - **MEDIUM — UTF32Encoding instance duplication** (M01): Eliminated 4 identical `new UTF32Encoding(bigEndian: true, byteOrderMark: false)` instances in `StringWrappers.cs`. Introduced a shared `Utf32BEEncoding` helper class with a single static instance referenced by `Utf32BE`, `Utf32BEVar`, `Utf32BEFixBE`, and `Utf32BEFixLE`. - **MEDIUM — Integer overflow in CalculateGrowth** (M02): Fixed potential integer overflow in `ZeroAllocHelper.CalculateGrowth` when buffer sizes exceed ~1.72 GiB. Growth calculation now uses `long` arithmetic before clamping to `int.MaxValue`. - **MEDIUM — Missing generator validation** (M03): Added diagnostics ZA2014/ZA2015 (BinaryParsableGenerator) and ZA3012/ZA3013 (BinaryWritableGenerator) for: - `[BinaryFixedLength]` or string encoding with length ≤ 0 - Multiple conflicting length/encoding attributes on the same member - **Generator code deduplication**: Extracted 7 shared helper methods (`IsEndianWrapper`, `IsPrimitiveInteger`, `GetEndianWrapperSize`, `GetPrimitiveSize`, `CalculateFixedSize`, `CalculateFixedBits`, `GetDefaultBitCount`) into `BinaryGeneratorHelpers.cs`, reducing maintenance risk when new wrapper types are added. - **BitWriter relocation**: Moved `BitWriter.cs` from `Parsing/` to `src/` root, since BitWriter performs serialization (writing), not parsing. - **Duplicated inheritdoc cleanup**: Removed ~50 occurrences of duplicated `/// <inheritdoc/>` tags in `StringWrappers.cs` (~3800 characters of noise). - **Global usings consolidation**: Moved all common `using` directives into `GlobalUsings.cs` files for each project, reducing clutter in individual files. - **Code style enforcement**: Added `.editorconfig` and applied `dotnet format` across the codebase for consistent formatting. - **README documentation**: Updated with LazyString usage examples, `ToString()` preference guidance, and implicit conversion documentation. - **FormattedWrappers tests** (14 new tests): Covers `Formatted<T>` and `Utf8Formatted<T>` including format string pass-through, culture handling, TryFormat buffer-too-small, Create factory methods, and ZA API integration. - **VarInt boundary tests** (extended): Added parametrized test cases for 3-byte (2,097,151/2,097,152), 4-byte (268,435,455/268,435,456), and 5-byte (34,359,738,367) boundaries. Extended `EncodedSize` assertions through 5-byte and `ulong.MaxValue` (10-byte). Added roundtrip test covering all boundary values up to `ulong.MaxValue`. - **VarIntZigZag boundary tests** (extended): Added symmetrical boundary tests for 2-byte (8,191/-8,192), 3-byte (1,048,575/-1,048,576), 4-byte boundaries, and `long.MaxValue`/`long.MinValue`. Extended roundtrip test with all boundary values. - **BitWriter roundtrip tests**: Added Write→Read roundtrip tests using BitReader for mixed bit fields, aligned integers, and signed integers. - **Total tests**: 647 (up from 626 in v0.1.3)
0.1.3
0.1.3
Add odd-length HexWrappers & improve recursive detection
Features
- Hex Wrappers: Add
Hex1,Hex3,Hex5,Hex6,Hex7for odd bit-length valuesHex1: 4-bit nibble (0-F)Hex3: 12-bit values (000-FFF)Hex5: 20-bit values (00000-FFFFF)Hex6: 24-bit values like RGB colors (000000-FFFFFF)Hex7: 28-bit values (0000000-FFFFFFF)
Improvements
- Generator: Improve
MayUseZeroAllocInternallydetection- Analyze TryFormat method body for actual ZeroAlloc API calls
- Check for ZeroAllocBase inheritance instead of broad assembly reference check
- Reduces false positive ZA1002 warnings for types that don't use ZeroAlloc
Internal
- Cleanup: Use
Encoding.UTF8instead ofSystem.Text.Encoding.UTF8in FormattedWrappers - Version bump to 0.1.3 for both ZeroAlloc and ZeroAlloc.Generator
0.1.2
0.1.2
Fix nuget package issue and make ZeroAlloc containing the generator
0.1.1
0.1.0
Summary
This is the first public release of ZeroAlloc, a zero-allocation serialization library for .NET 10+ that uses Roslyn source generators to create optimized, type-specific formatting and parsing code at compile time.
✨ Features
Core Library (ZeroAlloc)
- Zero-allocation string formatting via
ZA.String()with compile-time generated code - Zero-allocation UTF-8 generation via
ZA.Utf8() - Culture-sensitive variants via
ZA.LocalizedString()andZA.LocalizedUtf8() - Zero-allocation binary serialization via
ZA.Bytes() - Generated binary parsing via
ZA.ParseBytes<T>()and[BinaryParsable]attribute - Manual builders for advanced scenarios:
TempStringBuilder/SpanStringBuilderfor string buildingTempBytesBuilder/SpanBytesBuilderfor byte building
- Endian-aware type wrappers:
U16BE,U32LE,I64BE,F32LE, etc. - VarInt encoding:
VarInt7,VarInt14,VarInt21,VarInt28 - String encodings:
Utf8,Ascii, length-prefixed variants (Utf8L8,Utf8L16BE, etc.) - Hex formatting wrappers:
HexU8,HexU16,HexU32,HexU64,HexBytes - Bit-level parsing via
BitReaderfor sub-byte field extraction - Configurable buffer sizes (default 2 MiB per thread, compile-time or runtime adjustable)
Source Generator (ZeroAlloc.Generator)
- Roslyn incremental source generator targeting
netstandard2.0 - Generates optimized
ZA.String(),ZA.Utf8(),ZA.Bytes()overloads at compile time - Generates
TryParse()/Parse()methods for[BinaryParsable]structs - 17 analyzer diagnostics (ZA0001–ZA2013) for compile-time validation:
- Class structure checks (partial, internal)
- Argument type validation
- Binary parsing attribute consistency
- Length field ordering and alignment checks
📦 Project Structure
| Project | Description |
|---|---|
ZeroAlloc |
Core library (net10.0) |
ZeroAlloc.Generator |
Roslyn source generator (netstandard2.0) |
ZeroAlloc.Tests |
xUnit test suite with >50 test classes |
ZeroAlloc.Benchmarks |
BenchmarkDotNet performance tests |
ZeroAlloc.Demo |
Demo/example application |
🔧 Build & Packaging
- Solution file:
ZeroAlloc.slnx - VS Code tasks for build, test, pack, clean
- NuGet packages auto-generated on Release build:
ZeroAlloc.0.1.0.nupkg+.snupkgZeroAlloc.Generator.0.1.0.nupkg+.snupkg
- Deterministic builds with source-link support
- MIT License
📝 Files Changed
- 59 files changed, ~29,800 insertions
- Complete library implementation
- Comprehensive test coverage
- Full documentation in README.md
- VS Code workspace configuration
- NuGet packaging configuration
🎯 Target Scenarios
- Network protocol serialization (CAN, FlexRay, custom binary protocols)
- High-performance logging without allocations
- Embedded systems with memory constraints
- Game development with GC-sensitive code paths
- Any scenario requiring predictable, allocation-free serialization