7 个版本
0.2.0 | 2023 年 7 月 30 日 |
---|---|
0.1.5 | 2023 年 6 月 17 日 |
0.1.4 | 2023 年 5 月 25 日 |
0.1.0 | 2023 年 4 月 27 日 |
在 Rust 模式 中排名第 356
每月下载量 10,706 次
在 14 个crate 中使用(10 个直接使用)
30KB
208 行
bilge:最易读的位字段
是的,这又是一个位字段 crate,但请听我说
这比我们之前所拥有的要好。
我想有一个适合 Rust 的设计
- 安全
- 类型模型尽可能多的功能,并不允许错误的使用
- 快速
- 类似于手写的位操作代码
- 简单到复杂
- 直观且易读的基本前端,类似于普通结构体
- 仅逐步引入高级概念
- 提供扩展机制
该库为 无标准库(并在 "nightly"
功能门后面完全 const
)。
有关“为什么”和“如何”的更多解释:请参阅 博客文章 和 Reddit 评论。
警告
我们当前的版本仍然是预 1.0 版本,这意味着没有任何东西是完全稳定的。
但是,构造函数、获取器、设置器和 From/TryFrom 应该保持不变,因为它们的语义非常明确。
nightly 功能在 nightly-2022-11-03
上进行了测试,并且 将不会在新版 nightly 上工作,直到 const_convert 返回。
使用方法
为了使您的生命更轻松
use bilge::prelude::*;
不可失败(From)
您可以直接指定位大小字段,就像普通字段一样
#[bitsize(14)]
struct Register {
header: u4,
body: u7,
footer: Footer,
}
属性 bitsize
生成位字段,而 14
作为一种安全措施,如果您的结构体定义没有声明 14 位,则会发出编译错误。让我们也定义嵌套结构体 Footer
#[bitsize(3)]
#[derive(FromBits)]
struct Footer {
is_last: bool,
code: Code,
}
如您所见,我们添加了 #[derive(FromBits)]
,这对于 Register
的获取器和设置器是必需的。由于 Rust 宏的工作方式(由外向内),它需要放在 #[bitsize]
之下。此外,bool
可以用作一个比特。
Code
是另一种嵌套,这次是一个枚举
#[bitsize(2)]
#[derive(FromBits)]
enum Code { Success, Error, IoError, GoodExample }
现在我们可以构建 Register
let reg1 = Register::new(
u4::new(0b1010),
u7::new(0b010_1010),
Footer::new(true, Code::GoodExample)
);
或者,如果我们向 Register
添加 #[derive(FromBits)]
并想解析原始寄存器值
let mut reg2 = Register::from(u14::new(0b11_1_0101010_1010));
获取和设置字段的方式如下
let header = reg2.header();
reg2.set_footer(Footer::new(false, Code::Success));
也支持任何类型的元组和数组
#[bitsize(32)]
#[derive(FromBits)]
struct InterruptSetEnables([bool; 32]);
这会产生常规的获取器和设置器,但还有元素访问器
let mut ise = InterruptSetEnables::from(0b0000_0000_0000_0000_0000_0000_0001_0000);
let ise5 = ise.val_0_at(4);
ise.set_val_0_at(2, ise5);
assert_eq!(0b0000_0000_0000_0000_0000_0000_0001_0100, ise.value);
根据您正在处理的内容,可能只有枚举值的一部分是清晰的,或者一些值可能被保留。在这种情况下,您可以使用一个回退变体,如下定义
#[bitsize(32)]
#[derive(FromBits, Debug, PartialEq)]
enum Subclass {
Mouse,
Keyboard,
Speakers,
#[fallback]
Reserved,
}
这将把任何未声明的位转换为 Reserved
assert_eq!(Subclass::Reserved, Subclass::from(3));
assert_eq!(Subclass::Reserved, Subclass::from(42));
let num = u32::from(Subclass::from(42));
assert_eq!(3, num);
assert_ne!(42, num);
或者,如果您需要保留确切的数量,请使用
#[fallback]
Reserved(u32),
assert_eq!(Subclass2::Reserved(3), Subclass2::from(3));
assert_eq!(Subclass2::Reserved(42), Subclass2::from(42));
let num = u32::from(Subclass2::from(42));
assert_eq!(42, num);
assert_ne!(3, num);
可能失败(TryFrom)
与结构体不同,枚举不需要声明它们的全部位
#[bitsize(2)]
#[derive(TryFromBits)]
enum Class {
Mobile, Semimobile, /* 0x2 undefined */ Stationary = 0x3
}
这意味着这会工作
let class = Class::try_from(u2::new(2));
assert!(class.is_err());
但我们需要首先在 Class
上声明 #[derive(Debug, PartialEq)]
,因为 assert_eq!
需要这些。
让我们做这件事,并将 Class
用作字段
#[bitsize(8)]
#[derive(TryFromBits)]
struct Device {
reserved: u2,
class: Class,
reserved: u4,
}
这显示了 TryFrom
向上传递。还有一个小的帮助:常用于寄存器的保留字段(reserved
)都可以有相同的名字。
再次尝试打印这个
println!("{:?}", Device::try_from(0b0000_11_00));
println!("{:?}", Device::new(Class::Mobile));
再次,Device
没有实现 Debug
调试位
对于结构体,您需要添加 #[derive(DebugBits)]
以获得这样的输出
Ok(Device { reserved_i: 0, class: Stationary, reserved_ii: 0 })
Device { reserved_i: 0, class: Mobile, reserved_ii: 0 }
对于测试和概述,完整的 Readme 示例代码在 /examples/readme.rs
中。
自定义 -Bits 输出
我们方法的主要优点之一是我们可以将 #[bitsize]
保持得很瘦,将所有其他功能卸载到 derive 宏中。除了上面的 derive 宏之外,您还可以扩展 bilge
,使用您自己的 derive crate 在位字段上工作。一个例子在 /tests/custom_derive.rs
中给出,其实现在 tests/custom_bits
中。
向前和向后兼容性
语法与常规Rust结构体非常相似,原因很简单。
这个库的最终目标是支持将LLVM的可变位宽整数引入Rust,从而允许原生位域。在此之前,bilge正在使用由danlehmann编写的出色的arbitrary-int
crate。
在所有属性展开之后,我们生成的位域只包含一个字段,类似于
struct Register { value: u14 }
这意味着您可以直接修改内部值,但这会破坏类型安全保证(例如,未填充或只读字段)。因此,如果您需要修改整个字段,请使用类型安全的转换u14::from(register)
和Register::from(u14)
。这种内部类型可能会被设置为私有。
有关更多示例和功能概述,请参阅/examples
和/tests
。
替代方案
基准测试、性能、汇编行数
首先,基本基准测试显示,这里提到的所有替代方案(除了deku)都具有大致相同的性能和行数。这包括手动编写的版本。
构建时间
测量该crate的构建时间(包括其依赖项和无依赖项),在我的机器上得到以下这些数字
调试 | 调试单个crate | 发布 | 发布单个crate | |
---|---|---|---|---|
bilge 1.67-nightly | 8 | 1.8 | 6 | 0.8 |
bitbybit 1.69 | 4.5 | 1.3 | 13.5 [^*] | 9.5 [^*] |
modular-bitfield 1.69 | 8 | 2.2 | 7.2 | 1.6 |
[^*]: 这只是rustc的一个奇怪回归或我的设置或其他原因,不具有代表性。
这是使用以下命令测量的:cargo clean && cargo build [--release] --quiet --timings
。当然,还需要测量示例项目中的实际代码生成时间。
手动实现
Rust中位域的常见手动实现模式类似于benches/compared/handmade.rs,有时也会在字段偏移量周围抛出很多常量。这种方法的缺点是
- 可读性差
- 偏移量、转换或掩码错误可能会被忽视
- 位操作、移位和掩码在所有地方都进行了,这与位域不同
- 初学者会受到影响,尽管我认为即使是高级别的人也会,因为它们更像:“为什么我们需要学习和调试位操作,如果我们可以通过使用结构体来实现大部分呢?”
- 重新实现不同类型的不可靠的嵌套结构体枚举元组数组字段访问可能不是那么有趣
modular-bitfield
经常使用且非常鼓舞人心的modular-bitfield
有几个问题
- 它未维护,并且结构有些奇怪
- 构造函数使用构建器模式
- 如果有很多字段,则会使用户代码难以阅读
- 可能会意外地留下未初始化的项
from_bytes
可能会接受无效的参数,这使得内部验证变得内外不分- modular-bitfield 流:
u16
->PackedData::from_bytes([u16])
->PackedData::status_or_err()?
- 需要在每次访问时检查
Err
- 添加后缀
_or_err
的重复获取器和设置器 - 重新发明了
From<u16>
/TryFrom<u16>
作为一种混合类型
- 需要在每次访问时检查
- 细节:通常以类型系统为中心的流程:
u16
->PackedData::try_from(u16)?
->PackedData::status()
- 只需工作,无需在访问时检查任何内容
- 有关此内容的更多通用信息:解析,不要验证
- modular-bitfield 流:
- 大宏定义
- 功能强大,但对模块化位字段开发者的可读性较低
- 需要在其自身中覆盖许多 derive,例如
impl Debug
(其他位字段包也这样做)- 细节:通过为
-Bits
-derive 提供一种作用域来解决这个问题
- 细节:通过为
和实现差异
- 底层类型是字节数组
- 对于大于 u128 的位字段可能很有用
- 细节:如果您的位字段大于 u128,通常可以将它们拆分为多个原始大小(如 u64)的位字段,并将这些字段放入不是位字段的父结构中
- 对于大于 u128 的位字段可能很有用
尽管如此,modular-bitfield 还是相当不错,我着手构建一些与其相当或可能更好的东西。告诉我我可以改进的地方,我会尝试。
逐位
受同一软件包启发的库之一是 bitbybit
,它具有更高的可读性和更新的内容。实际上,我甚至帮助并仍在帮助那个项目。然而,在研究他们的代码并进行实验后,我意识到它需要进行重大更改才能实现我所期望的功能和结构。
实现差异(截至 2023 年 4 月 26 日)
- 它可以执行只读/只写、数组步长以及重复多个字段的相同位
- 细节:一旦有人需要,这些功能将会添加
- 冗余的位偏移量指定,这可能有助于或打扰,就像 bilge 的
reserved
字段一样,可能会帮助或打扰
deku
在查看了大量 Crates.io 上的位字段库后,我没有找到 deku
。我仍然在这里提到它,因为它使用了非常有趣的库(bitvec)。目前(截至 2023 年 4 月 26 日),由于 API 的一部分不是 const
,它生成的汇编代码更多,运行时间更长。我已经在他们仓库上就这一点提交了一个问题。
其他大多数
除此之外,许多位字段库试图模仿或看起来像 C 位字段,尽管许多人讨厌它们。我认为大多数初学者会想到使用基本原语(如 u1、u2 等)来指定位。这也为在这些原语上进行计算和转换开辟了一些可能性。
类似的情况也可以说关于 bitflags
,在这个模型中,它可以转换为具有布尔值和枚举的简单结构。
基本上,bilge
尝试将位操作、移位和掩码转换为更广泛认识的概念,如结构访问。
关于名称:bilge 是船的“最低”部分之一,没有其他含义:)
依赖关系
~0.8–1.3MB
~29K SLoC