Jlzeng.BitSerializer.Generator
0.13.1
dotnet add package Jlzeng.BitSerializer.Generator --version 0.13.1
NuGet\Install-Package Jlzeng.BitSerializer.Generator -Version 0.13.1
<PackageReference Include="Jlzeng.BitSerializer.Generator" Version="0.13.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Jlzeng.BitSerializer.Generator" Version="0.13.1" />
<PackageReference Include="Jlzeng.BitSerializer.Generator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Jlzeng.BitSerializer.Generator --version 0.13.1
#r "nuget: Jlzeng.BitSerializer.Generator, 0.13.1"
#:package Jlzeng.BitSerializer.Generator@0.13.1
#addin nuget:?package=Jlzeng.BitSerializer.Generator&version=0.13.1
#tool nuget:?package=Jlzeng.BitSerializer.Generator&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
Learn more about Target Frameworks and .NET Standard.
This package has 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 | 87 | 9/23/2026 |
| 0.13.0 | 116 | 7/24/2026 |
| 0.12.2 | 106 | 5/25/2026 |
| 0.12.1 | 105 | 5/25/2026 |
| 0.12.0 | 105 | 5/25/2026 |
| 0.11.0 | 108 | 5/24/2026 |
| 0.10.0 | 109 | 5/24/2026 |
| 0.9.0 | 106 | 4/17/2026 |
| 0.8.2 | 107 | 4/17/2026 |
| 0.8.1 | 115 | 4/17/2026 |
| 0.8.0 | 106 | 4/16/2026 |
| 0.7.3 | 111 | 4/13/2026 |
| 0.7.2 | 107 | 4/10/2026 |
| 0.7.1 | 104 | 4/10/2026 |
| 0.7.0 | 110 | 4/10/2026 |
| 0.6.8 | 115 | 4/5/2026 |
| 0.6.6 | 102 | 4/5/2026 |
| 0.6.5 | 104 | 4/5/2026 |
| 0.6.4 | 119 | 4/4/2026 |
| 0.6.3 | 106 | 4/4/2026 |