16个稳定版本 (4个主要版本)
4.0.2 | 2024年4月14日 |
---|---|
4.0.1 | 2024年2月10日 |
3.3.0 | 2024年1月25日 |
3.2.0 | 2023年8月5日 |
0.4.0 | 2021年5月2日 |
#82 在 编码
每月2,861次下载
用于 18 个crate(7个直接使用)
175KB
2.5K SLoC
binary-layout
binary-layout库允许以类型安全、就地、零拷贝的方式访问结构化二进制数据。您定义一个自定义数据布局,并给它一个二进制数据的切片,然后它可以允许您从二进制数据中读取和写入布局中定义的字段,而无需复制任何数据。它与将数据转换为从#[repr(packed)]
结构体转换,但更安全。
注意,数据不会经过序列化/反序列化或解析步骤。所有访问器直接访问底层数据包。
此crate与#[no_std]
兼容。
示例
use binary_layout::prelude::*;
// See https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol for ICMP packet layout
binary_layout!(icmp_packet, BigEndian, {
packet_type: u8,
code: u8,
checksum: u16,
rest_of_header: [u8; 4],
data_section: [u8], // open ended byte array, matches until the end of the packet
});
fn func(packet_data: &mut [u8]) {
let mut view = icmp_packet::View::new(packet_data);
// read some data
let code: u8 = view.code().read();
// equivalent: let code: u8 = packet_data[1];
// write some data
view.checksum_mut().write(10);
// equivalent: packet_data[2..4].copy_from_slice(&10u16.to_be_bytes());
// access an open ended byte array
let data_section: &[u8] = view.data_section();
// equivalent: let data_section: &[u8] = &packet_data[8..];
// and modify it
view.data_section_mut()[..5].copy_from_slice(&[1, 2, 3, 4, 5]);
// equivalent: packet_data[8..13].copy_from_slice(&[1, 2, 3, 4, 5]);
}
请参阅icmp_packet模块,了解这个binary_layout!宏为你生成的内容。
这个库有什么用呢?
任何需要原地零拷贝访问结构化二进制数据的情况。
- 网络数据包是一个明显的例子
- 文件系统inode
- 如果想要避免显式的序列化和反序列化,可以在文件中存储结构化二进制数据,可能还会与memmap结合使用。
为什么要使用这个库?
- 原地、零拷贝、类型安全的访问你的数据。
- 数据布局在一个中央位置定义,调用点不会意外地使用错误的字段偏移。
- 方便简单的宏DSL定义布局。
- 在布局中定义固定的字节序,确保跨平台兼容性。
- 完全使用安全的Rust编写,没有std::mem::transmute或类似的技巧。
- 常量泛型确保所有偏移计算都在编译时完成。这与内联注释一起,使得这个库无开销。使用它的性能与手动编写代码中的切片访问一样。
- 全面的测试覆盖率。
为什么不使用#[repr(packed)]
?
使用#[repr(packed)]
注解结构体可以提供这个crate的一些特性,即数据字段按照指定的顺序排列,没有填充。但它有一些严重的缺陷,这个库可以解决。
#[repr(packed)]
使用系统字节序,这取决于您是在小端还是大端系统上运行。这个#[repr(packed)]
不是跨平台的。这个库是。#[repr(packed)]
可能会在某些CPU上,当引用未对齐数据时导致未定义的行为。这个库通过不提供任何可以引用未对齐数据的API来避免这种情况。允许原始整数类型未对齐,但它们会被复制,你不能获取它们的引用。唯一可以获取引用的数据类型是字节数组,它们只需要1对齐,这总是很容易满足。
什么时候不应该使用这个库?
- 你需要动态数据结构,例如大小可以改变的列表。这个库只支持静态数据布局(除了布局末尾的开放端字节数组之外)。
- 并非所有布局都适合放入内存,您需要处理数据流。请注意,如果较小的布局包作为较大流的一部分,只要任何一个布局包适合内存,这个crate仍然很有帮助。
替代方案
据我所知,没有其他库提供原地、零拷贝和类型安全的结构化二进制数据访问。但是,如果您不需要直接访问数据,并且可以接受序列化/反序列化步骤,那么外面有很多令人惊叹的库。
APIs
使用 binary_layout! 宏定义布局。基于此类布局,此库提供两种不同的数据访问API
- Field API,它提供了一些免费函数来根据底层存储切片(如上面示例中的
packet_data
)读取/写入数据。此API不包装底层存储数据切片,这意味着您必须将其传递给每个访问者。上面示例中并未使用此API,请参阅 Field 以获取API示例。 - FieldView API,它包装存储数据切片,并将其存储在一个
View
对象中,允许在不需要每次都传递打包数据切片的情况下访问字段。上面示例中使用的是此API。请参阅 FieldView 以获取另一个示例。
支持的字段类型
原始整数类型
对于这些字段,Field API 提供了 FieldReadExt::read、FieldWriteExt::write、FieldCopyAccess::try_read、FieldCopyAccess::try_write,而 FieldView API 提供了 FieldView::read 和 FieldView::write。
原始浮点类型
非零原始整数类型
- NonZeroU8、NonZeroU16、NonZeroU32、NonZeroU64、NonZeroU128
- NonZeroI8、NonZeroI16、NonZeroI32、NonZeroI64、NonZeroI128
读取零值会引发错误。因此,对于这些类型,FieldReadExt::read 和 FieldView::read 都不可用,您需要使用 FieldCopyAccess::try_read 和 FieldView::try_read。
bool、char
bool 和 char 可以使用 bool as u8
和 char as u32
数据类型表示法来支持。
请注意,不仅 0u8
和 1u8
是有效的布尔值,并且并非所有 u32 值都是有效的 Unicode 代码点。读取无效值会引发错误。因此,对于这些类型,FieldReadExt::read 和 FieldView::read 都不可用,您需要使用 FieldCopyAccess::try_read 和 FieldView::try_read。
原始零大小类型 (ZSTs)
ZSTs 既不读取也不写入底层存储,但为它们实现了适当的特质,以支持 derive 宏,这些宏可能需要结构体的所有成员或枚举也支持各种特质。
()
,也称为unit
类型。
固定大小的字节数组:[u8; N]
。
对于这些字段,Field API 提供了 FieldSliceAccess::data、FieldSliceAccess::data_mut,并且 FieldView API 返回一个切片。
开放式的字节数组:[u8]
。
此字段类型只能作为布局的最后一个字段出现,并将匹配直到存储结束的数据。此字段的大小是动态的,取决于数据包数据的大小。对于这些字段,Field API 提供了 FieldSliceAccess::data、FieldSliceAccess::data_mut 和 FieldView API 返回一个切片。
自定义字段类型
只要它们实现了 LayoutAs trait 以定义如何从/到原始类型转换,您就可以定义自己的自定义类型。
数据类型可能在未来得到支持
这些数据类型目前还不受支持,但理论上可以添加,并且可能会在未来的版本中添加。
- 位字段 / bool 存储为 1 位
动态长度的数据类型
这个crate依赖于静态布局,它无法支持动态长度的数据类型。从理论上讲,如果这些类型是布局的最后一个字段,或者它们在数据包中间但具有最大大小并且始终为最大大小保留存储空间,即使它们更小,则可以支持具有动态长度的类型。这样,其后的字段仍然会有一个常量偏移量。
- 或者它们可能是布局的最后一个字段,这是一个已经实现的例子,比如未闭合的字节数组。
- 或者它们可能在数据包的中间,但具有最大大小,并且始终为最大大小保留存储空间,即使它们更小。这样,其后的字段仍然会有一个常量偏移量。
然而,这两者都需要一些努力来实现,并且目前尚不清楚是否会发生(除非有人提交一个PR)。
字符串
对于字符串,请注意,即使是固定大小的UTF-8字符串也因为UTF-8编码而占用可变数量的字节,这带来了所有具有动态长度的数据类型的问题。这就是为什么字符串目前还不受支持的原因。
除了 [u8; N]
之外的大小固定数组
假设我们想有一个 [u32; N]
字段。API不能只是返回一个零拷贝的 &[u32; N]
给调用者,因为这会使用系统字节顺序(即字节序),这可能与数据包布局中定义的字节顺序不同。为了实现跨平台兼容性,我们必须将这些切片包装到我们自己的切片类型中,该类型强制执行正确的字节序,并通过API返回它。这就是为什么尚未实现,但如果您需要此功能,请随时提交PR。
嵌套
可以通过使用 binary_layout! 宏创建的 NestedView
类型将布局嵌套在彼此中,其中一个布局作为另一个布局的字段类型。
示例
use binary_layout::prelude::*;
binary_layout!(icmp_header, BigEndian, {
packet_type: u8,
code: u8,
checksum: u16,
rest_of_header: [u8; 4],
});
binary_layout!(icmp_packet, BigEndian, {
header: icmp_header::NestedView,
data_section: [u8], // open ended byte array, matches until the end of the packet
});
嵌套布局不需要具有相同的字节序。以下内容是从此存储库中 tests/nested.rs
中的完整示例复制的,它展示了如何混合不同的字节序布局
use binary_layout::prelude::*;
use core::convert::TryInto;
binary_layout!(deep_nesting, LittleEndian, {
field1: u16,
});
binary_layout!(header, BigEndian, {
field1: i16,
});
binary_layout!(middle, NativeEndian, {
deep: deep_nesting::NestedView,
field1: u16,
});
binary_layout!(footer, BigEndian, {
field1: u32,
deep: deep_nesting::NestedView,
tail: [u8],
});
binary_layout!(whole, LittleEndian, {
head: header::NestedView,
field1: u64,
mid: middle::NestedView,
field2: u128,
foot: footer::NestedView,
});
许可证:MIT OR Apache-2.0
依赖关系
~125KB