相见恨晚的二进制序列化库:binrw

binrw 是一个针对二进制文件格式的(反)序列化库。不同于 serde 等其他的序列化库 —— serde 关注的是如何更轻松准确地把 Rust structs 映射为 serde 的内部数据模型,而序列化和反序列化这一步(即从 serde 的数据模型到文件格式的映射)需要自行编写后端,当然通用格式的后端基本都是有的,比如 JSON 和 XML。而 binrw 的针对的恰好是序列化/反序列化这一步:它能够让你更轻松地表达 Rust struct 与二进制文件格式里每个字节之间的映射关系,而无需手工编写 Serializer/Deserializer。

我一开始试图找过类似的库,也许是关键词不对、或者是看的不仔细而错过了,导致没有 binrw 可用的时候,我只能手写反序列化代码,比如:

pub fn read_pol(reader: &mut dyn Read) -> Result<PolFile, Box<dyn Error>> {
    let mut magic = [0u8; 4];

    // 读取四字节 magic number
    reader.read_exact(&mut magic)?;

    match magic {
        [0x50, 0x4f, 0x4c, 0x59] => (), // "POLY"
        _ => panic!("Not a valid pol file"),
    }

    // 序列化、反序列化时需要注意字节序
    let some_flag = reader.read_u32::<LittleEndian>()?;
    let mesh_count = reader.read_u32::<LittleEndian>()?;
    let mut geom_node_descs = vec![];

    // 读进一个数组
    for _i in 0..mesh_count {
        let unknown = reader.read_w_vec(26)?;
        geom_node_descs.push(GeomNodeDesc { unknown });
    }

    let mut unknown_count = 0;
    let mut unknown_data = vec![];
    if some_flag > 100 {
        // 只有版本大于某个值时才会有这部分数据
    }

    let mut meshes = vec![];
    for _i in 0..mesh_count {
        meshes.push(read_pol_mesh(reader)?);
    }

    Ok(...)
}

一开始对于我来说倒是问题不大,一是文件格式有限,几种格式写个遍也还好;再就是这些格式的含义都是逆向分析得出的,本身搞清楚他们的意思就很慢,写反序列化代码这部分暂时算不上瓶颈。

到了后来,问题的重要性才开始提高:

  1. 项目预计的领域扩大了。现在不光需要解析读取仙剑三的各类文件,还需要读仙剑四、五、轩辕剑、古剑等诸多游戏的各类文件,每个文件类型都手写一下,会很难受。
  2. 现在只写了反序列化的代码,序列化代码需要再写一份。
  3. 代码不够简洁。现在的代码其实还算清晰、复杂度也不高,但是 boilerplate 很多,维护起来虽不费劲但也有一些成本。文件格式增多会加重这一点。

直到解析古剑奇谭的 GameBryo NIF 格式的模型时,又去网上冲浪了一圈,果然被我找到了 binrw。有了 binrw,我就可以这样说话了:

#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct NiObjectNET {
    name: u32,
    num_extra_data: u32,

    #[br(count = num_extra_data)]
    extra_data: Vec<u32>,

    controller: u32,
}

首先是指定字节序这件事,用 [brw(little)] 就可以了。数组也不在话下,可以直接指定结构体里的成员作为数组长度。再比如:

#[binrw]
#[brw(little)]
#[derive(Debug, Serialize)]
pub struct HAnimPlugin {
    pub header: HAnimHeader,

    #[br(if(header.bone_count > 0))]
    pub unknown: Option<HAnimUnknown>,

    #[br(count = header.bone_count)]
    pub bones: Vec<HAnimBone>,
}

可选字段也可以轻松应对—— 只有在 bone_count > 0 的时候,二进制文件中才会有一段区域描述这个 unknown 结构,否则会直接进入 bones 字段的内容。再如:

#[binrw]
#[brw(little)]
#[brw(import{half_float: bool = false})]
#[derive(Debug, Serialize)]
pub struct Vec3f {
    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub x: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub y: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub z: f32,
}

一个坐标点,里面维度的数值可能是 f32,也可能是 f16。解析每个 field 时可以指定一个自定义的解析函数,同时支持传入一些参数。在这个例子中,传给 float_parser 的参数 half_float 本身是这个结构体的序列化参数,由 #[brw(import{half_float: bool = false})] 指定。使用这个 Vec3f 时,可以在外部传入这个参数:

#[binread]
#[brw(little)]
#[br(import(kf_type: u32))]
#[derive(Debug, Serialize)]
pub struct AnmKeyFrame {
    pub ts: f32,

    #[br(args{half_float: kf_type == 2})]
    pub rot: Vec4f,

    #[br(args{half_float: kf_type == 2})]
    pub pos: Vec3f,

    pub pref_frame_off: u32,

    #[br(if(kf_type == 2))]
    pub kf_offset: Option<Vec3f>,

    #[br(if(kf_type == 2))]
    pub kf_offset_scalar: Option<Vec3f>,
}

每个 binrw 了的结构体会自动生成 read、write 函数,可以直接调用进行反序列化/序列化:

NifModel::read(&mut Cursor::new(...))

现在除了一些 corner case,几乎不需要再手写任何序列化代码了。描述序列化的各种参数都跟字段本身放在一起,一眼望过去就大概知道文件格式是什么样的,序列化/反序列化时做了什么事情,查错维护很方便。

总之世界简洁了不少,绝对算得上相见恨晚。现在已经离不开了。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注