musli
卓越的性能,没有任何妥协1!
Müsli 是一个灵活、快速且通用的 Rust 二进制序列化框架,与 [serde
] 类似。
它提供了一系列格式,每种格式都有其明确记录的特性和权衡。包括 [musli::json
] 在内的每种面向字节的序列化方法都完全支持 #[no_std]
,无论是否使用 alloc
。还有一个特别出色的组件提供了简单明了的[零拷贝序列化][zerocopy]底层功能。
概览
- 查看 [
derives
] 了解如何实现 [Encode
] 和 [Decode
]。 - 查看 [
data_model
] 了解 Müsli 的抽象数据模型。 - 查看[基准测试]和[大小比较]了解该框架的性能。
- 查看 [
tests
] 了解该库的测试方法。 - 查看 [
musli::serde
] 了解与 [serde
] 的无缝兼容性。您可能还想了解 Müsli 与 serde 的区别。
使用方法
在 Cargo.toml
中添加以下内容,使用您想要的格式:
[dependencies]
musli = { version = "0.0.123", features = ["storage"] }
设计
主要工作由 [Encode
] 和 [Decode
] 派生宏完成,这些宏在 [derives
] 模块中有详细文档。
Müsli 基于实现这些特征的类型所表示的模式进行操作。
use musli::{Encode, Decode};
#[derive(Encode, Decode)]
struct Person {
/* .. 字段 .. */
}
注意 默认情况下,字段由其数字索引标识,如果重新排序字段,索引会发生变化。重命名字段和设置默认命名策略可以通过配置 [
derives
] 来完成。
提供的二进制序列化格式旨在高效且准确地编码 Rust 中可用的每种类型和数据结构。每种格式都有详细记录的权衡,并旨在使用时完全内存安全。
在内部,我们使用"编码"、"编码"和"解码"这些术语,因为它们与 [serde
] 使用的"序列化"、"序列化"和"反序列化"不同,从而使两个库之间的互操作性更加清晰。编码和解码也更具有"二进制序列化"的感觉,这更贴近本框架的重点。
Müsli 的设计原则与 [serde
] 类似。依靠 Rust 强大的特征系统来生成可以在很大程度上被优化掉的代码。最终结果应该与手写的高度优化代码非常相似。
例如,以下两个函数都生成相同的汇编代码(使用 --release
构建):
const OPTIONS: Options = options::new()
.with_integer(Integer::Fixed)
.with_byte_order(ByteOrder::NATIVE)
.build();
const ENCODING: Encoding<OPTIONS> = Encoding::new().with_options();
#[derive(Encode, Decode)]
#[musli(packed)]
pub struct Storage {
left: u32,
right: u32,
}
使用musli的函数:
```rust
fn with_musli(storage: &Storage) -> Result<[u8; 8]> {
let mut array = [0; 8];
ENCODING.encode(&mut array[..], storage)?;
Ok(array)
}
不使用musli的函数:
fn without_musli(storage: &Storage) -> Result<[u8; 8]> {
let mut array = [0; 8];
array[..4].copy_from_slice(&storage.left.to_ne_bytes());
array[4..].copy_from_slice(&storage.right.to_ne_bytes());
Ok(array)
}
Müsli 与 [serde
] 的不同之处
Müsli 的数据模型不直接对应 Rust。没有提供被序列化类型元数据的 serialize_struct_variant
方法。[Encoder
] 和 [Decoder
] trait 对此是不可知的。与 Rust 类型的兼容性完全由 [Encode
] 和 [Decode
] 派生结合模式来处理。
我们使用 GATs 提供更易用的抽象。当 serde 设计时 GATs 还不可用。
所有东西都是 [Decoder
] 或 [Encoder
]。因此字段名不限于字符串或索引,而可以根据需要命名为[任意类型][musli-name-type]。
仅在需要时使用访问器。serde
在反序列化时[完全使用访问器],相应的方法被视为底层格式的"提示"。然后反序列化器可以根据底层格式实际包含的内容自由调用访问器上的任何方法。在 Müsli 中,我们颠倒了这一点。如果调用者想要解码任意类型,它会调用 [decode_any
]。然后格式可以发出适当的底层类型信号,或调用 [Visitor::visit_unknown
] 告诉实现者它无法访问类型信息。
我们发明了模式编码 允许同一个 Rust 类型以多种不同方式编码,对编码方式有更大的控制。默认情况下我们包含 [Binary
] 和 [Text
] 模式,为二进制和基于文本的格式提供合理的默认值。
Müsli 从底层完全支持 [no-std 和 no-alloc],使用安全高效的[作用域分配]而不影响功能。
我们支持[详细跟踪] 解码时可以大大改善出错位置的诊断。
格式
当前格式通过支持不同程度的升级稳定性来区分。完全升级稳定的编码格式必须容忍一个模型可以添加旧版本模型应该能够忽略的字段。
部分升级稳定性仍然有用,就像下面的 [musli::storage
] 格式,因为从存储读取只需要解码是升级稳定的。所以如果使用 #[musli(default)]
正确管理,这永远不会导致任何读取者看到未知字段。
可用格式及其功能如下:
reorder | missing | unknown | self | |
---|---|---|---|---|
[musli::storage ] #[musli(packed)] | ✗ | ✗ | ✗ | ✗ |
[musli::storage ] | ✔ | ✔ | ✗ | ✗ |
[musli::wire ] | ✔ | ✔ | ✔ | ✗ |
[musli::descriptive ] | ✔ | ✔ | ✔ | ✔ |
[musli::json ] 2 | ✔ | ✔ | ✔ | ✔ |
reorder
决定字段是否必须按照在类型中指定的确切顺序出现。在这种类型中重新排序字段会导致某种未知但安全的行为。这只适用于每个客户端的数据模型严格同步的通信。
missing
决定读取是否可以通过类似 Option<T>
的方式处理缺失字段。这适用于磁盘存储,因为它意味着随着模式的演变可以添加新的可选字段。
unknown
决定格式是否可以跳过未知字段。这适用于网络通信。此时你已经达到了升级稳定性。这里可以进行一些级别的内省,因为序列化格式必须包含足够的字段信息来知道要跳过什么,这通常允许对基本类型进行推理。
self
决定格式是否是自描述的。允许从序列化状态完全重建数据结构。这些格式不需要模型来解码,可以与 [musli::value
] 等动态容器相互转换以进行内省。这种格式还允许执行类型强制转换,因此如果符合目标类型,有符号数可以正确读取为无符号数。
每减少一个功能,格式就变得更紧凑高效。例如使用 #[musli(packed)]
的 [musli::storage
] 大约与 [bincode
] 一样紧凑,而 [musli::wire
] 的大小与 [protobuf
] 相当。所有格式主要面向字节,但如果好处明显,有些可能会执行[位压缩]。
升级稳定性
以下是使用 [musli::wire
] 实现完全升级稳定性的示例。Version1
可以从 Version2
的实例中解码,因为它知道如何跳过属于 Version2
的字段。我们还明确地为字段添加 #[musli(name = ..)]
,以确保它们在重新排序时不会改变。
use musli::{Encode, Decode};
#[derive(Debug, PartialEq, Encode, Decode)]
struct Version1 {
#[musli(mode = Binary, name = 0)]
name: String,
}
#[derive(Debug, PartialEq, Encode, Decode)]
struct Version2 {
#[musli(mode = Binary, name = 0)]
name: String,
#[musli(mode = Binary, name = 1)]
#[musli(default)]
age: Option<u32>,
}
let version2 = musli::wire::to_vec(&Version2 {
name: String::from("Aristotle"),
age: Some(61),
})?;
let version1: Version1 = musli::wire::decode(version2.as_slice())?;
以下是使用 [musli::storage
] 对相同数据模型实现部分升级稳定性的示例。注意 Version2
如何从 Version1
解码,但不能反过来,这使其适用于磁盘存储,其中模式可以从旧版本演变到新版本。
let version2 = musli::storage::to_vec(&Version2 {
name: String::from("Aristotle"),
age: Some(61),
})?;
assert!(musli::storage::decode::<_, Version1>(version2.as_slice()).is_err());
let version1 = musli::storage::to_vec(&Version1 {
name: String::from("Aristotle"),
})?;
let version2: Version2 = musli::storage::decode(version1.as_slice())?;
模式
与 [serde
] 相比,在 Müsli 中同一个模型可以以不同的方式序列化。我们支持为单个模型实现不同的模式,而不是要求使用不同的模型。
模式是一种类型参数,它允许根据编码器配置的模式应用不同的属性。模式可以适用于任何musli属性,为您提供了很大的灵活性。
如果未指定模式,实现将应用于所有模式(M
)。如果至少指定了一个模式,它将应用于模型中存在的所有模式和[Binary
]。这样,使用默认模式Binary
的编码应该始终有效。
有关如何配置模式的更多信息,请参阅[derives
]。
以下是一个简单示例,展示如何使用两种模式为单个结构体提供两种完全不同的格式:
use musli::{Decode, Encode};
use musli::json::Encoding;
enum Alt {}
#[derive(Decode, Encode)]
#[musli(mode = Alt, packed)]
#[musli(name_all = "name")]
struct Word<'a> {
text: &'a str,
teineigo: bool,
}
const CONFIG: Encoding = Encoding::new();
const ALT_CONFIG: Encoding<Alt> = Encoding::new().with_mode();
let word = Word {
text: "あります",
teineigo: true,
};
let out = CONFIG.to_string(&word)?;
assert_eq!(out, r#"{"text":"あります","teineigo":true}"#);
let out = ALT_CONFIG.to_string(&word)?;
assert_eq!(out, r#"["あります",true]"#);
不安全性
以下是本crate中使用不安全代码的非详尽列表及其原因:
-
Tag::kind
中的mem::transmute
。它确保转换为#[repr(u8)]
的Kind
枚举尽可能高效。 -
一个主要不安全的
SliceReader
,它提供比&[u8]
的默认Reader
实现更高效的读取。因为它可以直接在指针上执行大部分必要的比较。 -
musli::json
中与UTF-8处理相关的一些不安全性,因为我们内部自己检查UTF-8有效性(类似serde_json
)。 -
FixedBytes<N>
是一个可以操作未初始化数据的基于栈的容器。它的实现大部分是不安全的。通过它可以执行基于栈的序列化,这在no-std环境中很有用。 -
在所有二进制格式中,一些
unsafe
用于拥有所有权的String
解码,以支持通过[simdutf8
]进行更快的字符串处理。禁用simdutf8
功能(默认启用)会移除这些不安全使用。
为确保这个库在内存安全方面的正确实现,使用miri
进行了广泛的测试和模糊测试。更多信息请参见[tests
]。
[此处省略了原文中的链接引用部分]