Spangle.LusterBits
1.2.0
dotnet add package Spangle.LusterBits --version 1.2.0
NuGet\Install-Package Spangle.LusterBits -Version 1.2.0
<PackageReference Include="Spangle.LusterBits" Version="1.2.0" />
<PackageVersion Include="Spangle.LusterBits" Version="1.2.0" />
<PackageReference Include="Spangle.LusterBits" />
paket add Spangle.LusterBits --version 1.2.0
#r "nuget: Spangle.LusterBits, 1.2.0"
#:package Spangle.LusterBits@1.2.0
#addin nuget:?package=Spangle.LusterBits&version=1.2.0
#tool nuget:?package=Spangle.LusterBits&version=1.2.0
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 readonlyvariables, 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 generatedInitializeConstants()and included inCompose() - Split fields — one logical value interleaved with marker bits (e.g. an MPEG PES
timestamp, 33 bits split 3/15/15) declared as multiple
BitFieldentries 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.CreateSpanover 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
bytesbyteushortshortuintintulonglongfloatdoublefloat/doubleare 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
- Evaluated as
- 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)
UTF8StringUTF16StringUTF16BigEndianStringstring(treated asUTF8String)
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 | Versions 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. |
-
.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.