#binary-format #run-time #serialization #generator #serializer-deserializer #interfacing #specification

inarybay-runtime

基于图论的二进制格式(反)序列化生成器(运行时)

2个版本

0.1.1 2024年5月18日
0.1.0 2023年11月19日

#1794 in 编码

ISC许可证

17KB
189

这是一个用于在 build.rs 中使用的基于图的二进制格式序列化/反序列化生成器!

这是用于与现有的、外部开发的二进制格式规范进行接口交互。如果您想自动序列化/反序列化自己的数据,请使用Protobuf或Bincode或类似工具,而不是使用这个。

通过避免基于宏的接口,我认为这比替代方案更灵活、更简单易用。

目标

目标

  1. 灵活性,支持人们在使用二进制格式规范时所做的所有疯狂的事情

    我不确定是否能够限制所有格式可以实现的事情。它一开始不会支持所有功能,但它的结构应该是这样的,即新的单个功能不需要大规模重写。

  2. 易于理解,可组合的元素

    API通过构建可逆处理节点的图来简单且灵活。

  3. 精确度

    规范是明确的,因此具有相同规范的二进制表示形式在未来的版本中不会因为不明确而改变。

  4. 安全性

    • 类型、重叠、边界检查
    • 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”这两个词在这里和那里都有。

反序列化从“序列”一侧获取数据,并移动到“rust”一侧,最终到达“rust根”,即要反序列化的对象。

序列化将“rust”一侧的数据(对象)通过多个节点转换,直到达到“序列根”——文件、套接字或任何东西。

节点之间的每个链接都有“序列”和“rust”一侧。

设置

要使用Inarybay,您需要在build.rs中定义一个模式,它将生成常规Rust代码。

您需要两个依赖项

cargo add --build inarybay
cargo add inarybay-runtime

后者包含一些辅助类型和重新发布的依赖项。

定义模式

这里的一些描述是针对反序列化的,但所有内容都是双向的,只是用参考方向描述更容易。

  1. 创建一个模式 let schema = Schema::new()
  2. 创建根作用域 let scope = schema.scope(...)
  3. scope上定义节点,如scope.fixed_rangescope.int等,并按需连接它们
  4. 使用scope.rust_root定义rust根
  5. 使用schema.generate生成代码,并将其写入您想要的文件

关于参数的说明

  • Node - 所有 Node* 类型都有一个 .into() 方法,它将它们转换为 NodeNode 实际上是一个包含所有节点类型的庞大枚举。
  • id - 这些用于生成的反/序列化代码中的变量名,以及用于错误消息和图遍历中的循环识别的唯一标识节点
  • TokenStream - 如果一个参数具有此类型,则表示它想要将某些代码注入到生成的代码中。您可以使用来自quote crate 的 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
      • 自定义节点,使用 ZX

自定义节点在转换期间执行一些复杂处理。

在序列化过程中,变体节点生成三个输出:枚举标签、枚举和 XY 需要在 X 和枚举之间写入,因此有两种选择

  1. 序列化到变量,然后按顺序写入变量
  2. 多次下降到枚举中

选项2允许直接将流写入而无需临时缓冲区,但 XZ 都依赖于自定义节点,因此每次下降都需要重新执行自定义节点的转换,或者以某种方式在枚举下降之间存储每个变体的临时值。

这可能可行,但这将显著更复杂,并且我认为当前的内存使用并不过多。

依赖关系

~135KB