#little-endian #endian #big-endian #byteorder #networking

simple_endian

用于在数据结构中定义字节序,以简化便携式数据结构的处理

12个版本

0.3.2 2024年3月9日
0.3.1 2024年3月4日
0.2.1 2021年9月21日
0.2.0 2020年8月31日
0.1.6 2019年12月30日

#125数据结构

每月21次下载
用于 mrubyedge

MIT 协议

57KB
1K SLoC

simple-endian

自0.3版本起,此库在稳定版Rust上工作。

另一个用于处理Rust中字节序的库。我们使用Rust的类型系统来确保正确的转换,并为处理器类型之间的数据结构便携性提供显式定义字节序的数据类型。它应该相当轻量,并支持 #![no_std]

此crate与其他处理字节序的crate之间的主要区别在于,在此crate中,您根本不需要手动进行转换。您只需处理您正在处理的数据结构适用的字节序,我们尝试提供所需的特性和方法,尽可能以正常的方式执行此操作。

已经有了这样的库吗?

是的,有几个。但我对它们都不完全满意。具体来说,目前大多数库都侧重于提供执行字节序转换的函数。以下是一些例子

byteorder有超过1100万次下载,并且显然是处理Rust中字节序的流行方式。然而,它依赖于程序员编写特定的逻辑来交换字节,并需要以与Rust的正常模式不同的方式访问内存。但实际上,大端和小端值之间的唯一区别是解释。访问它们不应需要截然不同的代码模式。

那么,为什么还要创建另一个呢?

因为我认为更好的方法是将您的字节序定义在数据定义中,而不是在程序的逻辑中,然后尽可能使字节顺序交换变得透明,同时仍然确保正确性。并且因为这与正常Rust数据类型和操作越相似,人们就越有可能一开始就编写可移植的代码和数据结构。

这个crate的哲学是,当您编写数据结构时,您定义字节序,然后使用清晰、命令式的逻辑来修改它,而无需考虑细节或主机字节序。这使得它与那些只提供将 &[u8; 8] 转换为 u64 方法的crate有着根本的不同。

本项目的目标

本crate的目标如下

  1. 安全地提供具有低或无运行时开销的特定字节序类型。当主机架构与指定的字节序匹配时,不应有运行时惩罚,否则应具有非常低的加载和存储惩罚。
  2. 简单、架构无关的声明性语法,确保加载和存储操作正确。
  3. 具有良好的人体工程学使用模式,在不牺牲正确性和安全性或正确性的情况下,最大化清晰性和便利性。
  4. 在编译时生成清晰的类型错误以处理数据的不正确处理。
  5. 正确的字节序应该在声明时确定,并且除非转换为不同的字节序,否则不需要重复。
  6. 支持所有与Rust的内置类型相关的字节序。
  7. 所需的唯一依赖是核心crate。然而,对于测试和基准测试,使用了std crate。

示例

use simple_endian::*;

let foo: u64be = 4.into();    //Stores 4 in foo in big endian.

println!("raw: {:x}, value: {:x}", foo.to_bits(), foo);

