Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

十四个实战技巧 🟡

你将学到: 十四个较小的构造正确性技术——从哨兵消除和密封 trait 到 session 类型、Pin、RAII 和 #[must_use]——每个都以几乎零成本消除特定的 bug 类别。

交叉引用: ch02(密封 trait 扩展 ch02)、ch05(typestate builder 扩展 ch05)、ch07(FromStr 扩展 ch07)

十四个实战技巧

八个核心模式(ch02–ch09)涵盖了主要的构造正确性技术。本章收集了十四个较小但高价值的技巧,这些技巧在生产 Rust 代码中反复出现——每个都以零或几乎零成本消除特定类别的 bug。

技巧 1 —— 边界处的哨兵 → Option

硬件协议充满了哨兵值:IPMI 使用 0xFF 表示“传感器不存在“,PCI 使用 0xFFFF 表示“无设备“,SMBIOS 使用 0x00 表示“未知“。如果你将这些哨兵作为普通整数在代码中传递,每个消费者都必须记得检查这个魔术值。如果有一个比较忘记了,就会出现虚幻的 255°C 读数或虚假的 vendor-ID 匹配。

规则: 在最早的解析边界将哨兵转换为 Option,并且仅在序列化边界转换回哨兵。

反模式(来自 pcie_tree/src/lspci.rs)

// 内部携带哨兵——每个比较必须记住
let mut current_vendor_id: u16 = 0xFFFF;
let mut current_device_id: u16 = 0xFFFF;

// ... 稍后,解析静默失败 ...
current_vendor_id = u16::from_str_radix(hex, 16)
    .unwrap_or(0xFFFF);  // 哨兵隐藏了错误

每个接收 current_vendor_id 的函数都必须知道 0xFFFF 是特殊的。如果有人写 if vendor_id == target_id 而没有先检查 0xFFFF,当目标也恰好从错误数据解析为 0xFFFF 时,缺失的设备会静默匹配。

正确模式(来自 nic_sel/src/events.rs)

pub struct ThermalEvent {
    pub record_id: u16,
    pub temperature: Option<u8>,  // 如果传感器报告 0xFF 则为 None
}

impl ThermalEvent {
    pub fn from_raw(record_id: u16, raw_temp: u8) -> Self {
        ThermalEvent {
            record_id,
            temperature: if raw_temp != 0xFF {
                Some(raw_temp)
            } else {
                None
            },
        }
    }
}

现在每个消费者必须处理 None 情况——编译器强制它:

// 安全——编译器确保我们处理缺失的温度
fn is_overtemp(temp: Option<u8>, threshold: u8) -> bool {
    temp.map_or(false, |t| t > threshold)
}

// 忘记处理 None 是编译错误:
// fn bad_check(temp: Option<u8>, threshold: u8) -> bool {
//     temp > threshold  // 错误:无法比较 Option<u8> 与 u8
// }

实际影响

inventory/src/events.rs 对 GPU 热警报使用相同的模式:

temperature: if data[1] != 0xFF {
    Some(data[1] as i8)
} else {
    None
},

pcie_tree/src/lspci.rs 的重构很简单:将 current_vendor_id: u16 改为 current_vendor_id: Option<u16>,用 None 替换 0xFFFF,让编译器找到每个需要更新的站点。

之前之后
let mut vendor_id: u16 = 0xFFFFlet mut vendor_id: Option<u16> = None
.unwrap_or(0xFFFF).ok()(已经返回 Option)
if vendor_id != 0xFFFF { ... }if let Some(vid) = vendor_id { ... }
序列化:vendor_idvendor_id.unwrap_or(0xFFFF)

技巧 2 —— 密封 Trait

第 2 章介绍了 IpmiCmd,它有一个关联类型将每个命令绑定到其响应。但有一个漏洞:如果任何代码都能实现 IpmiCmd,有人可以编写一个 MaliciousCmd,其 parse_response 返回错误的类型或 panic。整个系统的类型安全性取决于每个实现的正确性。

密封 trait 封闭了这个漏洞。想法很简单:让 trait 需要一个私有的 超 trait,只有你的 crate 可以实现。

// — 私有模块:不从 crate 导出 —
mod private {
    pub trait Sealed {}
}

// — 公共 trait:需要 Sealed,外人无法实现 —
pub trait IpmiCmd: private::Sealed {
    type Response;
    fn net_fn(&self) -> u8;
    fn cmd_byte(&self) -> u8;
    fn payload(&self) -> Vec<u8>;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

在你的 crate 内部,你为每个批准的命令类型实现 Sealed:

pub struct ReadTemp { pub sensor_id: u8 }
impl private::Sealed for ReadTemp {}

impl IpmiCmd for ReadTemp {
    type Response = Celsius;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.sensor_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        if raw.is_empty() { return Err(io::Error::new(io::ErrorKind::InvalidData, "empty")); }
        Ok(Celsius(raw[0] as f64))
    }
}

