Jlzeng.BitSerializer
0.13.1
dotnet add package Jlzeng.BitSerializer --version 0.13.1
NuGet\Install-Package Jlzeng.BitSerializer -Version 0.13.1
<PackageReference Include="Jlzeng.BitSerializer" Version="0.13.1" />
<PackageVersion Include="Jlzeng.BitSerializer" Version="0.13.1" />
<PackageReference Include="Jlzeng.BitSerializer" />
paket add Jlzeng.BitSerializer --version 0.13.1
#r "nuget: Jlzeng.BitSerializer, 0.13.1"
#:package Jlzeng.BitSerializer@0.13.1
#addin nuget:?package=Jlzeng.BitSerializer&version=0.13.1
#tool nuget:?package=Jlzeng.BitSerializer&version=0.13.1
BitSerializer
一个高性能的 .NET 位级别二进制序列化库。通过 Attribute 声明字段的位长度,Source Generator 在编译期自动生成序列化/反序列化代码,零反射开销,适用于网络协议解析、嵌入式通信、二进制文件格式处理等场景。
特性
- 位级别精度 — 字段可以是任意位长度(如 4-bit、12-bit),不受字节边界限制
- MSB / LSB 双模式 — 支持 MSB(高位优先)和 LSB(低位优先)两种位序编码
- Attribute 驱动 — 通过特性标注即可声明序列化结构,无需手写编解码逻辑
- 自动推断位长 — 未指定位长时,自动根据类型推断(
byte= 8,ushort= 16,int= 32 ...) - 嵌套类型 — 支持嵌套复合类型的递归序列化
- 继承链支持 — 支持多层继承,包括通过未标记
[BitSerialize]的中间抽象类型(如泛型基类)正确序列化 - 字符串支持 — 支持固定长度字符串(
[BitFixedString],可自定义 padding 字节)、NUL 终止字符串([BitTerminatedString])、长度前缀字符串([BitLengthPrefixString],length-prefix 8/16/32 bit)和字段引用长度字符串([BitLengthFieldString(nameof(NameLength))],长度由另一字段承载),支持 ASCII 和 UTF-8 编码 - 自定义序列化类型 — 支持实现
IBitSerializable接口的自定义类型,无需[BitSerialize]标记 - 泛型类型参数 — 支持泛型类型参数字段的序列化,通过
IBitSerializable接口在运行时分发 - 集合支持 — 支持
List<T>/T[]的序列化,元素数量可动态关联或固定指定;也支持按字节预算驱动嵌套动态元素集合(RelationKind = ByteLength) - 数组短包/剩余字节 —
[BitFieldCount(N, PadIfShort=true)]短数据自动补零;[BitFieldConsumeRemaining]读取直到数据末尾 - 声明式 CRC —
[BitCrc]+[BitCrcInclude]自动计算并回填 CRC 字段,内置 CRC-CCITT / CRC-16/ARC / CRC-32;[BitCrc(WholeBuffer = true)]一键覆盖整 buffer 并用SkipHeadBytes/SkipTailBytes排除帧头/帧尾 - 字段级大小端覆盖 —
[BitField(Endian = BitEndian.Big/Little)]让单个数值字段独立选择字节序,方便跨混合大小端的真实协议 - 常量字段钉死与校验 —
[BitFieldValue(0x7E)]序列化时强制写入魔数、反序列化时自动校验,把帧头/帧尾/协议版本验证从 FluentValidation 前移到解码失败点 - 嵌套类型按字节预算 —
[BitFieldRelated(nameof(Length), RelationKind = ByteLength)]既适用于 List 也适用于嵌套[BitSerialize]类型,序列化时自动按 nested 类型的实际字节数回填长度字段,反序列化时按 wire 长度严格校验 - 自动回填关联字段 — 序列化时自动将集合长度写入关联的计数字段、将运行时类型写入多态判别字段,无需手动设置
- 多态类型 — 通过类型判别字段自动分发到具体子类
- 值转换器 — 支持自定义序列化/反序列化时的值变换,支持上下文感知重载
- 序列化上下文与生命周期钩子 — 支持通过上下文对象传递状态,以及序列化/反序列化前后的回调
- record 类型支持 — 支持
record class和record struct - Source Generator — 编译期自动生成序列化代码,零反射开销
- 编译期诊断 — 自动检测嵌套类型是否缺少
[BitSerialize]标记,编译时报错 - 一包即用 — 只需引用
Jlzeng.BitSerializer,Source Generator 自动生效
环境要求
- .NET 8.0 与 .NET 10.0(双 TFM / LTS) — 从
0.13.1起同时面向net8.0与net10.0多目标构建;.NET 8 LTS 预计约 2026-11-10 结束支持,建议新项目优先选用 .NET 10。
快速开始
基本用法
using BitSerializer;
// 定义数据结构(需标记 [BitSerialize] 和 partial)
[BitSerialize]
public partial class Packet
{
[BitField(8)]
public byte Header { get; set; }
[BitField(16)]
public ushort Payload { get; set; }
[BitField(8)]
public byte Checksum { get; set; }
}
// 也支持 record 类型
[BitSerialize]
public partial record RecordPacket
{
[BitField(8)]
public byte Header { get; set; }
[BitField(16)]
public ushort Payload { get; set; }
}
// MSB 模式(高位优先,大端序)
var packet = new Packet { Header = 0xAB, Payload = 0x1234, Checksum = 0xCD };
byte[] bytes = BitSerializerMSB.Serialize(packet);
var result = BitSerializerMSB.Deserialize<Packet>(bytes);
// LSB 模式(低位优先,小端序)
byte[] lsbBytes = BitSerializerLSB.Serialize(packet);
var lsbResult = BitSerializerLSB.Deserialize<Packet>(lsbBytes);
高性能与零额外分配 API
BitSerializerMSB 和 BitSerializerLSB 提供相同的高性能入口:
int size = BitSerializerMSB.GetRequiredByteCount(packet);
Span<byte> buffer = stackalloc byte[size];
OperationStatus writeStatus =
BitSerializerMSB.TrySerialize(packet, buffer, out int bytesWritten);
var reusable = new Packet();
OperationStatus readStatus =
BitSerializerMSB.TryDeserializeInto(buffer, reusable, out int bytesConsumed);
TrySerialize不扩容;目标过小时返回DestinationTooSmall,成功时稳定热路径不产生库内临时分配。TryDeserializeInto复用顶层对象、嵌套对象、数组和 List。数值/值类型 List 须有足够Capacity;引用类型 List 还须预先填充足够Count,且每个元素对象非空;数组须有足够长度,引用元素也须预创建,否则返回DestinationTooSmall。TryDeserialize(ref T, ...)也支持 struct 和 class;失败时对象可能已部分更新,需要事务语义时继续使用Deserialize<T>()。- 输入不足返回
NeedMoreData,预分配对象图容量不足返回DestinationTooSmall,CRC/判别值/预算格式错误返回InvalidData;用户 Hook 与 Converter 的业务异常不会被吞掉。 - 从
0.13.0起,兼容Deserialize<T>()遇到未知多态判别值时抛InvalidDataException(旧版本为InvalidOperationException)。 0.13.1:多目标net8.0+net10.0(双 TFM / LTS),NuGet 包同时包含两套运行时程序集。- 字符串反序列化仍必须创建最终
string;多态类型变化等语义要求创建新对象的情况不属于严格原地模式。 - 内置 CRC 和字符串序列化使用无临时对象路径。自定义 CRC 可额外实现
IBitCrcAlgorithm<TSelf>;数值转换器可实现IBitFieldValueConverter<TProperty, TWire>或IBitFieldValueConverter<TProperty, TWire, TContext>避免装箱。 - 首次 JIT、泛型初始化、用户 Hook/Context/Converter 内部行为和异常路径不计入稳定热路径的零分配承诺。
三泛型 Converter 的 TContext 来自模型的 SerializeContext() / DeserializeContext(),Generator 会直接生成强类型调用;TWire 决定 wire 读写类型,并在其位宽小于字段 BitLength 时报告 BITS063。
MSB vs LSB:两者的 API 完全一致,区别仅在于字节内的位序方向。MSB 适用于网络协议(大端序),LSB 适用于硬件寄存器、部分嵌入式协议(小端序)。
性能实测
环境:Debian GNU/Linux,x64;BenchmarkDotNet;warmup 4 / iteration 10;载荷约 200 bits(BenchmarkTests 项目同一用例)。对比对象含手写 Span 编解码与 BinarySerializer。
.NET 8.0.30 — Serialize
| Method | Mean | Allocated |
|---|---|---|
| Manual_Ser | 8.153 ns | 0 B |
| BitSerializer_TrySer | 54.037 ns | 0 B |
| BitSerializer_Ser | 54.499 ns | 0 B |
| BinarySerializer_Ser | 14168 ns | 27928 B |
.NET 8.0.30 — Deserialize
| Method | Mean | Allocated |
|---|---|---|
| BitSerializer_TryDeInto | 53.521 ns | 0 B |
| Manual_De | 97.880 ns | 280 B |
| BitSerializer_De | 110.717 ns | 264 B |
| BinarySerializer_De | 24003 ns | 49016 B |
.NET 10.0.11 — Serialize
| Method | Mean | Allocated |
|---|---|---|
| Manual_Ser | 5.881 ns | 0 B |
| BitSerializer_TrySer | 61.984 ns | 0 B |
| BitSerializer_Ser | 63.533 ns | 0 B |
| BinarySerializer_Ser | 11476 ns | 27912 B |
.NET 10.0.11 — Deserialize
| Method | Mean | Allocated |
|---|---|---|
| BitSerializer_TryDeInto | 30.379 ns | 0 B |
| Manual_De | 81.965 ns | 280 B |
| BitSerializer_De | 112.110 ns | 264 B |
| BinarySerializer_De | 18061 ns | 46808 B |
要点:
- 相对
BinarySerializer:Serialize / Deserialize 仍是数量级差距,且TrySerialize/TryDeInto稳定热路径 0 B 分配。 - 相对手写 baseline:
Try*API 约在同一数量级(数十 ns),用 Attribute + Source Generator 换可维护性。 - .NET 10 上:
TryDeInto约快 43%(53.5 → 30.4 ns);Manual / BinarySerializer 也更快;本机上Ser/TrySer略慢约 15%(54 → 62 ns 量级)——如实记录,不强行夸大。 - 方法:BenchmarkTests 项目、相同 ~200 bits 载荷;不同机器/SDK 补丁结果会有波动。
0.13.1:多目标 net8.0;net10.0,CI 同步安装 8.0.x / 10.0.x SDK。
自动推断位长
不指定位长时,根据属性类型自动推断:
public class AutoData
{
[BitField] // 自动推断为 8 bit
public byte A { get; set; }
[BitField] // 自动推断为 16 bit
public ushort B { get; set; }
[BitField] // 自动推断为 32 bit
public int C { get; set; }
}
自定义位长度(非字节对齐)
字段可以跨越字节边界,实现紧凑的位打包:
public class CompactData
{
[BitField(4)] // 高 4 位
public byte NibbleHigh { get; set; }
[BitField(4)] // 低 4 位
public byte NibbleLow { get; set; }
[BitField(12)] // 12 位跨字节
public ushort TwelveBits { get; set; }
[BitField(4)]
public byte FourBits { get; set; }
}
// 总计 24 bits = 3 字节
枚举类型
枚举类型直接支持:
public enum Status : byte
{
Unknown = 0,
Active = 1,
Inactive = 2,
}
public class StatusPacket
{
[BitField(8)]
public Status CurrentStatus { get; set; }
[BitField(16)]
public ushort Code { get; set; }
}
嵌套类型
支持复合类型的递归序列化。嵌套类型也需要标记 [BitSerialize] 和 partial:
[BitSerialize]
public partial class Point
{
[BitField(8)]
public byte X { get; set; }
[BitField(8)]
public byte Y { get; set; }
}
[BitSerialize]
public partial class Frame
{
[BitField(8)]
public byte Id { get; set; }
[BitField] // 自动推断嵌套类型的总位长
public Point Position { get; set; } = new();
[BitField(8)]
public byte Flags { get; set; }
}
继承链
支持多层继承的序列化。中间基类即使未标记 [BitSerialize](如抽象泛型基类),其字段也会被正确包含:
[BitSerialize]
public partial record BasePacket
{
[BitField(8)]
public byte Header { get; set; }
[BitField(16)]
public ushort CommonField { get; set; }
}
// 中间抽象泛型基类,无需标记 [BitSerialize]
public abstract record GenericPacket<TPayload> : BasePacket
where TPayload : PayloadBase
{
[BitField]
public TPayload Payload { get; set; }
}
// 具体子类标记 [BitSerialize],自动继承整个链上的字段
[BitSerialize]
public partial record ConcretePacket : GenericPacket<MyPayload> { }
生成的代码会自动调用 base.SerializeLSB() 处理基类字段,并序列化中间类型的字段。
泛型类型参数
泛型类型参数字段通过 IBitSerializable 接口在运行时动态分发,位长度在运行时计算:
public abstract record Container<T> : BaseType where T : PayloadBase
{
[BitField]
public T Data { get; set; } // 运行时通过接口调用序列化
}
字符串
支持固定长度和 NUL 终止两种字符串序列化模式,默认 ASCII 编码,可选 UTF-8:
[BitSerialize]
public partial class StringPacket
{
[BitField]
[BitFixedString(10)] // 固定 10 字节,不足补 NUL
public string Name { get; set; } = "";
[BitField]
[BitTerminatedString] // NUL 终止,动态长度
public string Description { get; set; } = "";
[BitField]
[BitFixedString(16, Encoding = StringEncoding.UTF8)] // UTF-8 编码
public string Utf8Field { get; set; } = "";
}
自定义序列化类型(IBitSerializable)
实现 IBitSerializable 接口的类型可直接用作字段或集合元素,无需标记 [BitSerialize]:
public struct CustomHeader : IBitSerializable
{
public byte Magic;
public ushort Length;
public int SerializeLSB(Span<byte> bytes, int bitOffset) { /* 手动写入 */ }
public int SerializeMSB(Span<byte> bytes, int bitOffset) { /* 手动写入 */ }
public int DeserializeLSB(ReadOnlySpan<byte> bytes, int bitOffset) { /* 手动读取 */ }
public int DeserializeMSB(ReadOnlySpan<byte> bytes, int bitOffset) { /* 手动读取 */ }
public int GetTotalBitLength() => 24;
}
[BitSerialize]
public partial class Packet
{
[BitField(24)]
public CustomHeader Header { get; set; } // 自定义序列化类型
[BitField(8)]
[BitFieldCount(4)]
public CustomHeader[] Items { get; set; } // 也可作为集合元素
}
集合(List / Array)
通过 BitFieldRelated 关联计数字段,或通过 BitFieldCount 指定固定数量:
// 动态数量:关联计数字段
public class DynamicList
{
[BitField(4)]
public byte Count { get; set; } // 序列化时自动从 Items.Count 回填
[BitField(4)]
public byte Reserved { get; set; }
[BitField]
[BitFieldRelated(nameof(Count))] // 元素数量由 Count 字段决定
public List<byte> Items { get; set; } = new();
}
// 序列化时无需手动设置 Count,会自动回填:
var data = new DynamicList { Reserved = 0, Items = [0x11, 0x22, 0x33] };
byte[] bytes = BitSerializerMSB.Serialize(data);
// data.Count 自动设为 3,溢出(超过 4-bit 最大值 15)时抛出异常
// 固定数量
public class FixedList
{
[BitField]
[BitFieldCount(3)] // 固定 3 个元素
public List<byte> Items { get; set; } = new();
}
按字节预算驱动集合(RelationKind = ByteLength)
当线上协议的关联字段承载的不是元素个数,而是 集合占用的字节总数 时(典型例子:应用层帧 Length = Payload.Length + 4,或 Gal 这类 AppFrameLength 为应用帧区总字节数),使用 RelationKind = BitRelationKind.ByteLength:
// 线上 Length 字段 = Data.Length + 4;ValueConverterType 负责该常量换算
[BitSerialize]
public partial class AppFrameBase
{
[BitField] public ushort Length { get; set; }
[BitField] public ushort AppFrameType { get; set; }
[BitField] public ushort Reserved { get; set; }
[BitField]
[BitFieldRelated(nameof(Length),
ValueConverterType = typeof(AppFrameLengthConverter),
RelationKind = BitRelationKind.ByteLength)]
public byte[] Data { get; set; } = [];
}
// AppFrames 按总字节预算读取,直到预算耗尽
[BitSerialize]
public partial class Gal
{
[BitField] public GalHead Head { get; set; } = new();
[BitField] public ushort AppFrameLength { get; set; }
[BitField]
[BitFieldRelated(nameof(AppFrameLength),
RelationKind = BitRelationKind.ByteLength)]
public List<AppFrameBase> AppFrames { get; set; } = [];
}
行为:
- 序列化:先计算集合所有元素的实际字节总和 → 可选经过
ValueConverterType.OnSerializeConvert换算为线上值 → 回填关联字段。 - 反序列化:从关联字段读取线上值 → 可选经过
ValueConverterType.OnDeserializeConvert还原为字节预算 → 在预算内循环解析元素,直到预算耗尽。 - 元素可以是
byte[]、List<基础类型>,也可以是嵌套[BitSerialize]类型(含动态长度元素)。 ValueConverterType在ByteLength模式下是 长度换算器(而不是列表值换算器),必须同时实现OnSerializeConvert与OnDeserializeConvert(BITS024)。- 约束:不能与
[BitFieldCount]/[BitFieldConsumeRemaining]共用(BITS025);静态元素位宽必须是 8 的倍数(BITS026);只能作用在集合上(BITS027)。 - 运行时越界:元素越过预算 / 预算剩余未消费 / 预算超出剩余字节,都会抛出
InvalidDataException。
数组短包与消费剩余字节
针对定长缓冲但实际数据可能更短的场景(如 ATP BTM 报文:固定 276 字节但空报文为 0 字节):
[BitSerialize]
public partial class BtmPacket
{
[BitField(8)]
public byte Kind { get; set; }
// 序列化:始终写入 276 字节;实际数据不足时末尾补 0
// 反序列化:流不足 276 字节时读取实际可用部分,剩余填 default(T)
[BitField]
[BitFieldCount(276, PadIfShort = true)]
public byte[] BtmBytes { get; set; } = [];
}
对于尾部变长数组,使用 [BitFieldConsumeRemaining](必须是末尾字段,元素必须是基本数值/枚举类型):
[BitSerialize]
public partial class Frame
{
[BitField(8)] public byte Marker { get; set; }
[BitField(16)] public ushort Kind { get; set; }
// 序列化:按 array.Length 写入实际长度
// 反序列化:从当前位置读到 bytes 末尾
[BitField]
[BitFieldConsumeRemaining]
public byte[] Trailing { get; set; } = [];
}
声明式 CRC
[BitCrc] 标注 CRC 结果字段,[BitCrcInclude] 标注参与计算的字段。源码生成器在所有字段序列化完成后自动计算 CRC 并回填结果字段,无需 AfterSerialize 钩子或硬编码字节偏移:
using BitSerializer;
using BitSerializer.CrcAlgorithms;
[BitSerialize]
public partial class Frame
{
[BitField(8)]
public byte Start { get; set; } = 0x7E;
[BitField(8), BitCrcInclude(nameof(Crc))]
public byte DestAddr { get; set; }
[BitField(8), BitCrcInclude(nameof(Crc))]
public byte SrcAddr { get; set; }
[BitField, BitFieldCount(8, PadIfShort = true), BitCrcInclude(nameof(Crc))]
public byte[] Payload { get; set; } = [];
// CRC-CCITT (poly 0x1021),序列化时自动计算并写入
[BitField(16), BitCrc(typeof(CrcCcitt), InitialValue = 0)]
public ushort Crc { get; set; }
[BitField(8)]
public byte End { get; set; } = 0xCF;
}
- 内置算法(
BitSerializer.CrcAlgorithms命名空间):CrcCcitt(CRC-16-CCITT, poly 0x1021)、Crc16Arc(IBM CRC-16, poly 0x8005 反射)、Crc32(IEEE 802.3, poly 0xEDB88320 反射) - 可选参数:
InitialValue(初始值)、ValidateOnDeserialize(反序列化时校验不匹配抛InvalidDataException) - 自定义算法:实现
IBitCrcAlgorithm接口(BitWidth/Reset(ulong)/Update(ReadOnlySpan<byte>)/Result),要求公共无参构造函数 - 多层 CRC 支持:外层
[BitSerialize]类型包含内层已带 CRC 的嵌套类型时,内层先计算,外层看到的是内层 CRC 已就位的字节
约束(编译期报错):
[BitCrcInclude]字段必须连续且首尾字节对齐(BITS017)[BitCrc]结果字段必须字节对齐(BITS018)- 算法类型必须实现
IBitCrcAlgorithm且有公共无参构造函数(BITS016) [BitCrc]字段不能同时用[BitFieldRelated](避免 converter 产物被 CRC 覆盖,BITS021)- 动态长度
[BitSerialize]基类的派生类型不能含[BitCrc](运行时偏移会漂移,BITS022)
WholeBuffer 模式(覆盖整个 buffer + 头尾跳过)
逐字段 [BitCrcInclude] 在面向典型协议帧时往往写起来很啰嗦:所有 payload 字段都要标,多态/动态子类型在 BITS017 字节对齐限制下也不易表达。[BitCrc(WholeBuffer = true)] 提供了 "覆盖整个 buffer、再用 SkipHeadBytes / SkipTailBytes 跳掉帧头帧尾" 的对偶写法:
// DMI 风格帧:Sync(1) + Type(1) + Length(2) + Crc(2) + EOF(2),CRC 覆盖 Type + Length,
// 等价于 UartCrc16(bytes, offset = 1, length = totalBytes - 4)
[BitSerialize]
public partial class DmiFrame
{
[BitField(8)] public byte Sync { get; set; }
[BitField(8)] public byte Type { get; set; }
[BitField(16)] public ushort Length { get; set; }
[BitField(16), BitCrc(typeof(CrcCcitt),
WholeBuffer = true,
SkipHeadBytes = 1, // 跳过 Sync
SkipTailBytes = 4)] // 跳过 Crc(2) + EOF(2)
public ushort Crc { get; set; }
[BitField(16)] public ushort EndOfFrame { get; set; }
}
// 动态 List + 多态子类型也能直接用,无需逐个 [BitCrcInclude]
[BitSerialize]
public partial class DynamicFrame
{
[BitField(8)] public byte Count { get; set; }
[BitField(8), BitFieldRelated(nameof(Count))]
public List<byte> Data { get; set; } = new();
[BitField(16), BitCrc(typeof(CrcCcitt), WholeBuffer = true, SkipTailBytes = 2)]
public ushort Crc { get; set; }
}
SkipHeadBytes/SkipTailBytes默认0,必须 ≥ 0(BITS030)- 同一 CRC 字段不能同时使用
WholeBuffer = true与逐字段[BitCrcInclude](BITS029) - 编译期校验 CRC 自身字节 必须 落在 head/tail 跳过区内(BITS034),否则 CRC 会读到自己未初始化或过期的字节
- 若同时存在前置 + 后置动态字段(CRC 自身会被运行时漂移挤出保护区),编译期拒绝(BITS035);要么把所有动态字段放在 CRC 前 +
SkipTailBytes,要么放在后 +SkipHeadBytes,混排请改用[BitCrcInclude]模式
长度前缀字符串([BitLengthPrefixString])
很多协议字符串是 "1/2/4 字节长度前缀 + raw bytes" 的形式(典型例子:DMI 站名、TCP Modbus header)。这里提供一个声明式语法糖,免去手写 length 字段 + [BitFieldRelated] + 长度回填的样板代码:
[BitSerialize]
public partial class StationFrame
{
[BitField(8)] public byte Header { get; set; }
// 16-bit 长度前缀 + UTF-8 编码字符串,序列化时自动计算字节数并写入前缀
[BitLengthPrefixString(16, Encoding = BitStringEncoding.UTF8)]
public string StationName { get; set; } = "";
// 可选 MaxBytes:限制编码后字节上限,UTF-8 自动做合法字符边界回退(不会切到半个字符)
[BitLengthPrefixString(8, Encoding = BitStringEncoding.UTF8, MaxBytes = 32)]
public string Tag { get; set; } = "";
}
LengthBits必须是8、16或32(BITS032),匹配常见的byte/ushort/uint长度字段MaxBytes默认0= 不限(仅受LengthBits容量约束),必须 ≥ 0(BITS036);序列化和反序列化两侧都强制- 编码后字节超过
LengthBits上限或MaxBytes时抛InvalidDataException(运行时) - UTF-8 短缩时会回退到最近的字符边界,避免序列化出半个字符
- 字段起始必须字节对齐(BITS038),且不能位于 "运行时偏移可能漂移" 的动态字段之后(BITS039)
字段级大小端覆盖([BitField(Endian = ...)])
整个 BitSerializerMSB / BitSerializerLSB 决定了 "默认大端 / 默认小端",但真实协议常常同一帧里混用两种字节序——例如 DMI 帧用大端的 UART 包装但内部某些 ushort 字段是小端 Modbus 派生。[BitField(Endian = ...)] 让单个数值字段独立切换字节序,无需把整个类切到反向序列化器:
[BitSerialize]
public partial class HybridFrame
{
[BitField(8)] public byte Sync { get; set; }
// 帧体默认走外层序列化器的字节序
[BitField(16)] public ushort Type { get; set; }
// 这两个字段强制按小端写入,无视外层用的是 MSB 还是 LSB
[BitField(16, Endian = BitEndian.Little)] public ushort LittlePayloadA { get; set; }
[BitField(32, Endian = BitEndian.Little)] public uint LittlePayloadB { get; set; }
// 这个字段强制按大端写入
[BitField(16, Endian = BitEndian.Big)] public ushort BigChecksum { get; set; }
}
- 三个枚举值:
BitEndian.Inherit(默认,跟随外层)/BitEndian.Big(MSB)/BitEndian.Little(LSB) - 仅对 "字节对齐 + 字节倍数宽度的数值/枚举标量" 生效(BitStartIndex % 8 == 0 且 BitLength ∈ {8, 16, 32, 64},BITS028);不能用于位段/字符串/列表/嵌套
- 不能跟在运行时偏移可能漂移的动态字段后(BITS033),否则字节序翻转可能退化为位排列腐蚀
- 不能用越界 cast(如
(BitEndian)5)绕过枚举范围(BITS037) - 嵌套类型如果内部含
[BitField(Endian = ...)]字段,外层把它放到非字节对齐位置 / 跟动态字段后会被编译期拒绝(BITS040 / BITS041),跨 assembly 也能正确检测
常量字段钉死([BitFieldValue(...)])
协议帧普遍有 FrameStart=0x7E / FrameEnd=0xCF / ProtocolVersion=1 这类魔数字段——序列化时要强制写入、反序列化时要校验。[BitFieldValue] 把这套逻辑前移到字段声明:
[BitSerialize]
public partial class Frame
{
[BitField(8), BitFieldValue(0x7E)] public byte Start { get; set; }
[BitField(16)] public ushort Payload { get; set; }
[BitField(8), BitFieldValue(0xCF)] public byte End { get; set; }
}
// 序列化:无视用户给的值,强制写入常量,并把属性也回写为该常量
var src = new Frame { Start = 0x00, Payload = 0x1234, End = 0x00 };
byte[] bytes = BitSerializerMSB.Serialize(src);
// bytes[0] = 0x7E, bytes[3] = 0xCF;src.Start / src.End 也被改写为常量
// 反序列化:Verify=true(默认)时与常量不一致抛 InvalidDataException
var bad = new byte[] { 0xFF, 0x12, 0x34, 0xCF };
BitSerializerMSB.Deserialize<Frame>(bad); // 抛 InvalidDataException
// 枚举字面量也接受
public enum FrameKind : byte { Heartbeat = 1, Data = 2 }
[BitField(8), BitFieldValue(FrameKind.Data)] public FrameKind Kind { get; set; }
- 仅适用于数值/枚举标量(BITS042);常量必须能放进字段的 BitLength(BITS043)
- 不能与
[BitCrc]/[BitFieldRelated]/[BitFieldCount]/[BitPoly]共用(BITS044),这些机制会覆写 wire 字节 - 仅接受整数字面量和枚举成员,不接受 float / string / Type / 数组(BITS045)
Verify = false关闭反序列化校验(只是把读到的值赋回属性),适合"协议给什么就吃什么、再由调用方决定怎么处理"的场景
嵌套类型按字节预算(RelationKind = ByteLength on nested type)
以前 RelationKind = ByteLength 只能作用在 List/Array 上,嵌套类型不能用。v0.11.0 起放开了限制——嵌套 [BitSerialize] 类型也能像 list 一样按字节数自动回填、按字节预算严格校验:
[BitSerialize]
public partial class FrameContent
{
[BitField(8)] public byte A { get; set; }
[BitField(8)] public byte B { get; set; }
[BitField(16)] public ushort C { get; set; }
}
[BitSerialize]
public partial class Frame
{
[BitField(8)] public byte Sync { get; set; }
// Length 必须先于 Content 声明(BITS052)
[BitField(16)] public ushort Length { get; set; }
// Content 是嵌套 [BitSerialize] 类型;Length 自动 = Content 序列化字节数
[BitField, BitFieldRelated(nameof(Length), RelationKind = BitRelationKind.ByteLength)]
public FrameContent Content { get; set; } = new();
[BitField(8)] public byte End { get; set; }
}
// 序列化:Length 自动回填为 4(Content 占 4 字节)
var src = new Frame { Sync = 0x7E, Content = new() { A = 1, B = 2, C = 3 }, End = 0xCF };
byte[] bytes = BitSerializerMSB.Serialize(src); // Length 自动写入
// 反序列化:从 Length 读字节数 → 反序列化 Content → 验证 Content 消耗的 bit 数 == Length × 8
// 如果不一致(如帧错乱)抛 InvalidDataException
- 长度字段必须 先于 嵌套字段声明(BITS052),反序列化才能先知道字节预算
- 嵌套类型的静态位长必须是 8 的倍数(BITS051);动态长度嵌套类型在运行时校验
- 支持可选
ValueConverterType在线上长度与字节数之间换算(与 list ByteLength 一致),converter 必须同时实现序列化/反序列化方向(BITS024) - 同时适用于:静态嵌套
[BitSerialize]、动态嵌套[BitSerialize]、IBitSerializable类型、泛型类型参数
字段引用长度字符串([BitLengthFieldString])
区别于 [BitLengthPrefixString](长度内联在字符串前)——这里字符串的字节数来自 另一个独立声明的字段。典型 DMI 协议:站名 / 提示文本的长度字段位置和字符串位置由协议固定,无法自描述。
[BitSerialize]
public partial class StationFrame
{
[BitField(8)] public byte Header { get; set; }
[BitField(8)] public byte NameLength { get; set; } // 长度字段
[BitLengthFieldString(nameof(NameLength), Encoding = BitStringEncoding.UTF8)]
public string Name { get; set; } = "";
[BitField(8)] public byte Footer { get; set; }
}
// 序列化:NameLength 自动 = UTF8 字节数;序列化后 src.NameLength 同步更新
var src = new StationFrame { Header = 0xAB, Name = "Tokyo", Footer = 0xCD };
byte[] bytes = BitSerializerMSB.Serialize(src);
// bytes = [0xAB, 5, 'T','o','k','y','o', 0xCD],src.NameLength 已自动设为 5
- 引用字段必须是 byte/sbyte/short/ushort/int/uint,BitLength ∈ {8, 16, 32}(BITS047)
- 引用字段必须 先于 字符串字段声明(BITS047)
- 字符串字段必须从字节边界开始(BITS048)且不能跟在运行时偏移可能漂移的字段后(BITS049)
- 可选
MaxBytes限制编码字节上限(≥ 0,BITS050),UTF-8 截断自动避开多字节字符中间
多绑定 [BitFieldRelated](v0.12.0+)
同一字段可以同时贴 2 个 [BitFieldRelated] —— 一个 Count(判别值)+ 一个 ByteLength(字节预算)。
典型用法:多态字段绑定 Type 字段(决定类型)+ Length 字段(决定字节数):
[BitSerialize]
public partial class FrameHeader
{
[BitField(8)] public byte Type { get; set; }
[BitField(16)] public ushort Length { get; set; }
[BitField]
[BitFieldRelated(nameof(Type))] // 判别字段
[BitFieldRelated(nameof(Length), RelationKind = BitRelationKind.ByteLength)] // 字节预算
[BitPoly(1, typeof(SubFrameA))]
[BitPoly(2, typeof(SubFrameB))]
public FrameContent Content { get; set; }
}
序列化时:
Type自动按运行时 Concrete 类型写入对应的TypeIdLength自动按Content的GetTotalBitLength()/8回填Contentpayload 写入
反序列化时:先读 Type 选具体类型、读 Length 拿到字节预算,dispatch 解码后断言 consumedBits == declaredBytes * 8,wire 长度与实际 payload 不一致直接 InvalidDataException。
约束(编译期校验):
- 最多 2 个
[BitFieldRelated](BITS056) - 两个的
RelationKind必须不同(BITS058) - 多绑定模式仅 polymorphic 字段支持(BITS057)—— list/nested 单个 binding 已能表达
- 声明顺序无关,分析器按 RelationKind 把 Count 当 primary
多态类型
通过 BitPoly 特性实现基于判别值的类型分发:
public class BaseMessage
{
[BitField(8)]
public byte CommonField { get; set; }
}
public class MessageA : BaseMessage
{
[BitField(8)]
public byte FieldA { get; set; }
}
public class MessageB : BaseMessage
{
[BitField(16)]
public ushort FieldB { get; set; }
}
public class Container
{
[BitField(8)]
public byte MessageType { get; set; } // 序列化时自动从 Message 的运行时类型回填
[BitField(24)] // 需指定所有子类的最大位长
[BitFieldRelated(nameof(MessageType))] // 关联判别字段
[BitPoly(1, typeof(MessageA))] // MessageType=1 → MessageA
[BitPoly(2, typeof(MessageB))] // MessageType=2 → MessageB
public BaseMessage Message { get; set; }
}
// 序列化时无需手动设置 MessageType,会自动根据运行时类型回填:
var data = new Container { Message = new MessageA { CommonField = 0xAA, FieldA = 0xBB } };
byte[] bytes = BitSerializerMSB.Serialize(data);
// data.MessageType 自动设为 1
值转换器
实现 IBitFieldValueConverter 接口,自定义序列化/反序列化时的值变换。值转换器可用于所有字段类型(数值、枚举、字符串、嵌套类型、多态、泛型参数、集合)。
接口的四个方法均为 static virtual,只需实现你关心的重载即可:
public class DoubleConverter : IBitFieldValueConverter
{
// 只实现反序列化转换——序列化侧不会调用转换器
public static object OnDeserializeConvert(object value)
{
return (byte)((byte)value * 2); // 反序列化时乘 2
}
}
public class ConvertedData
{
[BitField(8)]
[BitFieldRelated(nameof(Value), typeof(DoubleConverter))]
public byte Value { get; set; }
}
上下文感知值转换器
值转换器可以接收上下文对象,用于在转换时访问额外状态。只需重写带 context 参数的重载:
public class OffsetConverter : IBitFieldValueConverter
{
// 基础重载(必须实现)
public static object OnDeserializeConvert(object value) => value;
public static object OnSerializeConvert(object value) => value;
// 上下文感知重载(可选)
public static object OnDeserializeConvert(object value, object? context)
{
if (context is int offset)
return (byte)((byte)value + offset);
return value;
}
public static object OnSerializeConvert(object value, object? context)
{
if (context is int offset)
return (byte)((byte)value - offset);
return value;
}
}
Source Generator 会独立检测每一侧的上下文重载:只重写了 OnSerializeConvert(object, object?) 的转换器,反序列化侧仍会调用无上下文的 OnDeserializeConvert(object),反之亦然。
序列化上下文与生命周期钩子
类型可以提供上下文对象并在序列化/反序列化前后执行回调,实现校验和计算、数据填充等逻辑:
[BitSerialize]
public partial class ChecksumPacket
{
[BitField(8)]
public byte Header { get; set; }
[BitField(16)]
public ushort Data { get; set; }
[BitField(8)]
public byte Checksum { get; set; }
// 提供序列化上下文(传递给嵌套类型和值转换器)
public object? SerializeContext() => this;
public object? DeserializeContext() => this;
// 序列化后回调:计算校验和
public void AfterSerialize(object? context, Span<byte> bytes)
{
Checksum = CalculateChecksum(bytes);
}
// 反序列化后回调:验证校验和
public void AfterDeserialize(object? context, ReadOnlySpan<byte> bytes)
{
if (Checksum != CalculateChecksum(bytes))
throw new InvalidDataException("Checksum mismatch");
}
}
生命周期钩子的完整调用顺序:
| 钩子 | 序列化时 | 反序列化时 |
|---|---|---|
SerializeContext() / DeserializeContext() |
获取上下文对象 | 获取上下文对象 |
BeforeSerialize() / BeforeDeserialize() |
序列化字段前 | 反序列化字段前 |
AfterSerialize() / AfterDeserialize() |
所有字段序列化后 | 所有字段反序列化后 |
用户也可以通过 partial void OnSerializing(object? context) / partial void OnDeserializing(object? context) 拦截生成代码的入口。
注意:上下文和钩子仅用于辅助序列化逻辑(如值转换、校验和计算),不应改变类型的总位长度。
忽略字段
使用 BitIgnore 跳过不需要序列化的字段:
public class MixedData
{
[BitField(8)]
public byte Value { get; set; }
[BitIgnore] // 不参与序列化
public string Description { get; set; } = "";
[BitField(8)]
public byte AnotherValue { get; set; }
}
编译期诊断
Source Generator 会在编译期检查常见错误并报告诊断信息:
| 诊断码 | 说明 |
|---|---|
BITS001 |
成员缺少 [BitField] 或 [BitIgnore] |
BITS002 |
不支持的字段类型 |
BITS003 |
List 缺少计数信息(需 [BitFieldRelated] 或 [BitFieldCount]) |
BITS004 |
值转换器未实现 IBitFieldValueConverter |
BITS005 |
标记 [BitSerialize] 的类型必须是 partial |
BITS006 |
嵌套的序列化字段类型必须标记 [BitSerialize] |
BITS007 |
多态成员缺少判别字段关联 |
BITS008 |
关联成员未找到 |
BITS009 |
成员有序列化辅助特性但缺少 [BitField] |
BITS010 |
[BitFixedString] 只能用于 string 类型 |
BITS011 |
[BitTerminatedString] 只能用于 string 类型 |
BITS012 |
[BitFixedString] 字节长度必须为正数 |
BITS013 |
泛型类型参数缺少 new() 或 struct 约束 |
BITS014 |
引用类型的 [BitField] 成员缺少默认值(序列化前可能为 null) |
BITS015 |
[BitCrcInclude] 目标字段不存在或不是 [BitCrc] |
BITS016 |
[BitCrc] 算法未实现 IBitCrcAlgorithm 或缺公共无参构造函数 |
BITS017 |
[BitCrcInclude] 字段必须连续且首尾字节对齐 |
BITS018 |
[BitCrc] 结果字段必须字节对齐 |
BITS019 |
[BitFieldConsumeRemaining] 必须是末尾基本元素字段 |
BITS020 |
[BitFieldCount(PadIfShort=true)] 仅支持基本数值/枚举元素 |
BITS021 |
[BitCrc] 不能与 [BitFieldRelated] / converter 共用 |
BITS022 |
动态长度 [BitSerialize] 基类不能含 [BitCrc] |
BITS023 |
[BitField(N)] 把固定位长贴在运行时大小可变的字段上(多态/manual IBitSerializable,警告) |
BITS024 |
RelationKind=ByteLength 下的 converter 必须同时实现序列化/反序列化方向 |
BITS025 |
RelationKind=ByteLength 不能与 [BitFieldCount] / [BitFieldConsumeRemaining] 共用 |
BITS026 |
RelationKind=ByteLength 的静态元素位宽必须是 8 的倍数 |
BITS027 |
RelationKind=ByteLength 只能作用在 List/Array 上 |
BITS028 |
[BitField(Endian = ...)] 仅对字节对齐 + 字节倍数宽度的数值/枚举标量生效 |
BITS029 |
[BitCrc(WholeBuffer = true)] 不能与 [BitCrcInclude] 共用 |
BITS030 |
[BitCrc] 的 SkipHeadBytes / SkipTailBytes 必须 ≥ 0 |
BITS031 |
[BitLengthPrefixString] 仅能用于 string 类型 |
BITS032 |
[BitLengthPrefixString] 的 LengthBits 必须是 8、16 或 32 |
BITS033 |
[BitField(Endian = ...)] 不能跟在运行时偏移可能漂移的动态字段之后 |
BITS034 |
[BitCrc(WholeBuffer = true)] 的 SkipHead/SkipTail 必须把 CRC 自身字节排除在保护区外 |
BITS035 |
[BitCrc(WholeBuffer = true)] 的 CRC 槽位被前置 + 后置混排动态字段挤出保护区时编译期拒绝 |
BITS036 |
[BitLengthPrefixString] 的 MaxBytes 必须 ≥ 0(0 表示不限) |
BITS037 |
[BitField(Endian = ...)] 不能用越界 cast(如 (BitEndian)5) |
BITS038 |
[BitLengthPrefixString] 字段必须从字节边界开始 |
BITS039 |
[BitLengthPrefixString] 不能跟在运行时偏移可能漂移的动态字段之后 |
BITS040 |
嵌入的类型含 [BitField(Endian = ...)] 时父字段必须落在字节对齐位置 |
BITS041 |
嵌入的类型含 [BitField(Endian = ...)] 时父字段不能跟在运行时漂移的动态字段后 |
BITS042 |
[BitFieldValue] 仅适用于数值/枚举标量 |
BITS043 |
[BitFieldValue] 常量超出字段 BitLength 可表示范围 |
BITS044 |
[BitFieldValue] 不能与 [BitCrc] / [BitFieldRelated] / [BitFieldCount] / [BitPoly] 共用 |
BITS045 |
[BitFieldValue] 仅接受整数字面量或枚举成员(不接受 float / string / Type / 数组) |
BITS046 |
[BitLengthFieldString] 仅能用于 string 类型 |
BITS047 |
[BitLengthFieldString] 引用的长度字段未找到,或不是先于本字段声明的 byte/ushort/uint 标量 |
BITS048 |
[BitLengthFieldString] 字段必须从字节边界开始 |
BITS049 |
[BitLengthFieldString] 不能跟在运行时偏移可能漂移的动态字段之后 |
BITS050 |
[BitLengthFieldString] 的 MaxBytes 必须 ≥ 0(0 = 不限) |
BITS051 |
[BitFieldRelated(ByteLength)] 嵌套类型的静态位长必须是 8 的倍数 |
BITS052 |
[BitFieldRelated(ByteLength)] 嵌套类型的长度字段必须先于嵌套字段声明 |
BITS063 |
强类型 Converter 的 TWire 位宽小于字段 BitLength |
例如,以下代码会触发 BITS006 编译错误:
// 错误:Point 未标记 [BitSerialize]
public record Point
{
[BitField(8)] public byte X { get; set; }
}
[BitSerialize]
public partial record Frame
{
[BitField] public Point Position { get; set; } // BITS006: 'Point' 必须标记 [BitSerialize]
}
修复方式:为 Point 添加 [BitSerialize] 和 partial。
Attribute 参考
| 特性 | 说明 |
|---|---|
[BitSerialize] |
标记类型参与位序列化(类型必须同时声明为 partial) |
[BitField(n)] |
声明字段参与序列化,n 为位长度(可选,不指定则自动推断) |
[BitField(n, Endian = BitEndian.Big/Little)] |
字段级大小端覆盖(仅字节对齐 + 字节倍数宽度的标量),Inherit(默认)跟随外层序列化器 |
[BitFieldValue(const, Verify = true)] |
钉死常量值:序列化时强制写入、反序列化时校验不一致抛 InvalidDataException |
[BitFieldRelated(name)] |
关联另一个字段(用于 List 计数或多态判别) |
[BitFieldRelated(name, converterType)] |
关联字段并指定值转换器(默认 Count 模式下为列表值转换器) |
[BitFieldRelated(name, RelationKind = ByteLength)] |
关联字段承载集合的字节预算(见按字节预算驱动集合) |
[BitFieldCount(n)] |
指定 List / Array 的固定元素数量 |
[BitFieldCount(n, PadIfShort=true)] |
定长集合,短数据自动补默认值(仅基本元素) |
[BitFieldConsumeRemaining] |
尾部变长集合,读到数据末尾(仅基本元素,必须是末尾字段) |
[BitCrc(typeof(Algo), InitialValue, ValidateOnDeserialize)] |
CRC 结果字段,指定算法 |
[BitCrc(typeof(Algo), WholeBuffer = true, SkipHeadBytes, SkipTailBytes)] |
整 buffer CRC 模式,用 SkipHead/SkipTail 排除帧头/帧尾 |
[BitCrcInclude(nameof(CrcField))] |
标注参与 CRC 计算的字段(与 WholeBuffer = true 互斥) |
[BitPoly(id, type)] |
多态映射:当判别值为 id 时反序列化为 type |
[BitFixedString(n)] |
固定长度字符串(n 字节),不足补 NUL,可选 Encoding 参数 |
[BitFixedString(n, Padding = 0x20)] |
自定义填充字节(如 ASCII 空格 0x20),反序列化时同步 trim |
[BitTerminatedString] |
NUL 终止字符串,动态长度,可选 Encoding 参数 |
[BitLengthPrefixString(lengthBits)] |
长度前缀字符串(lengthBits ∈ {8, 16, 32}),可选 Encoding / MaxBytes 参数 |
[BitLengthFieldString(nameof(LengthField))] |
长度由另一字段承载的字符串,可选 Encoding / MaxBytes;序列化时自动反向回填长度字段 |
[BitIgnore] |
忽略该字段,不参与序列化/反序列化 |
支持的数据类型
- 整数类型:
byte,sbyte,short,ushort,int,uint,long,ulong - 枚举类型(任意底层整数类型)
- 字符串:固定长度(
[BitFixedString])、NUL 终止([BitTerminatedString])、长度前缀([BitLengthPrefixString])、字段引用长度([BitLengthFieldString]) - 嵌套的 BitField 类型(class / struct / record)
- 自定义
IBitSerializable类型 List<T>/T[](T 为上述支持的类型)
API 参考
| 类 | 说明 |
|---|---|
BitSerializerMSB |
MSB 模式序列化器(高位优先,大端字节序) |
BitSerializerLSB |
LSB 模式序列化器(低位优先,小端字节序) |
两者提供完全相同的 API:
// 泛型序列化
byte[] bytes = BitSerializerMSB.Serialize(obj); // 返回 byte[]
BitSerializerMSB.Serialize(obj, spanBuffer); // 写入 Span<byte>
// 泛型反序列化
T result = BitSerializerMSB.Deserialize<T>(bytes); // 从 byte[]
T result = BitSerializerMSB.Deserialize<T>(span); // 从 ReadOnlySpan<byte>
// 非泛型序列化(通过 Type 参数指定类型)
byte[] bytes = BitSerializerMSB.Serialize(obj, typeof(Packet)); // 返回 byte[]
BitSerializerMSB.Serialize(obj, typeof(Packet), spanBuffer); // 写入 Span<byte>
// 非泛型反序列化(通过 Type 参数指定类型,返回 object)
object result = BitSerializerMSB.Deserialize(bytes, typeof(Packet)); // 从 byte[]
object result = BitSerializerMSB.Deserialize(span, typeof(Packet)); // 从 ReadOnlySpan<byte>
将 MSB 替换为 LSB 即可切换为低位优先模式。
非泛型 API 适用于编译期无法确定类型的场景(如通过配置或反射动态确定类型),返回
object,需自行转换为目标类型。
自动发布到 NuGet
仓库内已提供 GitHub Actions 工作流 .github/workflows/publish-nuget.yml,会在推送 v* 标签时自动构建、测试、打包并发布到 nuget.org。
发布前需要完成一次性配置:
- 在
nuget.org创建 API Key,并授予推送对应包的权限。 - 在 GitHub 仓库中打开
Settings→Secrets and variables→Actions。 - 新增仓库机密
NUGET_API_KEY,值填写上一步的 API Key。
之后每次发布:
- 更新项目版本号,或直接使用 tag 作为发布版本。
- 推送形如
v0.7.3的 Git tag。 - GitHub Actions 会自动把包发布到
https://api.nuget.org/v3/index.json。
许可证
MIT License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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. |
-
net10.0
- No dependencies.
-
net8.0
- 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 |
|---|---|---|
| 0.13.1 | 98 | 9/23/2026 |
| 0.13.0 | 133 | 7/24/2026 |
| 0.12.2 | 131 | 5/25/2026 |
| 0.12.1 | 112 | 5/25/2026 |
| 0.12.0 | 109 | 5/25/2026 |
| 0.11.0 | 126 | 5/24/2026 |
| 0.10.0 | 112 | 5/24/2026 |
| 0.9.0 | 118 | 4/17/2026 |
| 0.8.2 | 116 | 4/17/2026 |
| 0.8.1 | 120 | 4/17/2026 |
| 0.8.0 | 128 | 4/16/2026 |
| 0.7.3 | 116 | 4/13/2026 |
| 0.7.2 | 116 | 4/10/2026 |
| 0.7.1 | 111 | 4/10/2026 |
| 0.7.0 | 124 | 4/10/2026 |
| 0.6.8 | 119 | 4/5/2026 |
| 0.6.6 | 110 | 4/5/2026 |
| 0.6.5 | 116 | 4/5/2026 |
| 0.6.4 | 119 | 4/4/2026 |
| 0.6.3 | 110 | 4/4/2026 |