输出将取决于您使用的计算机类型。如果您正在运行小端系统,如x86(PC、Mac等),您将看到大端表示被解释为小端,因为它是存储在内存中的。请注意,``.to_bits()` 方法主要用作调试目的,不应经常使用。

这也可以相反操作

use simple_endian::*;

let foo: u64be = 4.into();
let bar = u64::from(foo);

println!("value: {:x}", bar);

如果您愿意,有一个便利的方法,这样您就不需要显式地转换回基本本地类型。

use simple_endian::*;

let foo: u64be = 4.into();
let bar = foo.to_native();

println!("value: {:x}", bar);

类型系统确保在写入之前,本地字节序的值永远不会被写入而不转换为正确的字节序。

let mut foo: u64be = 4.into();
foo = 7;     // Will not compile without .into().

它是如何工作的

该crate的核心围绕一个名为SpecificEndian<T>的特质展开,以及泛型结构体BigEndian<T>LittleEndian<T>。要创建BigEndian<T>LittleEndian<T>结构体,必须实现SpecificEndian。任何实现了SpecificEndian的数据类型,即使它以非传统方式处理字节序,也可以使用该crate中的结构体分配BigEndianLittleEndian变体,主要的限制是它们需要使用相同的底层结构。实际上,u64be只是BigEndian<u64>的类型别名。BigEndian<T>LittleEndian<T>结构体没有增加内存占用,实际上,在大多数情况下,它使用类型T来存储数据。结构体的唯一目的是为了标记它们,以便Rust的类型系统可以强制执行正确的访问。这意味着它可以直接在更大的结构体中使用,然后整个结构体可以写入磁盘,通过网络套接字发送,以及其他使用相同代码的处理器架构之间的共享,而不管主机字节序如何,使用声明性逻辑而不需要任何条件语句。

该crate为Rust中的大多数内置类型提供了SpecificEndian实现,包括

  • 单字节值(i8u8bool),尽管这实际上并没有做什么,但提供了完整性。
  • 多字节整数:u16u32u64u128usizei16i32i64i128isize
  • 浮点数:f32f64

在撰写本文时,唯一没有实现的标准内置类型是char,这是因为char的二进制表示可能会引起panic。通常,您根本不想直接存储char,因此这可能是一个小的限制。

此外,该crate还为它封装的类型提供了一系列有用的特质实现,包括整数类型的布尔逻辑实现,包括bool。这使得大多数布尔逻辑操作可以在不进行任何字节序转换的情况下使用普通运算符执行。但是,您需要使用相同字节序的操作数,如下所示

use simple_endian::*;

let ip: BigEndian::<u32> = 0x0a00000a.into();
let subnet_mask = BigEndian::from(0xff000000u32);

let network = ip & subnet_mask;

println!("value: {:x}", network);

正如您所看到的,网络是通过将IP地址与子网掩码进行掩码运算来计算的,这样程序员几乎不需要考虑转换操作。

或者,您可能希望定义一个具有类型元素的结构体,以便将其作为一个单元移动。

use simple_endian::*;

#[derive(Debug)]
#[repr(C)]
struct NetworkConfig {
    address: BigEndian<u32>,
    mask: BigEndian<u32>,
    network: BigEndian<u32>,
}

let config = NetworkConfig{address: 0x0a00000a.into(), mask: 0xff000000.into(), network: (0x0a00000a & 0xff000000).into()}

println!("value: {:x?}", config);

请注意,println!会将值转换为本地字节序。

最后,这个crate实现了一系列特质,使得大多数基本算术运算符可以在所有类型的大端和小端变体上使用,在适当的情况下,包括浮点数。由于每个操作至少需要一次,通常是两次或更多端序转换,因此这会产生一定的开销。然而,由于这个crate旨在最小化编写可移植代码的成本,它们被提供以减少采用阻力。如果您正在编写对这种开销非常敏感的代码,那么将端序转换为本地端序,执行操作,然后使用.into()或类似的函数将数据存回指定的端序可能是有意义的。但话说回来,这种开销通常很小,Rust的优化器非常好,因此我建议您在采取不舒适的代码编写方法之前先进行一些实际的基准测试。这里实现的特质太多,无法在此列出,因此我建议您查阅文档。或者,您也可以尝试您想做的操作,看看是否可以编译。除非您非常努力,否则它不应该允许您编译出无法正确处理端序的代码。

表示和ABI

您可能会注意到,我们上面使用了#[repr(C)],您可能会想知道为什么。通常情况下,当您编写将直接从某些介质中读取和写入的结构时,您希望写一个具有非常特定布局的struct。Rust的默认ABI并不能保证这一点。因此,这个crate中定义的所有结构都是#[repr(transparent)],如果您计划直接将这些结构写入磁盘或网络,强烈建议您采取某种措施来确保一致的布局,或者以其他方式保证字段存储的顺序。

对类型的操作

除了利用Rust类型系统确保正确使用端序外,这个crate还提供了一系列来自core库的特质实现,允许您在不将它们转换为本地端序类型的情况下直接操作值。在许多情况下,这实际上是一种零成本的功能,因为位操作与端序无关,并且只要您使用其他SpecificEndian类型,直接对它们进行操作就不会产生开销。在需要将端序转换为本地端序的情况下,这个crate将执行转换,并以与输入相同的类型返回值。

功能

尽管这个crate一开始就包含了很多有用的功能,但包含所有这些功能可能会显著增加编译后的代码大小。对于注重大小的应用程序,我建议不要包含所有功能。

默认情况下,这个crate将编译所有支持的功能。虽然这意味着在大多数情况下,您几乎可以做到您想做的任何事情,但从实际的角度来看,这可能会使crate变得相当大。为了避免膨胀,您可能最好在“Cargo.toml”中将default-features = false设置为,并添加您实际需要的功能。

最实用的两个功能可能是控制对大端和小端的支持

  • 大端
  • 小端

其他功能被分为几个类别

  • 操作类型 - 这些功能可以使 SpecificEndian<T> 类型的使用更加便捷,并可以通过避免不必要的转换到和从本地端序,进行一定程度的优化。
    • 位运算
    • 比较
    • 数学运算
    • 负数运算
    • 移位运算
  • 支持在 format 功能中的格式化。
  • 支持不同类型
    • 浮点数实现
    • 整数实现
    • 字节实现

性能

总的来说,端序操作的性能非常快,甚至比本地操作还要快。主要例外是 std::fmt 实现,在某些情况下比默认值慢得多。我愿意接受关于如何提高性能的建议,但在性能关键的环境中,可能值得使用 .to_native() 而不是直接打印封装的类型。

另请参阅

这个crate允许在内存中操作特定端序的结构。它不提供读取或写入这些结构的任何功能,这在大多数情况下可能是必要的。有关该功能,请参阅以下其他crate

没有运行时依赖