外部代码看到 IpmiCmd 并可以调用 execute(),但无法实现它:

// 在另一个 crate 中:
struct EvilCmd;
// impl private::Sealed for EvilCmd {}  // 错误:模块 `private` 是私有的
// impl IpmiCmd for EvilCmd { ... }     // 错误:`Sealed` 不满足

何时密封

密封当…不密封当…
安全性取决于正确的实现(IpmiCmd、DiagModule)用户应该扩展系统(自定义报告格式化程序)
关联类型必须满足不变量trait 是简单的 capability 标记(HasIpmi)
你拥有规范的实现集合第三方插件是设计目标

实际候选

  • IpmiCmd —— 错误的解析可能污染类型化响应
  • DiagModule —— 框架假设 run() 返回有效的 DER 记录
  • SelEventFilter —— 损坏的过滤器可能吞掉关键的 SEL 事件

技巧 3 —— 用于演进枚举的 #[non_exhaustive]

inventory/src/types.rs 中的 SkuVariant 今天有五个变体:

pub enum SkuVariant {
    S1001, S2001, S2002, S2003, S3001,
}

当下一代出货且你添加 S4001 时,任何外部代码如果匹配 SkuVariant 且没有通配符臂,将静默地编译失败——这正是重点。但内部代码呢?没有 #[non_exhaustive],你在同一个 crate 中的 match 编译时不需要通配符,添加新的变体会破坏你自己的构建。

标记枚举为 #[non_exhaustive] 强制外部 crate 在匹配时包含通配符臂。在定义的 crate 内,#[non_exhaustive] 没有效果——你仍然可以编写穷举匹配。

为什么这有用: 当你从库 crate(或 workspace 中的共享子 crate)发布 SkuVariant 时,下游代码被迫处理未知的未来变体。当你下一代添加 S4001 时,下游代码已经编译——他们有通配符臂。

// 在 gpu_sel crate(定义的 crate)中:
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SkuVariant {
    S1001,
    S2001,
    S2002,
    S2003,
    S3001,
    // 当下一代 SKU 出货时,在这里添加。
    // 外部消费者已经有通配符——对他们零破坏。
}

// 在 gpu_sel 内部——允许穷举匹配(不需要通配符):
fn diag_path_internal(sku: SkuVariant) -> &'static str {
    match sku {
        SkuVariant::S1001 => "legacy_gen1",
        SkuVariant::S2001 => "gen2_accel_diag",
        SkuVariant::S2002 => "gen2_alt_diag",
        SkuVariant::S2003 => "gen2_alt_hf_diag",
        SkuVariant::S3001 => "gen3_accel_diag",
        // 在定义的 crate 内不需要通配符。
        // 在这里添加 S4001 会导致这个 match 的编译错误,
        // 这正是你想要的——它强制你更新它。
    }
}
// 在二进制 crate(依赖 inventory 的下游 crate)中:
fn diag_path_external(sku: inventory::SkuVariant) -> &'static str {
    match sku {
        inventory::SkuVariant::S1001 => "legacy_gen1",
        inventory::SkuVariant::S2001 => "gen2_accel_diag",
        inventory::SkuVariant::S2002 => "gen2_alt_diag",
        inventory::SkuVariant::S2003 => "gen2_alt_hf_diag",
        inventory::SkuVariant::S3001 => "gen3_accel_diag",
        _ => "generic_diag",  // #[non_exhaustive] 对外部 crate 要求
    }
}

Workspace 提示: 如果你的所有代码都在单个 crate 中,#[non_exhaustive] 不会帮助——它只影响跨 crate 边界。对于项目的大型 workspace,将演进的枚举放在共享 crate(core_lib 或 inventory)中,这样属性可以保护其他 workspace crate 中的消费者。

候选

枚举模块为什么
SkuVariantinventory, net_inventory每代新 SKU
SensorTypeprotocol_libIPMI 规范保留 0xC0–0xFF 给 OEM
CompletionCodeprotocol_lib自定义 BMC 供应商添加代码
Componentevent_handler新硬件类别(最近添加了 NewSoC)

技巧 4 —— Typestate Builder

第 5 章展示了用于协议的 type-state(会话生命周期、链路训练)。同样的想法适用于builder —— structs 的 build() / finish() 只有在设置了所有必需字段后才能调用。

Fluent Builder 的问题

diag_framework/src/der.rs 中的 DerBuilder 今天看起来像这样(简化):

// 当前的 fluent builder —— finish() 总是可用
pub struct DerBuilder {
    der: Der,
}

impl DerBuilder {
    pub fn new(marker: &str, fault_code: u32) -> Self { ... }
    pub fn mnemonic(mut self, m: &str) -> Self { ... }
    pub fn fault_class(mut self, fc: &str) -> Self { ... }
    pub fn finish(self) -> Der { self.der }  // ← 总是可调用!
}

这会编译没有错误,但产生不完整的 DER 记录:

let bad = DerBuilder::new("CSI_ERR", 62691)
    .finish();  // 哎呀——没有 mnemonic,没有 fault_class

