2个版本

0.1.1 2024年8月3日
0.1.0 2024年8月2日
0.0.0 2024年8月1日

#2051 in 编码

Download history 324/week @ 2024-07-29 17/week @ 2024-08-05

每月下载量 341次

GPL-3.0 许可证

68KB
1.5K SLoC

rustbif - Rust二进制格式

这种二进制序列化格式旨在编码Rust的代数数据类型的一个子集,重点关注保持高度的效率、紧凑性和前后兼容性。数据结构可以遵循以下两个规则进行演化

  1. 只向结构体的末尾添加新字段。
  2. 只向枚举中添加新变体。

待办事项

  • 修复枚举标签:限制为 正数 至 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 — 带标签的元素。

  1. varint:编码最多 128 位的数据。

    • 编码原始类型,如布尔值、字符、整数和浮点数,以及单元枚举。
    • 适合 0..=95 范围的小整数使用一个字节编码。
    • 不适合 0..=95 范围的较大整数以额外的头部字节 0b1110LLLL 开头,其中 LLLL + 1(1..=16)表示随后编码数字的字节数。请注意,这是一个变长编码,例如,u32 整数根据其值可以使用 1、2、3 或 4 个字节进行编码。
  2. varenum:编码一个带标签的元素(支持 32 位及以下的标签)

    • 适合 0..=31 范围的枚举标签使用一个字节编码。
    • 不适合 0..=31 范围的较大标签以额外的头部字节 0b111111MM 开头,其中 MM + 1(1..=4)表示随后编码标签的字节数。
    • 标签后面正好跟一个元素。
  3. varbytes:编码最多 2^64 字节的变长字节序列。

    • 空字节序列编码为 varint 0(线格式不区分这是一个数字 0 或空序列)。
    • 长度为 1..=64 的字节序列以头部字节 0b10LLLLLL 开头,后面跟字节,其中 LLLLLL + 1 是字节序列的长度(1..=64)。
    • 较长的字节序列使用头部字节 0b11110MMM,后面跟最多 MMM + 1(1..=8)个 varint 字节编码序列的长度,然后是序列本身。
  4. varstruct:编码最多 2^32 个元素的变长元素序列。

    • 空元素序列编码为 varint 0(线格式不区分这是一个数字 0 或空序列)。
    • 长度为 1..=32 的元素序列以 0b110LLLLL 开头,后面跟 LLLLL + 1(1..=32)个元素。
    • 较长的元素序列使用头部字节 0b111110MM,后面跟最多 MM + 1(1..=4)个 varint 字节编码序列的长度,然后是元素。

支持的 Rust 数据类型及其编码

  • 原始类型:

    • u8u16u32u64u128:序列化为 LE 字节,然后编码为 varint
    • i8i16i32i64i128:使用 zigzag 编码转换为无符号整数(见下文),然后编码为无符号整数。
    • f32f64:序列化为 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 }:当变体为单一变体时,变体 ivarint 编码。否则,以 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