#二进制编码 #序列化 #无分配 #无标准库

无标准库 musli

Müsli是一个灵活且通用的二进制序列化框架

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 编码

Download history 894/week @ 2024-04-27 341/week @ 2024-05-04 330/week @ 2024-05-11 451/week @ 2024-05-18 666/week @ 2024-05-25 281/week @ 2024-06-01 447/week @ 2024-06-08 700/week @ 2024-06-15 810/week @ 2024-06-22 298/week @ 2024-06-29 513/week @ 2024-07-06 704/week @ 2024-07-13 1689/week @ 2024-07-20 1807/week @ 2024-07-27 1046/week @ 2024-08-03 787/week @ 2024-08-10

5,464 每月下载量
26 个crate中使用 (16 直接使用)

MIT/Apache

1MB
22K SLoC

musli

github crates.io docs.rs build status

卓越的性能,毫无妥协[^1]!

Müsli是一个灵活、快速且通用的Rust二进制序列化框架,类似于 serde

它提供了一组 格式,每个格式都有其自己的详细文档化的特性和权衡。每个以字节为单位的序列化方法,包括转义格式如 musli::json 都提供了完整的 #[no_std] 支持,无论是带 alloc 还是无 alloc。还有一个特别方便的组件,提供低级别的简洁 零拷贝序列化

[^1]: 如同Müsli应该能做你需要的一切,甚至更多。


概述


使用方法

使用您想要的 格式 在您的 Cargo.toml 中添加以下内容

[dependencies]
musli = { version = "0.0.123", features = ["storage"] }

设计

重载工作由EncodeDecode特征实现,这些特征在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方法来提供有关正在序列化类型的元数据。 EncoderDecoder特征对此是无关紧要的。与Rust类型的兼容性完全通过结合使用EncodeDecode特征以及模式来处理。

我们使用GATs来提供更易于使用的抽象。当serde设计时,GATs是不可用的。

一切都是DecoderEncoder。因此,字段名称不仅限于字符串或索引,如果需要,还可以命名到任意类型

仅在需要时使用访问者serde在反序列化时完全使用访问者,对应的方法被视为对底层格式的“提示”。然后反序列化器可以自由调用访问者上的任何方法,具体取决于底层格式实际包含的内容。在Müsli中,我们将其颠倒过来。如果调用者想要解码任意类型,它将调用decode_any。格式可以发出适当的底层类型信号,或者调用Visitor::visit_unknown告诉实现者它无法访问类型信息。

我们发明了一种名为模式编码的编码方法,允许相同的Rust类型以多种不同的方式编码,同时对编码方式有更大的控制。默认情况下,我们包含了BinaryText模式,为二进制和基于文本的格式提供了合理的默认值。

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