Typestate builder:finish() 需要两个字段

pub struct Missing;
pub struct Set<T>(T);

pub struct DerBuilder<Mnemonic, FaultClass> {
    marker: String,
    fault_code: u32,
    mnemonic: Mnemonic,
    fault_class: FaultClass,
    description: Option<String>,
}

// 构造函数:以两个 Missing 的必需字段开始
impl DerBuilder<Missing, Missing> {
    pub fn new(marker: &str, fault_code: u32) -> Self {
        DerBuilder {
            marker: marker.to_string(),
            fault_code,
            mnemonic: Missing,
            fault_class: Missing,
            description: None,
        }
    }
}

// 设置 mnemonic(无论 fault_class 的状态如何都有效)
impl<FC> DerBuilder<Missing, FC> {
    pub fn mnemonic(self, m: &str) -> DerBuilder<Set<String>, FC> {
        DerBuilder {
            marker: self.marker, fault_code: self.fault_code,
            mnemonic: Set(m.to_string()),
            fault_class: self.fault_class,
            description: self.description,
        }
    }
}

// 设置 fault_class(无论 mnemonic 的状态如何都有效)
impl<MN> DerBuilder<MN, Missing> {
    pub fn fault_class(self, fc: &str) -> DerBuilder<MN, Set<String>> {
        DerBuilder {
            marker: self.marker, fault_code: self.fault_code,
            mnemonic: self.mnemonic,
            fault_class: Set(fc.to_string()),
            description: self.description,
        }
    }
}

// 可选字段——在*任何*状态可用
impl<MN, FC> DerBuilder<MN, FC> {
    pub fn description(mut self, desc: &str) -> Self {
        self.description = Some(desc.to_string());
        self
    }
}

/// 完全构建的 DER 记录。
pub struct Der {
    pub marker: String,
    pub fault_code: u32,
    pub mnemonic: String,
    pub fault_class: String,
    pub description: Option<String>,
}

// finish() 仅在两个必需字段都 Set 时可调用
impl DerBuilder<Set<String>, Set<String>> {
    pub fn finish(self) -> Der {
        Der {
            marker: self.marker,
            fault_code: self.fault_code,
            mnemonic: self.mnemonic.0,
            fault_class: self.fault_class.0,
            description: self.description,
        }
    }
}

现在有 bug 的调用是编译错误:

// ✅ 编译通过——两个必需字段都设置了(顺序任意)
let der = DerBuilder::new("CSI_ERR", 62691)
    .fault_class("GPU Module")   // 顺序不重要
    .mnemonic("ACCEL_CARD_ER691")
    .description("Thermal throttle")
    .finish();

// ❌ 编译错误——finish() 在 DerBuilder<Set<String>, Missing> 上不存在
let bad = DerBuilder::new("CSI_ERR", 62691)
    .mnemonic("ACCEL_CARD_ER691")
    .finish();  // 错误:方法 `finish` 未找到

何时使用 typestate builder

使用当…不必麻烦当…
省略字段导致静默 bug(DER 缺少 mnemonic)所有字段都有 sensible 的默认值
builder 是公共 API 的一部分builder 仅是测试脚手架
超过 2-3 个必需字段单个必需字段(在 new() 中获取即可)

技巧 5 —— FromStr 作为验证边界

第 7 章展示了 TryFrom<&[u8]> 用于二进制数据(FRU 记录、SEL 条目)。对于字符串输入——配置文件、CLI 参数、JSON 字段——类似的边界是 FromStr。

问题

// C++ / 未验证的 Rust:静默回退到默认值
fn route_diag(level: &str) -> DiagMode {
    if level == "quick" { ... }
    else if level == "standard" { ... }
    else { QuickMode }  // 配置文件中的拼写错误?¯\_(ツ)_/¯
}

配置文件中有 "diag_level": "extendedd"(拼写错误)静默地获得 QuickMode。

模式(来自 config_loader/src/diag.rs)

use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagLevel {
    Quick,
    Standard,
    Extended,
    Stress,
}

impl FromStr for DiagLevel {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "quick"    | "1" => Ok(DiagLevel::Quick),
            "standard" | "2" => Ok(DiagLevel::Standard),
            "extended" | "3" => Ok(DiagLevel::Extended),
            "stress"   | "4" => Ok(DiagLevel::Stress),
            other => Err(format!("unknown diag level: '{other}'")),
        }
    }
}

现在拼写错误立即被捕获:

let level: DiagLevel = "extendedd".parse()?;
// Err("unknown diag level: 'extendedd'")

三个好处

  1. 快速失败: 错误输入在解析边界被捕获,而不是在诊断逻辑深处三层。
  2. 别名是显式的: "MEM"、"DIMM" 和 "MEMORY" 都映射到 Component::Memory —— match 臂记录了映射。
  3. .parse() 符合人体工程学: 因为 FromStr 与 str::parse() 集成,你获得简洁的单行代码:let level: DiagLevel = config["level"].parse()?;

