二进制规范序列化(BCS)

二进制规范序列化(BCS)是一种用于结构化数据的二进制编码格式。最初在Diem中设计,现在已成为Move语言的标准序列化格式。BCS简单、高效、确定性强,并且容易在任何编程语言中实现。

完整的格式规范可以在BCS仓库中找到。

格式

BCS是一种支持无符号整数(最高256位)、选项、布尔值、单位(空值)、定长和变长序列以及映射的二进制格式。该格式设计为确定性,即相同的数据总是会序列化为相同的字节。

“BCS不是自描述格式。因此,要反序列化消息,必须预先知道消息类型和布局。”——来自README

整数以小端格式存储,变长整数使用变长编码方案。序列前缀为其长度(ULEB128),枚举存储为变体的索引加数据,映射存储为有序的键值对序列。

结构体被视为字段的序列,字段按定义顺序序列化,使用与顶层数据相同的规则。

使用BCS

Sui框架包含sui::bcs模块,用于编码和解码数据。编码函数是虚拟机的原生函数,解码函数在Move中实现。

编码

要编码数据,可以使用bcs::to_bytes函数,将数据引用转换为字节向量。该函数支持编码任何类型,包括结构体。

// 文件: move-stdlib/sources/bcs.move
public native fun to_bytes<T>(t: &T): vector<u8>;

以下示例展示了如何使用BCS编码结构体。to_bytes函数可以接收任何值并将其编码为字节向量。

use sui::bcs;

// 0x01 - a single byte with value 1 (or 0 for false)
let bool_bytes = bcs::to_bytes(&true);
// 0x2a - just a single byte
let u8_bytes = bcs::to_bytes(&42u8);
// 0x2a00000000000000 - 8 bytes
let u64_bytes = bcs::to_bytes(&42u64);
// address is a fixed sequence of 32 bytes
// 0x0000000000000000000000000000000000000000000000000000000000000002
let addr = bcs::to_bytes(&@sui);

编码结构体

结构体的编码类似于简单类型。以下是如何使用BCS编码结构体:

let data = CustomData {
    num: 42,
    string: b"hello, world!".to_string(),
    value: true
};

let struct_bytes = bcs::to_bytes(&data);

let mut custom_bytes = vector[];
custom_bytes.append(bcs::to_bytes(&42u8));
custom_bytes.append(bcs::to_bytes(&b"hello, world!".to_string()));
custom_bytes.append(bcs::to_bytes(&true));

// struct is just a sequence of fields, so the bytes should be the same!
assert!(&struct_bytes == &custom_bytes, 0);

解码

由于BCS不是自描述的且Move是静态类型的,解码需要预先了解数据类型。sui::bcs模块提供了各种函数来帮助这个过程。

包装API

BCS在Move中实现为一个包装器。解码器按值接收字节,然后调用不同的解码函数(以peel_*为前缀)来“剥离”数据。数据从字节中分离,剩余字节保存在包装器中,直到调用into_remainder_bytes函数。

use sui::bcs;

// BCS instance should always be declared as mutable
let mut bcs = bcs::new(x"010000000000000000");

// Same bytes can be read differently, for example: Option<u64>
let value: Option<u64> = bcs.peel_option_u64();

assert!(value.is_some(), 0);
assert!(value.borrow() == &0, 1);

let remainder = bcs.into_remainder_bytes();

assert!(remainder.length() == 0, 2);

在解码过程中,通常在单个let语句中使用多个变量。这使代码更具可读性,并有助于避免不必要的数据复制。

let mut bcs = bcs::new(x"0101010F0000000000F00000000000");

// mind the order!!!
// handy way to peel multiple values
let (bool_value, u8_value, u64_value) = (
    bcs.peel_bool(),
    bcs.peel_u8(),
    bcs.peel_u64()
);

解码向量

虽然大多数基本类型有专门的解码函数,但向量需要特殊处理,具体取决于元素类型。对于向量,首先需要解码向量的长度,然后在循环中解码每个元素。

let mut bcs = bcs::new(x"0101010F0000000000F00000000000");

// bcs.peel_vec_length() peels the length of the vector :)
let mut len = bcs.peel_vec_length();
let mut vec = vector[];

// then iterate depending on the data type
while (len > 0) {
    vec.push_back(bcs.peel_u64()); // or any other type
    len = len - 1;
};

assert!(vec.length() == 1, 0);

对于最常见的场景,bcs模块提供了一组基本的函数来解码向量:

  • peel_vec_address(): vector<address>
  • peel_vec_bool(): vector<bool>
  • peel_vec_u8(): vector<u8>
  • peel_vec_u64(): vector<u64>
  • peel_vec_u128(): vector<u128>
  • peel_vec_vec_u8(): vector<vector<u8>> - 字节向量的向量

解码选项

Option 表示为一个0或1个元素的向量。要读取一个选项,可以将其视为向量并检查其长度(第一个字节 - 1或0)。

let mut bcs = bcs::new(x"00");
let is_some = bcs.peel_bool();

assert!(is_some == false, 0);

let mut bcs = bcs::new(x"0101");
let is_some = bcs.peel_bool();
let value = bcs.peel_u8();

assert!(is_some == true, 1);
assert!(value == 1, 2);

如果需要解码自定义类型的选项,请使用上面的代码片段中的方法。

对于最常见的场景,bcs模块提供了一组基本的函数来解码选项:

  • peel_option_address(): Option<address>
  • peel_option_bool(): Option<bool>
  • peel_option_u8(): Option<u8>
  • peel_option_u64(): Option<u64>
  • peel_option_u128(): Option<u128>

解码结构体

结构体按字段逐个解码,没有标准函数可以自动将字节解码为Move结构体,因为这会违反Move的类型系统。相反,需要手动解码每个字段。

// some bytes... 
let mut bcs = bcs::new(x"0101010F0000000000F00000000000");

let (age, is_active, name) = (
    bcs.peel_u8(),
    bcs.peel_bool(),
    bcs.peel_vec_u8().to_string()
);

let user = User { age, is_active, name };

总结

二进制规范序列化是一种高效的结构化数据二进制格式,确保跨平台的一致序列化。Sui框架提供了全面的BCS工具,通过内置函数实现了广泛的功能。