2个版本
0.1.1 | 2024年8月3日 |
---|---|
0.1.0 | 2024年8月2日 |
0.0.0 |
|
#2051 in 编码
每月下载量 341次
68KB
1.5K SLoC
rustbif
- Rust二进制格式
这种二进制序列化格式旨在编码Rust的代数数据类型的一个子集,重点关注保持高度的效率、紧凑性和前后兼容性。数据结构可以遵循以下两个规则进行演化
- 只向结构体的末尾添加新字段。
- 只向枚举中添加新变体。
待办事项
- 修复枚举标签:限制为 正数 至 u32,允许在区分符中使用常量表达式。负数区分符?考虑移除对显式变体区分符的支持。
- 错误(移除 unwraps 和 panics),弄清楚是否需要插入
?;Ok(...)
或直接返回。 - 声明元组的宏。
- [T; N] 不实现 Default,用户也不能实现它。当 "新" 程序读取 "旧" 数据时需要 Default。需要解决这个问题。
- 实现 Arr<bool, N>、Arr<char, N>、Arr<u8, N>、...、Arr<u128, N>、Arr<i8, N>、...、Arr<i128, N>、Arr<f32, N>、Arr<f64, N>、VArr、VArr、VArr、...、VArr、VArr、...、VArr、VArr、VArr; 以及 Arr<T; N>(我们需要这来实现 Default),VArr(这与 Vec 相同,然而,如果需要实现 foreign trait,有一个新类型是好的)。
- 考虑在数据结构中使用 Result<T, E> -- 这是一个透明的(或不是?)包装器,不会出现在线路上,它只会捕获子结构解码的成功/失败。如何编码具有 Err() 的数据?
- 考虑使用属性(默认值)来控制运行时兼容行为:当字段缺失时,返回 Ok(default()) OR Error(); 或者尝试弄清楚类型是否实现了 Default。显式属性可能更好,例如,属性也可以提供默认值。
线格式(元素编码)
每种数据类型都编码为一个元素,可以是以下之一: varint
— 最多 128 位的数据,varbytes
— 可变长度的字节序列,varstruct
— 元素的变长序列(例如 struct,tuple,list),或者 varenum
— 带标签的元素。
-
varint:编码最多 128 位的数据。
- 编码原始类型,如布尔值、字符、整数和浮点数,以及单元枚举。
- 适合 0..=95 范围的小整数使用一个字节编码。
- 不适合 0..=95 范围的较大整数以额外的头部字节
0b1110LLLL
开头,其中LLLL
+ 1(1..=16)表示随后编码数字的字节数。请注意,这是一个变长编码,例如,u32
整数根据其值可以使用 1、2、3 或 4 个字节进行编码。
-
varenum:编码一个带标签的元素(支持 32 位及以下的标签)
- 适合 0..=31 范围的枚举标签使用一个字节编码。
- 不适合 0..=31 范围的较大标签以额外的头部字节
0b111111MM
开头,其中MM
+ 1(1..=4)表示随后编码标签的字节数。 - 标签后面正好跟一个元素。
-
varbytes:编码最多 2^64 字节的变长字节序列。
- 空字节序列编码为
varint
0(线格式不区分这是一个数字 0 或空序列)。 - 长度为 1..=64 的字节序列以头部字节
0b10LLLLLL
开头,后面跟字节,其中LLLLLL
+ 1 是字节序列的长度(1..=64)。 - 较长的字节序列使用头部字节
0b11110MMM
,后面跟最多MMM
+ 1(1..=8)个 varint 字节编码序列的长度,然后是序列本身。
- 空字节序列编码为
-
varstruct:编码最多 2^32 个元素的变长元素序列。
- 空元素序列编码为
varint
0(线格式不区分这是一个数字 0 或空序列)。 - 长度为 1..=32 的元素序列以
0b110LLLLL
开头,后面跟LLLLL
+ 1(1..=32)个元素。 - 较长的元素序列使用头部字节
0b111110MM
,后面跟最多MM
+ 1(1..=4)个 varint 字节编码序列的长度,然后是元素。
- 空元素序列编码为
支持的 Rust 数据类型及其编码
-
原始类型:
u8
,u16
,u32
,u64
,u128
:序列化为 LE 字节,然后编码为varint
。i8
,i16
,i32
,i64
,i128
:使用 zigzag 编码转换为无符号整数(见下文),然后编码为无符号整数。f32
,f64
:序列化为 BE 字节,然后使用varint
编码。bool
:转换为u8
(false:0,true:1),然后编码为u8
。char
:转换为u32
,然后编码为u32
。
-
不透明数组
String
:编码为varbyte
。ByteVec
(与VArr<u8>
相同):编码为varbyte
。Arr<bool, N>
,Arr<char, N>
,Arr<u8, N>
,...,Arr<u128, N>
,Arr<i8, N>
,...,Arr<i128, N>
,Arr<f32, N>
,Arr<f64, N>
,VArr<bool>
,VArr<char>
,VArr<u8>
,...,VArr<u128>
,VArr<i8>
,...,VArr<i128>
,VArr<f32>
,VArr<f64>
:编码为varbyte
,数组数据使用对应原始数据类型的固定大小LE顺序进行编码。
-
泛型数组:
[T; N]
:编码为varstruct
。Vec<T>
:编码为varstruct
。
-
元组、结构体和枚举:
- 元组
()
,(T1,)
,(T1, T2, ..., T32)
:编码为varstruct
。 - 元组结构体
Struct(T1, ..., TN)
:编码为(T1, ..., TN)
。 - 具有命名字段的结构体
Struct{ f1: T1, ..., fN: TN }
:编码为(T1, ..., TN)
。 - 具有变体的枚举
Enum{ V1 = d1, ..., Vi(T1, ..., TM) = di, ..., VN = dN }
:当变体为单一变体时,变体i
以varint
编码。否则,以varenum
编码,即枚举标签后跟变体结构体。
- 元组
示例
如果我们编码以下数据结构
(
SampleEnum::B {
a: 'A',
b: SampleStruct {
a: String::from("hello, world!"),
b: 15,
},
},
(),
)
其中结构体和枚举定义如下
#[derive(Debug, Default, Encode, Decode)]
struct SampleStruct {
a: String,
b: i32,
}
#[repr(u8)]
#[derive(Debug, Default, Encode, Decode)]
enum SampleEnum {
#[default]
None,
A(String) = 10,
B {
a: char,
b: SampleStruct,
} = 20,
}
我们将生成以下二进制代码
c1 74 c1 41 c1 8c 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 1e 00
其中
0xc1 0b11000001 - varstruct tuple, its 2 elements follow
0x74 0b01110100 - varenum with tag 20, its 1 element follow
0xc1 0b11000001 - varstruct struct "SampleEnum::B", its 2 fields follow
0x41 - field a: varint encoded char 'A'
0xc1 0b11000001 - field b: varstruct struct "SampleStruct", its 2 fields follow
0x8c 0b10001100 - field a: varbyte string, its 13 bytes follow
0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21 - "hello, world!"
0x1e - zigzag encoded integer 15
0x00 - empty varstruct encoded as 0
之字形编码
之字形编码将一个有符号整数编码为一个无符号整数。它是通过从零开始计数,交替表示正数和负数来实现的。
要编码任何有符号整数 x
,其表示长度(以位为单位)为 n
,公式如下
(x >> n - 1) ^ (x << 1)
注意:操作符 ^
是按位异或。
要将之字形编码的无符号整数 x
转换为其解码的有符号对应值,公式如下
(x >>> 1) ^ -(x & 1)
注意:操作符 >>>
表示逻辑右移,而不是算术右移。在 Rust 中,无符号整数类型实现右移操作符为逻辑而不是算术。因此,Rust 中的公式简化为 (x >> 1) ^ -(x & 1)
。
处理数据结构演变和兼容性
此编码格式不是完全自描述的,这意味着不能仅从二进制格式中重建原始数据结构。但是,它包含足够的信息来支持在特定约束下实现自动向前和向后兼容。
- 新结构体字段仅在末尾附加。
- 仅添加新枚举变体。
当新程序读取旧数据时,将默认值分配给任何新的结构体字段。相反,旧程序读取新数据将跳过未知结构体字段,如果遇到不受支持的枚举新变体,将报告错误。这确保了数据结构随时间演变时的鲁棒性,并消除了对数据结构版本管理的繁琐管理。
依赖项
~0.4–0.9MB
~20K SLoC