实际代码库使用

项目已经有 8 个 FromStr 实现:

类型模块值得注意的别名
DiagLevelconfig_loader"1" = Quick, "4" = Stress
Componentevent_handler"MEM" / "DIMM" = Memory, "SSD" / "NVME" = Disk
SkuVariantnet_inventory"Accel-X1" = S2001, "Accel-M1" = S2002, "Accel-Z1" = S3001
SkuVariantinventory相同的别名(单独的模块,相同的模式)
FaultStatusconfig_loaderFault 生命周期状态
DiagActionconfig_loader补救动作类型
ActionTypeconfig_loader动作类别
DiagModecluster_diag多节点测试模式

与 TryFrom 的对比:

TryFrom<&[u8]>FromStr
输入原始字节(二进制协议)字符串(配置、CLI、JSON)
典型来源IPMI、PCIe 配置空间、FRUJSON 字段、环境变量、用户输入
章节ch07ch11
两者都使用Result —— 强制调用者处理无效输入

技巧 6 —— 用于编译时大小验证的 Const 泛型

当硬件缓冲区、寄存器组或协议帧有固定大小时,const 泛型让编译器强制执行它们:

/// 固定大小的寄存器组。大小是类型的一部分。
/// `RegisterBank<256>` 和 `RegisterBank<4096>` 是不同的类型。
pub struct RegisterBank<const N: usize> {
    data: [u8; N],
}

impl<const N: usize> RegisterBank<N> {
    /// 读取给定偏移处的寄存器。
    /// 编译时:N 已知,所以数组大小是固定的。
    /// 运行时:只检查偏移。
    pub fn read(&self, offset: usize) -> Option<u8> {
        self.data.get(offset).copied()
    }
}

// PCIe 传统配置空间:256 字节
type PciConfigSpace = RegisterBank<256>;

// PCIe 扩展配置空间:4096 字节
type PcieExtConfigSpace = RegisterBank<4096>;

// 这些是不同的类型——不能意外地用一个替换另一个:
fn read_extended_cap(config: &PcieExtConfigSpace, offset: usize) -> Option<u8> {
    config.read(offset)
}
// read_extended_cap(&pci_config, 0x100);
//                   ^^^^^^^^^^^ 期望 RegisterBank<4096>,找到 RegisterBank<256> ❌

带有 const 泛型的编译时断言:

/// NVMe 管理命令使用 4096 字节的缓冲区。在编译时强制执行。
pub struct NvmeBuffer<const N: usize> {
    data: Box<[u8; N]>,
}

impl<const N: usize> NvmeBuffer<N> {
    pub fn new() -> Self {
        // 运行时断言:只允许 512 或 4096
        assert!(N == 4096 || N == 512, "NVMe 缓冲区必须是 512 或 4096 字节");
        NvmeBuffer { data: Box::new([0u8; N]) }
    }
}
// NvmeBuffer::<1024>::new();  // 以这种形式在运行时 panic
// 对于真正的编译时强制执行,参见技巧 9(const 断言)。

何时使用: 固定大小的协议缓冲区(NVMe、PCIe 配置空间)、DMA 描述符、硬件 FIFO 深度。任何大小是硬件常量的地方,永远不应该在运行时变化。


技巧 7 —— unsafe 周围的安全包装器

项目目前有零个 unsafe 块。但当你添加 MMIO 寄存器访问、DMA 或与 accel-mgmt/accel-query 的 FFI 时,你将需要 unsafe。构造正确性方法:将每个 unsafe 块包装在安全抽象中,这样不安全性被包含且可审计。

/// MMIO 映射的寄存器。指针对映射的生命周期有效。
/// 所有 unsafe 都包含在这个模块中——调用者使用安全方法。
pub struct MmioRegion {
    base: *mut u8,
    len: usize,
}

impl MmioRegion {
    /// # Safety
    /// - `base` 必须是指向 MMIO 映射区域的有效指针
    /// - 该区域必须在该结构的生命周期内保持映射
    /// - 没有其他代码可以 alias 这个区域
    pub unsafe fn new(base: *mut u8, len: usize) -> Self {
        MmioRegion { base, len }
    }

    /// 安全读取——边界检查防止越界 MMIO 访问。
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        if offset + 4 > self.len { return None; }
        // SAFETY: offset 在上面进行了边界检查,base 根据 new() 契约有效
        Some(unsafe {
            core::ptr::read_volatile(self.base.add(offset) as *const u32)
        })
    }

    /// 安全写入——边界检查防止越界 MMIO 访问。
    pub fn write_u32(&self, offset: usize, value: u32) -> bool {
        if offset + 4 > self.len { return false; }
        // SAFETY: offset 在上面进行了边界检查,base 根据 new() 契约有效
        unsafe {
            core::ptr::write_volatile(self.base.add(offset) as *mut u32, value);
        }
        true
    }
}

