Spangle.LusterBits 1.2.0

dotnet add package Spangle.LusterBits --version 1.2.0
                    
NuGet\Install-Package Spangle.LusterBits -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Spangle.LusterBits" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Spangle.LusterBits" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Spangle.LusterBits" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Spangle.LusterBits --version 1.2.0
                    
#r "nuget: Spangle.LusterBits, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Spangle.LusterBits@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Spangle.LusterBits&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Spangle.LusterBits&version=1.2.0
                    
Install as a Cake Tool

Spangle.LusterBits

日本語版: README.ja.md

Spangle.LusterBits is a C# source generator that turns structs with fixed byte array fields (e.g. private fixed byte _data[SomeFixedLength];) into type-safe, zero-allocation views over binary data. It is useful for working with binary formats such as network packets and media containers, especially when the declaration should read like the protocol spec itself.

Spangle.LusterBits targets structs holding network-ordered (big-endian) fixed byte arrays with no padding. You do not need to unmarshal packets — just map the buffer with MemoryMarshal.AsRef<TargetStruct>(...) and read.

Features

  • Readonly getters for every bit field — no defensive copies through in / ref readonly variables, no allocation
  • Setters with read-modify-write on the affected bytes only; neighbor bits are always preserved
  • Compose() / ComposeTo() — generated single-pass factories: build the whole struct, or compose directly into an output buffer (one store per byte, masks folded at generation time; as fast as hand-packed writes)
  • Constant bits (BitFieldConst) for markers / reserved / sync patterns, stamped in one call by the generated InitializeConstants() and included in Compose()
  • Split fields — one logical value interleaved with marker bits (e.g. an MPEG PES timestamp, 33 bits split 3/15/15) declared as multiple BitField entries sharing a name
  • Segments — read-only views over variable structures whose field presence and offsets depend on preceding fields, including variable-length byte ranges
  • Compile-time validation — declarations are checked against the physical buffer (bounds, bit lengths, overlaps) as compiler diagnostics, before any unsafe access is generated
  • Interface and DTO generation (GenerateType.Interface / GenerateType.Deserialized)
  • GC-safe by construction — generated spans are built on MemoryMarshal.CreateSpan over tracked references, so they stay valid even if the GC relocates a heap-resident struct
  • Fixed-length string views (experimental)

Getting Started

Spangle.LusterBits is available as a NuGet package. You can install it from the NuGet Package Manager Console:

Install-Package Spangle.LusterBits

Or via the .NET CLI:

dotnet add package Spangle.LusterBits

Usage

Spangle.LusterBits searches for the marker attribute LusterCharmAttribute. It must be placed on a partial struct whose fixed byte array fields carry the field setting attributes (BitFieldAttribute).

// All target structs have a signature like:
// `accessibility unsafe partial struct StructName`
[LusterCharm]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe partial struct SomePacket
{
    public const int PacketLength = 8; // 64 bits

    [
        BitField(typeof(byte), "SomeByte", 2),
        BitField(typeof(uint), "SomeNotAlignedUInt32", 30),
        BitField(typeof(SomeEnum), "SomeEnumCastedValue", 4),
        BitField(typeof(ushort), "SomeUShort", 15),
        BitField(typeof(bool), "SomeBoolean", 1),
        BitField(typeof(byte), "SomeByte2", 2),
        BitField(typeof(ushort), "SomeUShort2", 10),
    ]
    private fixed byte _data[PacketLength];
}
public enum SomeEnum : byte
{
    Value0 = 0b0000,
    Value1 = 0b0001,
    Value2 = 0b0010,
    // ... can use 4 bits for value
}

The generator then emits a property per field (readonly getter + neighbor-preserving setter), a Compose() factory and a span accessor. Excerpt of the actual output:

public unsafe partial struct SomePacket
{
    public byte SomeByte
    {
        readonly get => (byte)(_data[0] >>> 6);
        set
        {
            ulong __v = unchecked((ulong)value);
            _data[0] = unchecked((byte)((_data[0] & 0x3F) | ((byte)(__v << 6) & 0xC0)));
        }
    }

    public uint SomeNotAlignedUInt32
    {
        readonly get => (uint)(((uint)(_data[0] & 0x3F) << 24) + ((uint)_data[1] << 16) + ((uint)_data[2] << 8) + ((uint)_data[3]));
        set { /* read-modify-write per affected byte */ }
    }

    // ... the remaining fields alike ...

    public static SomePacket Compose(byte someByte, uint someNotAlignedUInt32, /* ... */) { /* one store per byte */ }

    internal Span<byte> DataAsSpan() { /* GC-safe span over the fixed buffer */ }
}

Reading

ref readonly var packet = ref MemoryMarshal.AsRef<SomePacket>(buffer);
return packet.SomeByte; // the first 2 bits of the byte sequence

Writing

Build packets either field by field, or in one pass with Compose():

var packet = SomePacket.Compose(0b10, 12345u, SomeEnum.Value1, 500, true, 0b01, 300);
packet.DataAsSpan().CopyTo(output);

For streaming writers there is also ComposeTo(), which composes straight into a caller-provided buffer — no intermediate struct, one store per byte — and returns the number of bytes written, so it chains naturally with GetSpan / Advance:

writer.Advance(SomePacket.ComposeTo(writer.GetSpan(8), 0b10, 12345u, /* ... */));

Setters and Compose() are not generated for string / floating point fields (those views stay get-only).

Backing buffers: fixed or InlineArray

The annotated buffer may be declared in either of two byte-identical forms:

