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(...) }
一开始对于我来说倒是问题不大,一是文件格式有限,几种格式写个遍也还好;再就是这些格式的含义都是逆向分析得出的,本身搞清楚他们的意思就很慢,写反序列化代码这部分暂时算不上瓶颈。
到了后来,问题的重要性才开始提高:
- 项目预计的领域扩大了。现在不光需要解析读取仙剑三的各类文件,还需要读仙剑四、五、轩辕剑、古剑等诸多游戏的各类文件,每个文件类型都手写一下,会很难受。
- 现在只写了反序列化的代码,序列化代码需要再写一份。
- 代码不够简洁。现在的代码其实还算清晰、复杂度也不高,但是 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,几乎不需要再手写任何序列化代码了。描述序列化的各种参数都跟字段本身放在一起,一眼望过去就大概知道文件格式是什么样的,序列化/反序列化时做了什么事情,查错维护很方便。
总之世界简洁了不少,绝对算得上相见恨晚。现在已经离不开了。