与 phantom 类型(ch09)结合用于类型化 MMIO:

use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;

pub struct TypedMmio<Perm> {
    region: MmioRegion,
    _perm: PhantomData<Perm>,
}

impl TypedMmio<ReadOnly> {
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        self.region.read_u32(offset)
    }
    // 没有 write 方法——如果你尝试写入 ReadOnly 区域是编译错误
}

impl TypedMmio<ReadWrite> {
    pub fn read_u32(&self, offset: usize) -> Option<u32> {
        self.region.read_u32(offset)
    }
    pub fn write_u32(&self, offset: usize, value: u32) -> bool {
        self.region.write_u32(offset, value)
    }
}

unsafe 包装器的指南:

规则为什么
一个带有文档化 # Safety 不变量的 unsafe fn new()调用者一次性承担责任
所有其他方法是安全的调用者无法触发 UB
每个 unsafe 块上的 # SAFETY: 注释审计者可以在本地验证
用 #[deny(unsafe_op_in_unsafe_fn)] 包装在模块中即使在 unsafe fn 内部,单个操作也需要 unsafe
在包装器上运行 cargo +nightly miri test验证内存模型合规性

✅ 检查点:技巧 1-7

你现在有七个日常技巧。这是一个快速计分卡:

技巧消除的 bug 类别采用成本
1哨兵混淆(0xFF)低——边界处一个 match
2未授权的 trait 实现低——添加 Sealed 超 trait
3枚举增长后的破坏的消费者低——一行属性
4缺失 builder 字段中——额外的类型参数
5字符串类型配置中的拼写错误低——impl FromStr
6错误的缓冲区大小低——const 泛型参数
7unsafe 散布在代码库中中——包装器模块

技巧 8-14 更高级——它们涉及 async、const 求值、session 类型、Pin 和 Drop。如果需要可以在这里休息一下;上面的技术已经是高价值、低成本的胜利,你明天就可以采用。


技巧 8 —— Async Type-State 机

当硬件驱动使用 async(例如异步 BMC 通信、异步 NVMe I/O)时,type-state 仍然有效——但在 .await 点之间的所有权需要注意:

use std::marker::PhantomData;

pub struct Idle;
pub struct Authenticating;
pub struct Active;

pub struct AsyncSession<S> {
    host: String,
    _state: PhantomData<S>,
}

impl AsyncSession<Idle> {
    pub fn new(host: &str) -> Self {
        AsyncSession { host: host.to_string(), _state: PhantomData }
    }

    /// 过渡 Idle → Authenticating → Active。
    /// Session 被消费(移动进入 future)跨越 .await。
    pub async fn authenticate(self, user: &str, pass: &str)
        -> Result<AsyncSession<Active>, String>
    {
        // 阶段 1:发送凭证(消费 Idle session)
        let pending: AsyncSession<Authenticating> = AsyncSession {
            host: self.host,
            _state: PhantomData,
        };

        // 模拟异步 BMC 认证
        // tokio::time::sleep(Duration::from_secs(1)).await;

        // 阶段 2:返回 Active session
        Ok(AsyncSession {
            host: pending.host,
            _state: PhantomData,
        })
    }
}

impl AsyncSession<Active> {
    pub async fn send_command(&mut self, cmd: &[u8]) -> Vec<u8> {
        // async I/O 在这里...
        vec![0x00]
    }
}

// 用法:
// let session = AsyncSession::new("192.168.1.100");
// let mut session = session.authenticate("admin", "pass").await?;
// let resp = session.send_command(&[0x04, 0x2D]).await;

async type-state 的关键规则:

规则为什么
过渡方法获取 self(按值),而不是 &mut self所有权转移在 .await 之间有效
返回 Result<NextState, (Error, PrevState)> 用于可恢复错误调用者可以从 previous 状态重试
不要将状态分割到多个 futures 中一个 future 拥有一个 session
如果使用 tokio::spawn 使用 Send + 'static 边界session 必须可以跨线程移动

注意: 如果你需要在错误时取回previous状态(用于重试),返回 Result<AsyncSession<Active>, (Error, AsyncSession<Idle>)> 以便调用者取回所有权。没有这个,失败的 .await 会永久丢弃 session。


技巧 9 —— 通过 Const 断言的精炼类型

当数值约束是编译时不变量(不是运行时数据)时,使用 const 求值强制执行它。这与技巧 6(提供类型级大小区分)不同——这里我们在编译时拒绝无效值:

/// 必须在 IPMI SDR 范围内的传感器 ID(0x01..=0xFE)。
/// 当 `N` 是 const 时,约束在编译时检查。
pub struct SdrSensorId<const N: u8>;

impl<const N: u8> SdrSensorId<N> {
    /// 编译时验证:如果 N 超出范围,在编译期间 panic。
    pub const fn validate() {
        assert!(N >= 0x01, "传感器 ID 必须 >= 0x01");
        assert!(N <= 0xFE, "传感器 ID 必须 <= 0xFE(0xFF 保留)");
    }

