#位域 # #寄存器 # #位域 #bilge

bilge-impl

将位宽类型使用得像 Rust 的一个特性一样

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 日

#35#位域

Download history • Rust 包仓库 1610/week @ 2024-03-14 • Rust 包仓库 2419/week @ 2024-03-21 • Rust 包仓库 1396/week @ 2024-03-28 • Rust 包仓库 1494/week @ 2024-04-04 • Rust 包仓库 1653/week @ 2024-04-11 • Rust 包仓库 2370/week @ 2024-04-18 • Rust 包仓库 3530/week @ 2024-04-25 • Rust 包仓库 2504/week @ 2024-05-02 • Rust 包仓库 1860/week @ 2024-05-09 • Rust 包仓库 2744/week @ 2024-05-16 • Rust 包仓库 2339/week @ 2024-05-23 • Rust 包仓库 2119/week @ 2024-05-30 • Rust 包仓库 2245/week @ 2024-06-06 • Rust 包仓库 2720/week @ 2024-06-13 • Rust 包仓库 3271/week @ 2024-06-20 • Rust 包仓库 2236/week @ 2024-06-27 • Rust 包仓库

10,711 每月下载量
15 个 crate 中使用 (通过 bilge)

MIT/Apache

96KB
1.5K SLoC

bilge:最易读的位域

crates.io docs.rs loc

是的,这又是一个位域 crate,但请听我说

这比我们之前拥有的东西要好。

我想有一个适合 Rust 的设计

  • 安全
    • 类型模型尽可能多的功能,并防止错误使用
  • 快速
    • 就像手写的位操作代码一样
  • 从简单到复杂
    • 直观且易读的基本前端,就像普通的结构体一样
    • 仅逐步引入高级概念
    • 提供扩展机制

该库是 no-std(并在 "nightly" 特性门后面完全 const)。

有关“为什么”和“如何”的更多解释,请参阅 博客文章Reddit 评论

警告

我们当前的版本仍然是预 1.0 版本,这意味着没有任何东西是完全稳定的。

但是,构造函数、获取器、设置器和 From/TryFrom 应该保持不变,因为它们的语义非常明确。

nightly 特性已在 nightly-2022-11-03 上进行测试,并且 将在 const_convert 返回之前在新版 nightly 上无法工作

使用方法

让您的生命变得更简单

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 正在向上传播。还有一个小的帮助:保留字段(通常在寄存器中使用)可以具有相同的名称。

再次尝试打印这个

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 宏之外,您还可以通过使用自己的 derive crate 来扩展 bilge,这些 crate 专注于位字段。一个示例可以在 /tests/custom_derive.rs 中找到,其实现位于 tests/custom_bits

向后和向前兼容性

语法保持与常规 Rust 结构体非常相似,原因很简单

本库的最终目标是支持在Rust中采用LLVM的任意位宽整数,从而允许Rust本地使用位字段。在此之前,bilge正在使用danlehmann的神奇crate arbitrary-int

在所有属性展开后,我们生成的位字段只包含一个字段,大致如下

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()
  • 宏很大
    • 功能强大,但对模块化位域的开发者来说可读性较低
    • 需要覆盖很多衍生内容,例如 impl Debug(其他位域库也这样做)
      • 细节:通过为 -Bits-衍生提供一种作用域来解决此问题

和实现差异

  • 基础类型是字节数组
    • 对于大于 u128 的位域可能很有用
      • 细节:如果您的位域大于 u128,通常可以将它们拆分成多个原始大小(如 u64)的位域,并将这些位域放在一个不是位域的父结构体中

尽管如此,modular-bitfield 还是很不错,我最初的目标是构建一个与其相当或更好的库。告诉我我可以做得更好的地方,我会尝试。

按位

受同一库启发的库之一是 bitbybit,它具有更高的可读性和更新的内容。实际上,我甚至帮助并仍在帮助这个项目。但是,在尝试并修改他们的代码后,我意识到它需要为我的功能和结构进行重大更改。

实现差异(截至 2023 年 4 月 26 日)

  • 它可以进行只读/只写、数组步长和重复多个字段的相同位
    • 细节:这些将在有人需要的时候添加
  • 多余的位偏移指定,这可能有助于或打扰,就像 bilge 的 reserved 字段一样,可能有助于或打扰

deku

在查看 crates.io 上的大量位域库后,我没有找到 deku。我仍然在这里提到它,因为它使用一个非常有趣的底层库(bitvec)。目前(截至 2023 年 4 月 26 日),由于 API 的一部分不是 const,它生成的汇编代码更多,运行时间更长。我在他们的仓库上打开了一个问题。

其他许多

除此之外,许多位域库试图模仿或看起来像 C 位域,尽管许多人讨厌它们。我认为大多数初学者会有用基本原语(如 u1、u2 等)指定位的想法。这也为这些原语上的计算和转换打开了一些可能性。

可以说,bitflags 也可以这样,在这个模型中,它可以变成简单的具有布尔值和枚举的 struct。

基本上,bilge 尝试将位操作、移位和掩码转换为更广泛认知的结构访问等概念。

关于名称:舱底是船的“最低”部分之一,没有其他含义:)

依赖关系

~0.8–1.3MB
~28K SLoC