// classic (any target framework; requires the unsafe context)
private fixed byte _data[PacketLength];

// C# 12+ inline array (no unsafe anywhere; bounds-checked element access)
[InlineArray(PacketLength)]
public struct ByteData { private byte _element0; }
// ...
private ByteData _data;

The BCL InlineArrayN<byte> family (InlineArray2<byte>..InlineArray16<byte>, .NET 9+) works as a backing type too; sizes beyond 16 need a custom [InlineArray(N)] struct like the one above. With an inline-array backing the generated partial drops its unsafe modifier — the accessors compile to the same masks and shifts either way, so this is purely a safety/ergonomics choice. fixed remains fully supported for consumers below C# 12.

Constant bits and split fields

A logical value interleaved with constant marker bits (e.g. an MPEG PES timestamp: 33 bits split 3/15/15 by markers) is declared as multiple BitField entries sharing a name, plus BitFieldConst for the fixed bits:

[
    BitField(typeof(byte), "Prefix", 4),
    BitField(typeof(ulong), "Value", 3),   // bits 32..30
    BitFieldConst("Marker1", 1, 1),
    BitField(typeof(ulong), "Value", 15),  // bits 29..15
    BitFieldConst("Marker2", 1, 1),
    BitField(typeof(ulong), "Value", 15),  // bits 14..0
    BitFieldConst("Marker3", 1, 1),
]
private fixed byte _value[5];

One Value property (get/set) covers all ranges. InitializeConstants() stamps every constant bit in one call, and Compose(prefix, value) includes them automatically.

Segments: variable structures, read-only

Segment declares a chain of typed views whose presence — and therefore the offsets of everything after them — depends on preceding fields. The generator emits the offset chain, Has*** guards and ref readonly accessors:

[
    Segment(typeof(TSHeader), "Header"),
    Segment(typeof(AdaptationFieldsBasic), "AdaptationFields",
        If = "Header.AdaptationFieldControl.HasAdaptationField()"),
    Segment(typeof(PCR), "PCR", If = "AdaptationFields.HasPCR"),
    Segment("PrivateData", Size = "PrivateDataLength.Value"), // raw variable-length range
]
private fixed byte _value[188];

Each typed segment type must declare public const int Size; raw segments (Segment("Name")) take a Size expression instead and are exposed as ReadOnlySpan<byte>. If and Size are emitted verbatim and may reference preceding segment accessors. Segments are deliberately read-only: build variable structures sequentially instead of patching them in place. Accessing a segment whose Has*** guard is false throws InvalidOperationException instead of silently returning a view over the wrong bytes.

Field Settings

BitFieldAttribute has the following parameters:

  • type: Type of the generated property. The extracted value is cast to this type. The type MUST be a primitive type or an enum.
  • name: Name of the generated property. Repeating a name declares a split field.
  • length: Length of the field in bits. If the length exceeds the type, higher bits are ignored.
  • position: Position of the field in bits. Can be omitted when the fields are declared in order without gaps.
  • description: Description of the field. When specified, the generated property gets a documentation comment.

BitFieldConst(name, length, value) declares constant bits and takes the same optional position / description. BitFieldOptionsAttribute(targetFieldLength) sets the size of the generated ***AsSpan() explicitly when the bit declarations do not cover the whole buffer, and its AsSpanAccessibility raises the ***AsSpan() / ***AsSpanReadOnly() accessors from the default internal to public:

[BitFieldOptions(AsSpanAccessibility = SpanAccessibility.Public)]
private fixed byte _data[PacketLength];

Types

  • Almost all primitive types
    • byte
    • sbyte
    • ushort
    • short
    • uint
    • int
    • ulong
    • long
    • float
    • double
      • float / double are read as IEEE 754 bit patterns; the field must be exactly 32 / 64 bits, and the view is get-only
    • bool
      • Evaluated as fieldVal != 0
  • Enum types

Signed integer fields narrower than their type are sign-extended (two's complement), so a -1 written through the setter reads back as -1. decimal is not supported: it has no meaningful bit-level wire format.

  • String types (Experimental)
    • UTF8String
    • UTF16String
    • UTF16BigEndianString
    • string (treated as UTF8String)

String types are experimental, because they can only be used in the rare case of fixed-length strings.

There may be cases where an enum-like (e.g. A1234 | B5678) fixed-length ASCII string is needed; in such cases you can use UTF8String, assume that each character is a single byte, and treat the value as an ordinary ASCII string.

Nested Structs

For nested layouts, either give the child struct its own fixed byte array with BitFieldAttributes and place it as a plain field, or — when the child's offset depends on preceding fields — declare it as a Segment (see above).

[LusterCharm]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe partial struct SomePacket
{
    public const int PacketLength = 8; // 64 bits

    [
        BitField(typeof(byte), "SomeByte", 2),
        // its own field settings...
    ]
    private fixed byte _data[PacketLength];

    public readonly SomeNestedPacket SomeNestedPacket;
}

Request for Comments

If this project is interesting to you and you have a real use for it, please let us know if there are any missing features. We may not implement everything, but we may respond to make this project better. One thing that is intentionally out of scope is reading from streams or pipes: buffer acquisition, framing and buffer lifetime belong to the consumer.

Versioning

From 1.0.0 on, this library follows Semantic Versioning: breaking changes to the attribute surface or the generated member shapes only happen in major releases.

License

This library is under the MIT License. See the LICENSE file for the full license text.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.0 113 7/11/2026
1.1.0 122 7/10/2026
1.0.0 100 7/9/2026
0.1.1 352 7/18/2023
0.1.0 304 7/18/2023