    pub const VALIDATED: () = Self::validate();

    pub const fn value() -> u8 { N }
}

// 用法:
fn read_sensor_const<const N: u8>() -> f64 {
    let _ = SdrSensorId::<N>::VALIDATED;  // 编译时检查
    // 读取传感器 N...
    42.0
}

// read_sensor_const::<0x20>();   // ✅ 编译通过——0x20 有效
// read_sensor_const::<0x00>();   // ❌ 编译错误——"传感器 ID 必须 >= 0x01"
// read_sensor_const::<0xFF>();   // ❌ 编译错误——0xFF 保留

更简单的形式——有界风扇 ID:

pub struct BoundedFanId<const N: u8>;

impl<const N: u8> BoundedFanId<N> {
    pub const VALIDATED: () = assert!(N < 8, "服务器最多 8 个风扇(0..7)");

    pub const fn id() -> u8 {
        let _ = Self::VALIDATED;
        N
    }
}

// BoundedFanId::<3>::id();   // ✅
// BoundedFanId::<10>::id();  // ❌ 编译错误

何时使用: 编译时已知的硬件定义的固定 ID(传感器 ID、风扇插槽、PCIe 插槽号)。当值来自运行时数据(配置文件、用户输入)时,改用 TryFrom / FromStr(ch07、技巧 5)。


技巧 10 —— 用于通道通信的 Session 类型

当两个组件通过通道通信时(例如诊断编排器 ↔ 工作线程),session 类型在类型系统中编码协议:

use std::marker::PhantomData;

// 协议:客户端发送 Request,服务器发送 Response,然后完成。
pub struct SendRequest;
pub struct RecvResponse;
pub struct Done;

/// 类型化的通道端点。`S` 是当前协议状态。
pub struct Chan<S> {
    // 在实际代码中:包装 mpsc::Sender/Receiver 对
    _state: PhantomData<S>,
}

impl Chan<SendRequest> {
    /// 发送请求——过渡到 RecvResponse 状态。
    pub fn send(self, request: DiagRequest) -> Chan<RecvResponse> {
        // ... 在通道上发送 ...
        Chan { _state: PhantomData }
    }
}

impl Chan<RecvResponse> {
    /// 接收响应——过渡到 Done 状态。
    pub fn recv(self) -> (DiagResponse, Chan<Done>) {
        // ... 从通道接收 ...
        (DiagResponse { passed: true }, Chan { _state: PhantomData })
    }
}

impl Chan<Done> {
    /// 关闭通道——仅在协议完成时可能。
    pub fn close(self) { /* drop */ }
}

pub struct DiagRequest { pub test_name: String }
pub struct DiagResponse { pub passed: bool }

// 协议*必须*按顺序遵循:
fn orchestrator(chan: Chan<SendRequest>) {
    let chan = chan.send(DiagRequest { test_name: "gpu_stress".into() });
    let (response, chan) = chan.recv();
    chan.close();
    println!("结果:{}", if response.passed { "PASS" } else { "FAIL" });
}

// 不能在发送前接收:
// fn wrong_order(chan: Chan<SendRequest>) {
//     chan.recv();  // ❌ 在 Chan<SendRequest> 上没有方法 `recv`
// }

何时使用: 线程间诊断协议、BMC 命令序列、任何顺序重要的请求 - 响应模式。对于复杂的多消息协议,考虑 session-types 或 rumpsteak crate。


技巧 11 —— 用于自引用状态机的 Pin

一些 type-state 机需要持有对自身数据的引用(例如其拥有的缓冲区内跟踪位置的分析器)。Rust 通常禁止这一点,因为移动结构体会使内部指针无效。Pin<T> 通过保证值不会被移动来解决这个问题:

use std::pin::Pin;
use std::marker::PhantomPinned;

/// 持有自身缓冲区引用的流式分析器。
/// 一旦固定,就不能移动——内部引用保持有效。
pub struct StreamParser {
    buffer: Vec<u8>,
    /// 指向 `buffer`。仅在固定时有效。
    cursor: *const u8,
    _pin: PhantomPinned,  // 退出 Unpin——防止意外解固定
}

impl StreamParser {
    pub fn new(data: Vec<u8>) -> Pin<Box<Self>> {
        let parser = StreamParser {
            buffer: data,
            cursor: std::ptr::null(),
            _pin: PhantomPinned,
        };
        let mut boxed = Box::pin(parser);

        // 设置 cursor 指向固定的缓冲区
        let cursor = boxed.buffer.as_ptr();
        // SAFETY: 我们有独占访问且分析器已固定
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).cursor = cursor;
        }

        boxed
    }

    /// 读取下一个字节——仅可通过 Pin<&mut Self> 调用。
    pub fn next_byte(self: Pin<&mut Self>) -> Option<u8> {
        // 分析器不能移动,所以 cursor 保持有效
        if self.cursor.is_null() { return None; }
        // ... 在缓冲区中前进 cursor ...
        Some(42) // 存根
    }
}

