123次发布
0.0.123 | 2024年7月11日 |
---|---|
0.0.121 | 2024年4月28日 |
0.0.108 | 2024年3月30日 |
0.0.94 | 2023年12月12日 |
0.0.37 | 2022年5月27日 |
#83 in 编码
5,464 每月下载量
在 26 个crate中使用 (16 直接使用)
1MB
22K SLoC
musli
卓越的性能,毫无妥协[^1]!
Müsli是一个灵活、快速且通用的Rust二进制序列化框架,类似于 serde
。
它提供了一组 格式,每个格式都有其自己的详细文档化的特性和权衡。每个以字节为单位的序列化方法,包括转义格式如 musli::json
都提供了完整的 #[no_std]
支持,无论是带 alloc
还是无 alloc
。还有一个特别方便的组件,提供低级别的简洁 零拷贝序列化。
[^1]: 如同Müsli应该能做你需要的一切,甚至更多。
概述
- 查看
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 {
/* .. fields .. */
}
注意默认情况下,字段通过其数字索引进行识别,如果它们被重新排序,则该索引会发生变化。通过配置
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,
}
fn with_musli(storage: &Storage) -> Result<[u8; 8]> {
let mut array = [0; 8];
ENCODING.encode(&mut array[..], storage)?;
Ok(array)
}
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
特征对此是无关紧要的。与Rust类型的兼容性完全通过结合使用Encode
和Decode
特征以及模式来处理。
我们使用GATs来提供更易于使用的抽象。当serde设计时,GATs是不可用的。
一切都是Decoder
或Encoder
。因此,字段名称不仅限于字符串或索引,如果需要,还可以命名到任意类型。
仅在需要时使用访问者。 serde
在反序列化时完全使用访问者,对应的方法被视为对底层格式的“提示”。然后反序列化器可以自由调用访问者上的任何方法,具体取决于底层格式实际包含的内容。在Müsli中,我们将其颠倒过来。如果调用者想要解码任意类型,它将调用decode_any
。格式可以发出适当的底层类型信号,或者调用Visitor::visit_unknown
告诉实现者它无法访问类型信息。
我们发明了一种名为模式编码的编码方法,允许相同的Rust类型以多种不同的方式编码,同时对编码方式有更大的控制。默认情况下,我们包含了Binary
和Text
模式,为二进制和基于文本的格式提供了合理的默认值。
Müsli从底层完全支持无标准库和无分配,不牺牲功能,使用安全高效的范围分配。
我们支持解码时的详细跟踪,以极大地改善对错误发生位置的诊断。
格式
格式目前通过支持不同程度的升级稳定性来区分。一个完全升级稳定的编码格式必须能够容忍一个模型可以添加字段,而较旧版本的模型应该能够忽略这些字段。
部分升级稳定性仍然有用,例如以下musli::storage
格式,因为从存储中读取只需要解码是升级稳定的。所以,如果用#[musli(default)]
正确管理,就不会导致任何读者看到未知字段。
可用的格式及其功能如下
reorder |
missing |
unknown |
self |
|
---|---|---|---|---|
musli::storage #[musli(packed)] |
✗ | ✗ | ✗ | ✗ |
musli::storage |
✔ | ✔ | ✗ | ✗ |
musli::wire |
✔ | ✔ | ✔ | ✗ |
musli::descriptive |
✔ | ✔ | ✔ | ✔ |
musli::json [^json] |
✔ | ✔ | ✔ | ✔ |
reorder
确定字段是否必须按照在它们的类型中指定的顺序出现。在这样一个类型中重新排序字段会导致某种未知但安全的行为。这仅适用于客户端数据模型严格同步的通信。
missing
确定是否可以通过类似Option<T>
的东西处理缺失的字段。这适用于磁盘存储,因为它意味着可以随着模式的发展添加新的可选字段。
unknown
确定格式是否可以跳过未知字段。这适用于网络通信。此时你已达到升级稳定性。这里可以进行一定程度的自省,因为序列化格式必须包含足够关于字段的信息,以便知道要跳过什么,这通常允许对基本类型进行推理。
self
决定格式是否是自描述的。允许从其序列化状态完全重建数据结构。这些格式不需要模型来解码,并且可以转换为和从动态容器,如 musli::value
进行检查。这样的格式还允许进行类型转换,以便有符号数字如果它适合目标类型,可以正确地读取为无符号数字。
对于您删除的每个功能,格式变得更紧凑和高效。例如,使用 musli::storage
的 #[musli(packed)]
与 bincode
大约一样紧凑,而 musli::wire
的大小与类似 protobuf
的大小相当。所有格式主要是以字节为导向的,但有些可能会进行 位打包,如果好处明显的话。
[^json]: 这严格来说不是二进制序列化,但它被实现为一个基准测试,以确保 Müsli 有必要的框架功能来支持它。幸运的是,实现也相当不错!
升级稳定性
以下是一个使用 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
,这保证了将转换为Kind
枚举(其类型为#[repr(u8)]
)的过程尽可能高效。 -
一个很大程度上不安全的
SliceReader
,它为&[u8]
提供了比默认的Reader
实现更高效的读取。因为它可以直接在指针上执行大多数必要的比较。 -
在
musli::json
中对UTF-8处理的某些不安全性,因为我们自己检查UTF-8的有效性(类似于serde_json
)。 -
FixedBytes<N>
,这是一个基于栈的容器,可以操作未初始化的数据。其实现大部分是不安全的。利用它可以在无std环境中执行基于栈的序列化,这在某些情况下很有用。 -
在所有二进制格式中用于解码所有所有权的
String
的某些不安全使用,以支持通过simdutf8
进行更快的字符串处理。禁用simdutf8
功能(默认启用)将移除此不安全的使用。
为确保此库在内存安全性方面正确实现,使用miri
进行了广泛的测试和模糊测试。有关更多信息,请参阅tests
。
依赖项
~0.3–25MB
~350K SLoC