2个版本
0.1.1 | 2024年5月18日 |
---|---|
0.1.0 | 2023年9月8日 |
#1221 in 编码
90 每月下载量
150KB
4K SLoC
这是一个用于在build.rs
中使用的基于图的二进制格式序列化/反序列化生成器!
这是用于与现有的、外部开发的二进制格式规范进行接口交互。如果您想自动序列化/反序列化自己的数据,请使用Protobuf或Bincode或类似工具,而不是使用此工具。
通过避免基于结构宏的接口,我认为这比替代方案更灵活、更易于使用。
目标
目标
-
灵活性,支持人们对二进制格式规范所做的所有疯狂事情
我不确定是否有可能限制所有格式可以实现的事情。这不会一开始就支持所有东西,但它的结构应该是这样的,即新的单个特性不需要进行重大的重写。
-
易于理解,可组合的元素
API通过构建可逆处理节点的图来简单灵活。
-
精度
规范是不含糊的,因此具有相同规范的二进制表示在未来的版本中不会因为不完整规范而改变。
-
安全性
- 类型、重叠、界限检查
- Rust到Rust往返是无损的
非目标
-
方便的二进制格式开发
如果您想要方便,只需使用ProtoBuf或Bincode或一些其他通用、经过实战检验的生成器,这些生成器为您处理低级细节。
这主要用于与现有的、外部开发的规范进行接口交互。
-
极端优化
这是Rust,我不会做疯狂的事情,所以它不会慢。但优化比上面的优先级低。
功能
当前功能
- 基本模式 - 原始类型、整数、数组、枚举
- 序列化位字段
- 对齐
- 顺序/拆分反序列化
- 自定义类型(serde,奇特的字符串编码)
- 同步和异步
- ✨宏和泛型自由✨
想要的功能
- 从文件末尾读取/写入(反向方向)
- Rust位字段
- 固定长度数组
- 分隔符数组
- 解包单字段对象
短期不支持的特性
- 零分配读写
- 全局字节对齐(对齐相对于当前对象,但在未对齐对象中对齐将是不对齐的)
示例
一个最小版本的数据容器。
build.rs:
use std::{
path::PathBuf,
env,
str::FromStr,
fs::{
self,
},
};
use inarybay::scope::Endian;
use quote::quote;
pub fn main() {
println!("cargo:rerun-if-changed=build.rs");
let root = PathBuf::from_str(&env::var("CARGO_MANIFEST_DIR").unwrap()).unwrap();
let schema = inarybay::schema::Schema::new();
{
let scope = schema.scope("scope", inarybay::schema::GenerateConfig {
read: true,
write: true,
..Default::default()
});
let version = scope.int("version_int", scope.fixed_range("version_bytes", 2), Endian::Big, false);
let body = scope.remaining_bytes("data_bytes");
let object = scope.object("obj", "Versioned");
{
object.add_type_attrs(quote!(#[derive(Clone, Debug, PartialEq)]));
object.field("version", version);
object.field("body", body);
}
scope.rust_root(object);
}
fs::write(root.join("src/versioned.rs"), schema.generate().as_bytes()).unwrap();
}
然后像这样使用
main.rs
use std::fs::File;
use crate::versioned::versioned_read;
pub fn main() {
let mut f = File::open("x.jpg.ver").unwrap();
let v = versioned_read(&mut f).unwrap();
println!("x.jpg.ver is {}", v.version);
println!("x.jpg.ver length is {}", v.body.len());
}
指南
术语:serial-rust 轴
“serial”和“rust”这两个词在这里那里散落。
反序列化从“serial”端获取数据,并移动到“rust”端,最终到达“rust root”,即要反序列化的对象。
序列化将“rust”端数据(即对象)通过多个节点进行转换,直到达到“serial root”——文件、套接字或任何东西。
节点之间的每个链接都有一个“serial”和“rust”端。
设置
要使用 Inarybay,您需要在 build.rs
中定义一个模式,它将生成正常的 rust 代码。
您需要两个依赖项
cargo add --build inarybay
cargo add inarybay-runtime
后者包含一些辅助类型和重新发布的依赖项。
定义模式
这里的一些描述是面向反序列化的,但一切都是双向的,只是更容易用参考方向来描述。
- 创建一个模式
let schema = Schema::new()
- 创建一个根作用域
let scope = schema.scope(...)
- 在
scope
上定义节点,如scope.fixed_range
、scope.int
等,边走边连接它们 - 使用
scope.rust_root
定义 rust 根 - 使用
schema.generate
生成代码,并将其写入您想要的文件
关于参数的说明
Node
- 所有Node*
类型都有一个.into()
,它将它们转换为Node
。Node
基本上是一个包含所有节点类型的大枚举。id
- 这些用于生成反/序列化代码中的变量名,以及用于错误消息和图遍历期间的循环识别的唯一标识节点TokenStream
- 如果一个参数具有此类型,则表示它想要一些将被注入到生成的代码中的代码。您可以使用来自 quotecrate 的quote!()
或quote!{}
(等效,使用您喜欢的任何括号)生成代码。代码可以是类型(如quote!(my::special::Type)
)、表达式(quote!(#source * 33)
)或多个语句,具体取决于函数的要求。
故障排除
-
错误行号
构建脚本使用恐慌来传达构建错误。默认情况下在
build.rs
中,这些错误没有行号——要获取行号,请CARGO_PROFILE_DEV_BUILD_OVERRIDE_DEBUG=true RUST_BACKTRACE=1 cargo build
-
类型不匹配
使用变量指定常量时,例如使用
quote!
,如quote!(#i)
,quote!
会追加字面类型后缀。在传递给quote!
之前,您可能需要将其转换为正确的数字类型以匹配所使用数据的类型。在未来的更新中,这可能会变得更加类型安全。
设计和实现
反/序列化是通过依赖图遍历完成的 - 在任何依赖项之前都会遍历所有依赖项。
对于序列化,图根是文件/套接字/等等 - 它依赖于每个序列段,每个序列段依赖于字段的单独数据序列化。后续段依赖于早期段,因此早期段先写入。
对于反序列化,图根是根 Rust 类型,如 Rust 结构体 - 根依赖于要读取的每个字段,它依赖于数据转换,它依赖于正在读取的段。同样,与序列化一样,每个序列段依赖于前一个序列段,因此该段在读取下一个段之前读取。
嵌套
嵌套对象,如数组和枚举,被视为具有内部自己的图的单个节点。我觉得这应该可以在单个图中统一,但我还没有想出如何做到这一点。
写入和内存使用
目前,每个节点在写入时会为其输出分配内存。我希望直接写入输出流而不分配额外的缓冲区,但到目前为止,这会使得以下场景变得困难。考虑以下图
- 序列化枚举标记
- 序列化
X
- 序列化
Y
- 序列化枚举
- 变体
- 序列化整数
Z
- 自定义节点,使用
Z
和X
- 序列化整数
- 变体
自定义节点在转换期间执行一些复杂的处理。
在序列化过程中,变体节点生成三个输出:枚举标记、枚举和 X
。需要在 X
和枚举之间写入 Y
,所以有两种选择
- 序列化到变量中,然后按顺序写入变量
- 多次进入枚举
第二种方法将允许直接写入流而没有临时缓冲区,但 X
和 Z
都依赖于自定义节点,因此每次下降都需要重新执行自定义节点的转换,或者以某种方式在下降到枚举之间存储每个变体的临时值。
这可能可行,但会显著更复杂,并且我认为目前的内存使用并不过多。
依赖关系
~13-24MB
~372K SLoC