// 用法:
// let mut parser = StreamParser::new(vec![0x01, 0x02, 0x03]);
// let byte = parser.as_mut().next_byte();

关键洞察: Pin 是解决自引用结构体问题的构造正确性方案。没有它,你将需要 unsafe 和手动生命周期跟踪。有了它,编译器防止移动且内部指针不变量得到维护。

使用 Pin 当…不使用 Pin 当…
状态机持有结构体内引用所有字段独立拥有
异步 futures 在 .await 之间借用不需要自引用
DMA 描述符在内存中不能重定位数据可以自由移动
带有内部 cursor 的硬件环形缓冲区简单的基于索引的迭代有效

技巧 12 —— RAII / Drop 作为正确性保证

Rust 的 Drop trait 是一个构造正确性机制:清理代码不可能被忘记,因为编译器自动插入它。这对于必须恰好一次释放的硬件资源特别有价值。

use std::io;

/// 完成后*必须*关闭的 IPMI 会话。
/// `Drop` impl 保证清理即使在 panic 或提前 `?` 返回时。
pub struct IpmiSession {
    handle: u32,
}

impl IpmiSession {
    pub fn open(host: &str) -> io::Result<Self> {
        // ... 协商 IPMI 会话 ...
        Ok(IpmiSession { handle: 42 })
    }

    pub fn send_raw(&self, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![0x00])
    }
}

impl Drop for IpmiSession {
    fn drop(&mut self) {
        // Close Session 命令:总是运行,即使在 panic/提前返回时。
        // 在 C 中,忘记 CloseSession() 会泄漏 BMC 会话插槽。
        let _ = self.send_raw(&[0x06, 0x3C]);
        eprintln!("[RAII] 会话 {} 已关闭", self.handle);
    }
}

// 用法:
fn diagnose(host: &str) -> io::Result<()> {
    let session = IpmiSession::open(host)?;
    session.send_raw(&[0x04, 0x2D, 0x20])?;
    // 不需要显式关闭——Drop 在这里自动运行
    Ok(())
    // 即使 send_raw 返回 Err(...),会话仍然关闭。
}

RAII 消除的 C/C++ 失败模式:

C:     session = ipmi_open(host);
       ipmi_send(session, data);
       if (error) return -1;        // 🐛 泄漏的会话——忘记 close()
       ipmi_close(session);

Rust:  let session = IpmiSession::open(host)?;
       session.send_raw(data)?;     // ✅ Drop 在 ? 返回时运行
       // Drop 总是运行——泄漏是不可能的

将 RAII 与 type-state(ch05)结合用于有序清理:

你不能为泛型参数特化 Drop(Rust 错误 E0366)。相反,对每个状态使用单独的包装类型:

use std::marker::PhantomData;

pub struct Open;
pub struct Locked;

pub struct GpuContext<S> {
    device_id: u32,
    _state: PhantomData<S>,
}

impl GpuContext<Open> {
    pub fn lock_clocks(self) -> LockedGpu {
        // ... 锁定 GPU 时钟用于稳定的基准测试 ...
        LockedGpu { device_id: self.device_id }
    }
}

/// 锁定状态的单独类型——有自己的 Drop。
/// 我们不能做 `impl Drop for GpuContext<Locked>`(E0366),
/// 所以我们使用一个独立的包装器来拥有锁定的资源。
pub struct LockedGpu {
    device_id: u32,
}

impl LockedGpu {
    pub fn run_benchmark(&self) -> f64 {
        // ... 使用锁定的时钟进行基准测试 ...
        42.0
    }
}

impl Drop for LockedGpu {
    fn drop(&mut self) {
        // drop 时解锁时钟——仅针对锁定的包装器触发。
        eprintln!("[RAII] GPU {} 时钟已解锁", self.device_id);
    }
}

// GpuContext<Open> 没有特殊的 Drop——没有时钟需要解锁。
// LockedGpu 总是在 drop 时解锁,即使在 panic 或提前返回时。

为什么不 impl Drop for GpuContext<Locked>? Rust 要求 Drop impl 应用于泛型类型的所有实例。要获得特定状态的清理,使用以下之一:

方法优点缺点
单独的包装类型(如上)干净、零成本额外的类型名
泛型 Drop + 运行时 TypeId 检查单一类型需要 'static,运行时成本
Drop 中的 enum 状态与穷举匹配单一泛型类型运行时分发,较少的类型安全性

何时使用: BMC 会话、GPU 时钟锁定、DMA 缓冲区映射、文件句柄、互斥守卫、任何带有强制释放步骤的资源。如果你发现自己写 fn close(&mut self) 或 fn cleanup(),它几乎肯定应该是 Drop。


技巧 13 —— 作为正确性的错误类型层次结构

精心设计的错误类型防止静默错误吞没,确保调用者适当地处理每种失败模式。使用 thiserror 进行结构化错误处理是一个构造正确性模式:编译器强制穷举匹配。

# Cargo.toml
[dependencies]
thiserror = "1"
# 对于应用程序级错误处理(可选):
# anyhow = "1"
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DiagError {
    #[error("IPMI 通信失败:{0}")]
    Ipmi(#[from] IpmiError),

    #[error("传感器 {sensor_id:#04x} 读数超出范围:{value}")]
    SensorRange { sensor_id: u8, value: f64 },

    #[error("GPU {gpu_id} 无响应")]
    GpuTimeout { gpu_id: u32 },

    #[error("配置无效:{0}")]
    Config(String),
}

#[derive(Debug, Error)]
pub enum IpmiError {
    #[error("会话认证失败")]
    AuthFailed,

    #[error("命令 {net_fn:#04x}/{cmd:#04x} 超时")]
    Timeout { net_fn: u8, cmd: u8 },

    #[error("完成码 {0:#04x}")]
    CompletionCode(u8),
}

// 调用者*必须*处理每个变体——没有静默吞没:
fn run_thermal_check() -> Result<(), DiagError> {
    // 如果这返回 IpmiError,它通过 #[from] 属性自动转换为 DiagError::Ipmi。
    let temp = read_cpu_temp()?;
    if temp > 105.0 {
        return Err(DiagError::SensorRange {
            sensor_id: 0x20,
            value: temp,
        });
    }
    Ok(())
}

fn read_cpu_temp() -> Result<f64, DiagError> { Ok(42.0) }

为什么这是构造正确的:

没有结构化错误使用 thiserror 枚举
fn op() -> Result<T, String>fn op() -> Result<T, DiagError>
调用者获得不透明的字符串调用者匹配特定的变体
无法区分认证失败与超时DiagError::Ipmi(IpmiError::AuthFailed) vs Timeout
日志记录吞掉错误match 强制处理每个情况
新错误变体 → 没有人注意新变体 → 编译器警告未匹配的臂

anyhow vs thiserror 决策:

使用 thiserror 当…使用 anyhow 当…
编写库/crate编写二进制/CLI
调用者需要匹配错误变体调用者只是日志记录并退出
错误类型是公共 API 的一部分内部错误管道
protocol_lib, accel_diag, thermal_diagdiag_tool 主二进制

何时使用: workspace 中的每个 crate 都应该用 thiserror 定义它自己的错误枚举。顶层二进制 crate 可以使用 anyhow 来聚合它们。这给库调用者编译时错误处理保证,同时保持二者的简洁性。


技巧 14 —— 用于强制执行消费的 #[must_use]

#[must_use] 属性将忽略的返回值转换为编译器警告。这是一个轻量级的构造正确性工具,可以与本指南中的每个模式配对:

/// 校准令牌,*必须*使用——静默丢弃它是一个 bug。
#[must_use = "校准令牌必须传递给 calibrate(),不能丢弃"]
pub struct CalibrationToken {
    _private: (),
}

/// 诊断结果,*必须*检查——忽略失败是一个 bug。
#[must_use = "诊断结果必须检查失败"]
pub struct DiagResult {
    pub passed: bool,
    pub details: String,
}

/// 返回重要值的函数也应该标记:
#[must_use = "已认证的会话必须使用或显式关闭"]
pub fn authenticate(user: &str, pass: &str) -> Result<Session, AuthError> {
    // ...
  unimplemented!()
}

pub struct Session;
pub struct AuthError;

编译器告诉你的内容:

警告:未使用必须使用的 `CalibrationToken`
  --> src/main.rs:5:5
   |
5  |     CalibrationToken { _private: () };
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = 注意:校准令牌必须传递给 calibrate(),不能丢弃

将 #[must_use] 应用于这些模式:

模式注释什么为什么
一次性令牌(ch03)CalibrationToken, FusePayload不使用就丢弃 = 逻辑 bug
Capability 令牌(ch04)AdminToken认证但忽略令牌
Type-State 过渡authenticate(), activate() 的返回类型创建的会话从未使用
结果DiagResult, SensorReading静默失败吞没
RAII 句柄(技巧 12)IpmiSession, LockedGpu打开但不使用资源

经验法则: 如果丢弃一个值而不使用它总是一个 bug,添加 #[must_use]。如果有时是故意的(例如 Vec),不要。_ 前缀(let _ = foo())显式确认并消除警告——当丢弃是故意的时这没问题。

关键要点

  1. 边界处的哨兵 → Option ——在解析时将魔术值转换为 Option;编译器强制调用者处理 None。
  2. 密封 trait 封闭实现漏洞 ——私有超 trait 意味着只有你的 crate 可以实现 trait。
  3. #[non_exhaustive] + #[must_use] 是一行、高价值的注释 ——将它们添加到演进的枚举和消费的令牌。
  4. Typestate builder 强制执行必需字段 ——当所有必需的参数是 Set 时,finish() 才存在。
  5. 每个技巧针对特定的 bug 类别 ——增量采用它们;没有技巧需要重写你的架构。