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

Rust 中的类型驱动正确性

演讲者介绍

  • 微软 SCHIE(硅与云硬件基础设施工程)团队首席固件架构师
  • 行业资深专家,专长于安全、系统编程(固件、操作系统、管理程序)、CPU 和平台架构以及 C++ 系统
  • 2017 年开始使用 Rust 编程(@AWS EC2),从此爱上了这门语言

本实用指南介绍如何使用 Rust 的类型系统让整个类别的 bug无法编译。配套的 Rust Patterns 书籍涵盖了机制(traits、关联类型、type-state),而本指南则展示如何将这些机制应用到现实世界领域——硬件诊断、加密、协议验证和嵌入式系统。

这里的每个模式都遵循一个原则:将不变量从运行时检查推入类型系统,让编译器强制执行。

如何使用本书

难度图例

符号级别目标读者
🟢入门熟悉所有权 + traits
🟡中级熟悉泛型 + 关联类型
🔴高级准备好学习 type-state、phantom types 和 session types

进度指南

目标路径时间
快速概览ch01, ch13(参考卡)30 分钟
IPMI / BMC 开发者ch02, ch05, ch07, ch10, ch172.5 小时
GPU / PCIe 开发者ch02, ch06, ch09, ch10, ch152.5 小时
Redfish 实现者ch02, ch05, ch07, ch08, ch17, ch183 小时
框架 / 基础设施ch04, ch08, ch11, ch14, ch182.5 小时
构造正确性新手按顺序 ch01 → ch10,然后 ch12 练习4 小时
完整深入按顺序阅读所有章节7 小时

带注释的目录

章标题难度核心思想
1理念——为什么类型击败测试🟢三层正确性;类型作为编译器检查的保证
2类型化命令接口🟡关联类型绑定请求 → 响应
3一次性类型🟡移动语义作为加密的线性类型
4Capability 令牌🟡零尺寸权限证明令牌
5协议状态机🔴IPMI sessions + PCIe LTSSM 的 type-state
6量纲分析🟢Newtype 包装器防止单位混淆
7有效性边界🟡在边缘解析一次,在类型中携带证明
8Capability 混合🟡成分 traits + 空白实现
9Phantom 类型🟡PhantomData 用于寄存器宽度、DMA 方向
10整合一切🟡一个诊断平台中的全部 7 个模式
11实战十四个技巧🟡Sentinel→Option、密封 traits、builders 等
12练习🟡六个综合项目问题附带解决方案
13参考卡—模式目录 + 决策流程图
14测试类型级保证🟡trybuild, proptest, cargo-show-asm
15Const Fn🟠内存映射、寄存器、比特域的编译时证明
16Send & Sync🟠编译时并发证明
17Redfish 客户端演练🟡八个模式组合成类型安全的 Redfish 客户端
18Redfish 服务器演练🟡Builder type-state、源令牌、健康汇总、mixins

前置知识

概念学习资源
所有权和借用Rust Patterns, ch01
Traits 和关联类型Rust Patterns, ch02
Newtypes 和 type-stateRust Patterns, ch03
PhantomDataRust Patterns, ch04
泛型和 trait 边界Rust Patterns, ch01

构造正确性谱系

← 更安全度较低                                              更安全度较高 →

运行时检查        单元测试         属性测试          构造正确性
─────────────     ──────────       ──────────────    ──────────────────────

if temp > 100 {   #[test]          proptest! {        struct Celsius(f64);
  panic!("too     fn test_temp() {   |t in 0..200| {  // 类型层面无法
  hot");            assert!(         assert!(...)      // 与 Rpm 混淆
}                   check(42));    }
                  }                }
                                                       无效程序?
无效程序?        无效程序?       无效程序?          无法编译。
生产环境崩溃。    CI 中失败。      CI 中失败           永不复存在。
                                    (概率性)。

本指南位于最右侧的位置——在这个位置上,bug 不存在,因为类型系统无法表达它们。


理念——为什么类型击败测试 🟢

你将学到: 三层编译时正确性(值、状态、协议),泛型函数签名如何充当编译器检查的保证,以及何时构造正确性模式值得(或不值得)投入。

交叉引用: ch02(类型化命令)、ch05(type-state)、ch13(参考卡)

运行时检查的代价

考虑诊断代码库中典型的运行时守卫:

fn read_sensor(sensor_type: &str, raw: &[u8]) -> f64 {
    match sensor_type {
        "temperature" => raw[0] as i8 as f64,          // 有符号字节
        "fan_speed"   => u16::from_le_bytes([raw[0], raw[1]]) as f64,
        "voltage"     => u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0,
        _             => panic!("unknown sensor type: {sensor_type}"),
    }
}

这个函数有四种失败模式编译器无法捕获:

  1. 拼写错误:"temperture" → 运行时 panic
  2. raw 长度错误:fan_speed 只有 1 字节 → 运行时 panic
  3. 调用者将返回的 f64 当作 RPM 使用,而实际上是 °C → 逻辑 bug,静默失败
  4. 添加了新传感器类型但未更新此 match → 运行时 panic

每种失败模式都是在部署后才发现的。测试有帮助,但它们只覆盖有人想到要写的情况。类型系统覆盖所有情况,包括没人想象到的情况。

三层正确性

第一层 —— 值正确性

使无效值无法表示。

// ❌ 任何 u16 都可以是"port"——0 无效但可以编译
fn connect(port: u16) { /* ... */ }

// ✅ 只有经过验证的 port 才能存在
pub struct Port(u16);  // 私有字段

impl TryFrom<u16> for Port {
    type Error = &'static str;
    fn try_from(v: u16) -> Result<Self, Self::Error> {
        if v > 0 { Ok(Port(v)) } else { Err("port must be > 0") }
    }
}

fn connect(port: Port) { /* ... */ }
// Port(0) 永远无法构造——不变量在任何地方都成立

硬件示例: SensorId(u8) —— 包装原始传感器编号,并验证它在 SDR 范围内。

第二层 —— 状态正确性

使无效转换无法表示。

use std::marker::PhantomData;

struct Disconnected;
struct Connected;

struct Socket<State> {
    fd: i32,
    _state: PhantomData<State>,
}

impl Socket<Disconnected> {
    fn connect(self, addr: &str) -> Socket<Connected> {
        // ... connect logic ...
        Socket { fd: self.fd, _state: PhantomData }
    }
}

impl Socket<Connected> {
    fn send(&mut self, data: &[u8]) { /* ... */ }
    fn disconnect(self) -> Socket<Disconnected> {
        Socket { fd: self.fd, _state: PhantomData }
    }
}

// Socket<Disconnected> 没有 send() 方法——如果尝试则会编译错误

硬件示例: GPIO 引脚模式——Pin<Input> 有 read() 但没有 write()。

第三层 —— 协议正确性

使无效交互无法表示。

use std::io;

trait IpmiCmd {
    type Response;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

// 为说明简化——完整 trait 见 ch02,包含
// net_fn()、cmd_byte()、payload() 和 parse_response()。

struct ReadTemp { sensor_id: u8 }
impl IpmiCmd for ReadTemp {
    type Response = Celsius;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Celsius> {
        Ok(Celsius(raw[0] as i8 as f64))
    }
}

#[derive(Debug)] struct Celsius(f64);

fn execute<C: IpmiCmd>(cmd: &C, raw: &[u8]) -> io::Result<C::Response> {
    cmd.parse_response(raw)
}
// ReadTemp 总是返回 Celsius——不会意外得到 Rpm

硬件示例: IPMI、Redfish、NVMe Admin 命令——请求类型决定响应类型。

类型作为编译器检查的保证

当你写下:

fn execute<C: IpmiCmd>(cmd: &C) -> io::Result<C::Response>

你不仅仅是在写一个函数——你是在声明一个保证:“对于任何实现 IpmiCmd 的命令类型 C,执行它会产生精确的 C::Response。“编译器验证这个保证,每次构建你的代码时都会检查。如果类型不匹配,程序将无法编译。

这就是为什么 Rust 的类型系统如此强大——它不仅仅是在捕获错误,而是在编译时强制执行正确性。

何时不使用这些模式

构造正确性并不总是正确的选择:

情况建议
安全关键边界(电源时序、加密)✅ 始终使用——这里的 bug 会熔化硬件或泄露机密
跨模块公共 API✅ 通常使用——误用应该是编译错误
有 3+ 个状态的状态机✅ 通常使用——type-state 防止错误转换
一个 50 行函数内的内部辅助❌ 过度——简单的 assert! 就足够了
原型设计 / 探索未知硬件❌ 先用原始类型——理解行为后再精炼
面向用户的 CLI 解析⚠️ 边界处用 clap + TryFrom,内部用原始类型即可

关键问题:“如果这个 bug 在生产环境中发生,有多糟糕?”

  • 风扇停止 → GPU 熔化 → 使用类型
  • 错误的 DER 记录 → 客户获得错误数据 → 使用类型
  • 调试日志消息略有错误 → 使用 assert!

关键要点

  1. 三层正确性 —— 值(newtypes)、状态(type-state)、协议(关联类型)—— 每一层消除更广泛的 bug 类别。
  2. 类型作为保证 —— 每个泛型函数签名都是编译器在每次构建时检查的契约。
  3. 代价问题 —— “如果这个 bug 发布出去,有多糟糕?“决定类型还是测试是合适的工具。
  4. 类型补充测试 —— 它们消除整个类别;测试覆盖特定的值和边界情况。
  5. 知道何时停止 —— 内部辅助和临时原型很少需要类型级强制执行。

类型化命令接口——请求决定响应 🟡

你将学到: 命令 trait 上的关联类型如何在请求和响应之间创建编译时绑定,消除 IPMI、Redfish 和 NVMe 协议中的不匹配解析、单位混淆和静默类型强制。

交叉引用: ch01(理念)、ch06(量纲类型)、ch07(有效性边界)、ch10(集成)

无类型泥潭

大多数硬件管理栈——IPMI、Redfish、NVMe Admin、PLDM——开始时都是原始字节进 → 原始字节出。这创造了一类测试只能部分发现的 bug:

use std::io;

struct BmcRaw { /* ipmitool handle */ }

impl BmcRaw {
    fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result<Vec<u8>> {
        // ... shells out to ipmitool ...
        Ok(vec![0x00, 0x19, 0x00]) // stub
    }
}

fn diagnose_thermal(bmc: &BmcRaw) -> io::Result<()> {
    let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?;
    let cpu_temp = raw[0] as f64;        // 🤞 字节 0 是读数吗?

    let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?;
    let fan_rpm = raw[0] as u32;         // 🐛 风扇速度是 2 字节 LE

    let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?;
    let voltage = raw[0] as f64;         // 🐛 需要除以 1000

    if cpu_temp > fan_rpm as f64 {       // 🐛 比较 °C 和 RPM
        println!("uh oh");
    }

    log_temp(voltage);                   // 🐛 传递伏特作为温度
    Ok(())
}

fn log_temp(t: f64) { println!("Temp: {t}°C"); }
#Bug发现时间
1风扇 RPM 解析为 1 字节而非 2生产环境,凌晨 3 点
2电压未缩放所有 PSU 标记为过压
3比较 °C 和 RPM可能永远不会
4伏特传递给温度日志6 个月后,读取历史数据时

根本原因: 一切都是 Vec<u8> → f64 → 祈祷。

类型化命令模式

步骤 1 —— 领域 newtypes

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32);  // u32: 原始 IPMI 传感器值(整数 RPM)

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

关于 Rpm(u32) vs Rpm(f64) 的说明: 本章中内部类型是 u32,因为 IPMI 传感器读数是整数值。在 ch06(量纲分析)中,Rpm 使用 f64 以支持算术运算(平均、缩放)。两者都有效——无论内部类型如何,newtype 都能防止跨单位混淆。

步骤 2 —— 命令 trait(类型索引分发)

关联类型 Response 是关键——它将每个命令结构绑定到其返回类型。每个实现结构将 Response 固定到特定的领域类型,因此 execute() 总是返回精确的正确类型:

pub trait IpmiCmd {
    /// "类型索引" —— 决定 execute() 返回什么。
    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>;
}

步骤 3 —— 每个命令一个结构

pub struct ReadTemp { pub sensor_id: u8 }
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 response"));
        }
        // 注意:ch01 的无类型示例使用 `raw[0] as i8 as f64`(有符号)
        // 因为该函数演示没有 SDR 元数据的通用解析。这里使用无符号(`as f64`)
        // 因为 IPMI 规范 §35.5 中的 SDR 线性化公式将无符号原始读数转换为校准值。
        // 在生产中,应用完整的 SDR 公式:result = (M × raw + B) × 10^(R_exp)。
        Ok(Celsius(raw[0] as f64))  // 无符号原始字节,根据 SDR 公式转换
    }
}

pub struct ReadFanSpeed { pub fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
    type Response = Rpm;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("fan speed needs 2 bytes, got {}", raw.len())));
        }
        Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32))
    }
}

pub struct ReadVoltage { pub rail: u8 }
impl IpmiCmd for ReadVoltage {
    type Response = Volts;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.rail] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Volts> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("voltage needs 2 bytes, got {}", raw.len())));
        }
        Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0))
    }
}

步骤 4 —— 执行器(零 dyn,单态化)

pub struct BmcConnection { pub timeout_secs: u32 }

impl BmcConnection {
    pub fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        let raw = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
        cmd.parse_response(&raw)
    }

    fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![0x19, 0x00]) // stub
    }
}

步骤 5 —— 所有四个 bug 变成编译错误

fn diagnose_thermal_typed(bmc: &BmcConnection) -> io::Result<()> {
    let cpu_temp: Celsius = bmc.execute(&ReadTemp { sensor_id: 0x20 })?;
    let fan_rpm:  Rpm     = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?;
    let voltage:  Volts   = bmc.execute(&ReadVoltage { rail: 0x40 })?;

    // Bug #1 —— 不可能:解析逻辑在 ReadFanSpeed::parse_response 中
    // Bug #2 —— 不可能:单位缩放在 ReadVoltage::parse_response 中

    // Bug #3 —— 编译错误:
    // if cpu_temp > fan_rpm { }
    //    ^^^^^^^^   ^^^^^^^ Celsius vs Rpm → "类型不匹配" ❌

    // Bug #4 —— 编译错误:
    // log_temperature(voltage);
    //                 ^^^^^^^ Volts, 期望 Celsius ❌

    if cpu_temp > Celsius(85.0) { println!("CPU overheating: {:?}", cpu_temp); }
    if fan_rpm < Rpm(4000)      { println!("Fan too slow: {:?}", fan_rpm); }

    Ok(())
}

fn log_temperature(t: Celsius) { println!("Temp: {:?}", t); }
fn log_voltage(v: Volts)       { println!("Voltage: {:?}", v); }

IPMI:无法混淆的传感器读数

添加新传感器是一个结构 + 一个实现——没有分散的解析:

pub struct ReadPowerDraw { pub domain: u8 }
impl IpmiCmd for ReadPowerDraw {
    type Response = Watts;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.domain] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Watts> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData,
                format!("power draw needs 2 bytes, got {}", raw.len())));
        }
        Ok(Watts(u16::from_le_bytes([raw[0], raw[1]]) as f64))
    }
}

// 任何使用 bmc.execute(&ReadPowerDraw { domain: 0 }) 的代码
// 自动获得 Watts 返回——其他地方没有解析代码

隔离测试每个命令

#[cfg(test)]
mod tests {
    use super::*;

    struct StubBmc {
        responses: std::collections::HashMap<u8, Vec<u8>>,
    }

    impl StubBmc {
        fn execute<C: IpmiCmd>(&self, cmd: &C) -> io::Result<C::Response> {
            let key = cmd.payload()[0];
            let raw = self.responses.get(&key)
                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no stub"))?;
            cmd.parse_response(raw)
        }
    }

    #[test]
    fn read_temp_parses_raw_byte() {
        let bmc = StubBmc {
            responses: [(0x20, vec![0x19])].into(), // 25 decimal = 0x19
        };
        let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap();
        assert_eq!(temp, Celsius(25.0));
    }

    #[test]
    fn read_fan_parses_two_byte_le() {
        let bmc = StubBmc {
            responses: [(0x30, vec![0x00, 0x19])].into(), // 0x1900 = 6400
        };
        let rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 }).unwrap();
        assert_eq!(rpm, Rpm(6400));
    }

    #[test]
    fn read_voltage_scales_millivolts() {
        let bmc = StubBmc {
            responses: [(0x40, vec![0xE8, 0x2E])].into(), // 0x2EE8 = 12008 mV
        };
        let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap();
        assert!((v.0 - 12.008).abs() < 0.001);
    }
}

Redfish:模式类型的 REST 端点

Redfish 甚至是更好的例子——每个端点返回 DMTF 定义的 JSON schema:

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct ThermalResponse {
    #[serde(rename = "Temperatures")]
    pub temperatures: Vec<RedfishTemp>,
    #[serde(rename = "Fans")]
    pub fans: Vec<RedfishFan>,
}

#[derive(Debug, Deserialize)]
pub struct RedfishTemp {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "ReadingCelsius")]
    pub reading: f64,
    #[serde(rename = "UpperThresholdCritical")]
    pub critical_hi: Option<f64>,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishFan {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "Reading")]
    pub rpm: u32,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct PowerResponse {
    #[serde(rename = "Voltages")]
    pub voltages: Vec<RedfishVoltage>,
    #[serde(rename = "PowerSupplies")]
    pub psus: Vec<RedfishPsu>,
}

#[derive(Debug, Deserialize)]
pub struct RedfishVoltage {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "ReadingVolts")]
    pub reading: f64,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishPsu {
    #[serde(rename = "Name")]
    pub name: String,
    #[serde(rename = "PowerOutputWatts")]
    pub output_watts: Option<f64>,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct ProcessorResponse {
    #[serde(rename = "Model")]
    pub model: String,
    #[serde(rename = "TotalCores")]
    pub cores: u32,
    #[serde(rename = "Status")]
    pub status: RedfishHealth,
}

#[derive(Debug, Deserialize)]
pub struct RedfishHealth {
    #[serde(rename = "State")]
    pub state: String,
    #[serde(rename = "Health")]
    pub health: Option<String>,
}

/// 类型化 Redfish 端点——每个知道自己的响应类型。
pub trait RedfishEndpoint {
    type Response: serde::de::DeserializeOwned;
    fn method(&self) -> &'static str;
    fn path(&self) -> String;
}

pub struct GetThermal { pub chassis_id: String }
impl RedfishEndpoint for GetThermal {
    type Response = ThermalResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Chassis/{}/Thermal", self.chassis_id)
    }
}

pub struct GetPower { pub chassis_id: String }
impl RedfishEndpoint for GetPower {
    type Response = PowerResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Chassis/{}/Power", self.chassis_id)
    }
}

pub struct GetProcessor { pub system_id: String, pub proc_id: String }
impl RedfishEndpoint for GetProcessor {
    type Response = ProcessorResponse;
    fn method(&self) -> &'static str { "GET" }
    fn path(&self) -> String {
        format!("/redfish/v1/Systems/{}/Processors/{}", self.system_id, self.proc_id)
    }
}

pub struct RedfishClient {
    pub base_url: String,
    pub auth_token: String,
}

impl RedfishClient {
    pub fn execute<E: RedfishEndpoint>(&self, endpoint: &E) -> io::Result<E::Response> {
        let url = format!("{}{}", self.base_url, endpoint.path());
        let json_bytes = self.http_request(endpoint.method(), &url)?;
        serde_json::from_slice(&json_bytes)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
    }

    fn http_request(&self, _method: &str, _url: &str) -> io::Result<Vec<u8>> {
        Ok(vec![]) // stub — 真实实现使用 reqwest/hyper
    }
}

// 用法——完全类型化,自文档化
fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> {
    let thermal: ThermalResponse = client.execute(&GetThermal {
        chassis_id: "1".into(),
    })?;
    let power: PowerResponse = client.execute(&GetPower {
        chassis_id: "1".into(),
    })?;

    // ❌ 编译错误——不能传递 PowerResponse 给 thermal 检查:
    // check_thermals(&power);  → "期望 ThermalResponse, 找到 PowerResponse"

    for temp in &thermal.temperatures {
        if let Some(crit) = temp.critical_hi {
            if temp.reading > crit {
                println!("CRITICAL: {} at {}°C (threshold: {}°C)",
                    temp.name, temp.reading, crit);
            }
        }
    }
    Ok(())
}

NVMe Admin:Identify 不返回日志页

NVMe 管理命令遵循相同的形状。控制器区分命令操作码,但在 C 中调用者必须知道在 4 KB 完成缓冲区上覆盖哪个结构。类型化命令模式使这不可能出错:

use std::io;

/// NVMe Admin 命令 trait——与 IpmiCmd 形状相同。
pub trait NvmeAdminCmd {
    type Response;
    fn opcode(&self) -> u8;
    fn parse_completion(&self, data: &[u8]) -> io::Result<Self::Response>;
}

// ── Identify (opcode 0x06) ──

#[derive(Debug, Clone)]
pub struct IdentifyResponse {
    pub model_number: String,   // 字节 24–63
    pub serial_number: String,  // 字节 4–23
    pub firmware_rev: String,   // 字节 64–71
    pub total_capacity_gb: u64,
}

pub struct Identify {
    pub nsid: u32, // 0 = 控制器,>0 = 命名空间
}

impl NvmeAdminCmd for Identify {
    type Response = IdentifyResponse;
    fn opcode(&self) -> u8 { 0x06 }
    fn parse_completion(&self, data: &[u8]) -> io::Result<IdentifyResponse> {
        if data.len() < 4096 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "short identify"));
        }
        Ok(IdentifyResponse {
            serial_number: String::from_utf8_lossy(&data[4..24]).trim().to_string(),
            model_number: String::from_utf8_lossy(&data[24..64]).trim().to_string(),
            firmware_rev: String::from_utf8_lossy(&data[64..72]).trim().to_string(),
            total_capacity_gb: u64::from_le_bytes(
                data[280..288].try_into().unwrap()
            ) / (1024 * 1024 * 1024),
        })
    }
}

// ── Get Log Page (opcode 0x02) ──

#[derive(Debug, Clone)]
pub struct SmartLog {
    pub critical_warning: u8,
    pub temperature_kelvin: u16,
    pub available_spare_pct: u8,
    pub data_units_read: u128,
}

pub struct GetLogPage {
    pub log_id: u8, // 0x02 = SMART/Health
}

impl NvmeAdminCmd for GetLogPage {
    type Response = SmartLog;
    fn opcode(&self) -> u8 { 0x02 }
    fn parse_completion(&self, data: &[u8]) -> io::Result<SmartLog> {
        if data.len() < 512 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "short log page"));
        }
        Ok(SmartLog {
            critical_warning: data[0],
            temperature_kelvin: u16::from_le_bytes([data[1], data[2]]),
            available_spare_pct: data[3],
            data_units_read: u128::from_le_bytes(data[32..48].try_into().unwrap()),
        })
    }
}

// ── 执行器 ──

pub struct NvmeController { /* fd, BAR, etc. */ }

impl NvmeController {
    pub fn admin_cmd<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        let raw = self.submit_and_wait(cmd.opcode())?;
        cmd.parse_completion(&raw)
    }

    fn submit_and_wait(&self, _opcode: u8) -> io::Result<Vec<u8>> {
        Ok(vec![0u8; 4096]) // stub — 真实实现发出 doorbell + 等待 CQ 条目
    }
}

// ── 用法 ──

fn nvme_health_check(ctrl: &NvmeController) -> io::Result<()> {
    let id: IdentifyResponse = ctrl.admin_cmd(&Identify { nsid: 0 })?;
    let smart: SmartLog = ctrl.admin_cmd(&GetLogPage { log_id: 0x02 })?;

    // ❌ 编译错误——Identify 返回 IdentifyResponse,不是 SmartLog:
    // let smart: SmartLog = ctrl.admin_cmd(&Identify { nsid: 0 })?;

    println!("{} (FW {}): {}°C, {}% spare",
        id.model_number, id.firmware_rev,
        smart.temperature_kelvin.saturating_sub(273),
        smart.available_spare_pct);

    Ok(())
}

这三种协议的进展现在遵循一个渐进的弧线(ch07 对有效性边界使用的相同技术):

节拍协议复杂度添加内容
1IPMI简单:传感器 ID → 读数核心模式:trait + 关联类型
2RedfishREST:端点 → 类型化 JSONSerde 集成,模式类型响应
3NVMe二进制:操作码 → 4 KB 结构覆盖原始缓冲区解析,多结构完成数据

扩展:命令脚本的宏 DSL

/// 执行一系列类型化 IPMI 命令,返回结果元组。
macro_rules! diag_script {
    ($bmc:expr; $($cmd:expr),+ $(,)?) => {{
        ( $( $bmc.execute(&$cmd)?, )+ )
    }};
}

fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> {
    let (temp, rpm, volts) = diag_script!(bmc;
        ReadTemp     { sensor_id: 0x20 },
        ReadFanSpeed { fan_id:    0x30 },
        ReadVoltage  { rail:      0x40 },
    );
    // 类型:(Celsius, Rpm, Volts) —— 完全推断,交换 = 编译错误
    assert!(temp  < Celsius(95.0), "CPU 太热");
    assert!(rpm   > Rpm(3000),     "风扇太慢");
    assert!(volts > Volts(11.4),   "12V 轨下降");
    Ok(())
}

扩展:用于动态脚本的 Enum 分发

当命令在运行时来自 JSON 配置时:

pub enum AnyReading {
    Temp(Celsius),
    Rpm(Rpm),
    Volt(Volts),
    Watt(Watts),
}

pub enum AnyCmd {
    Temp(ReadTemp),
    Fan(ReadFanSpeed),
    Voltage(ReadVoltage),
    Power(ReadPowerDraw),
}

impl AnyCmd {
    pub fn execute(&self, bmc: &BmcConnection) -> io::Result<AnyReading> {
        match self {
            AnyCmd::Temp(c)    => Ok(AnyReading::Temp(bmc.execute(c)?)),
            AnyCmd::Fan(c)     => Ok(AnyReading::Rpm(bmc.execute(c)?)),
            AnyCmd::Voltage(c) => Ok(AnyReading::Volt(bmc.execute(c)?)),
            AnyCmd::Power(c)   => Ok(AnyReading::Watt(bmc.execute(c)?)),
        }
    }
}

fn run_dynamic_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result<Vec<AnyReading>> {
    script.iter().map(|cmd| cmd.execute(bmc)).collect()
}

模式家族

这个模式适用于每个硬件管理协议:

协议请求类型响应类型
IPMI 传感器读数ReadTempCelsius
Redfish RESTGetThermalThermalResponse
NVMe AdminIdentifyIdentifyResponse
PLDMGetFwParamsFwParamsResponse
MCTPGetEidEidResponse
PCIe 配置空间ReadCapabilityCapabilityHeader
SMBIOS/DMIReadType17MemoryDeviceInfo

请求类型决定响应类型——编译器在每个地方强制执行它。

类型化命令流程

flowchart LR
    subgraph "编译时"
        RT["ReadTemp"] -->|"type Response = Celsius"| C[Celsius]
        RF["ReadFanSpeed"] -->|"type Response = Rpm"| R[Rpm]
        RV["ReadVoltage"] -->|"type Response = Volts"| V[Volts]
    end
    subgraph "运行时"
        E["bmc.execute(&cmd)"] -->|"单态化"| P["cmd.parse_response(raw)"]
    end
    style RT fill:#e1f5fe,color:#000
    style RF fill:#e1f5fe,color:#000
    style RV fill:#e1f5fe,color:#000
    style C fill:#c8e6c9,color:#000
    style R fill:#c8e6c9,color:#000
    style V fill:#c8e6c9,color:#000
    style E fill:#fff3e0,color:#000
    style P fill:#fff3e0,color:#000

练习:PLDM 类型化命令

设计一个 PldmCmd trait(与 IpmiCmd 形状相同)用于两个 PLDM 命令:

  • GetFwParams → FwParamsResponse { active_version: String, pending_version: Option<String> }
  • QueryDeviceIds → DeviceIdResponse { descriptors: Vec<Descriptor> }

要求:静态分发,parse_response 返回 io::Result<Self::Response>。

解决方案
use std::io;

pub trait PldmCmd {
    type Response;
    fn pldm_type(&self) -> u8;
    fn command_code(&self) -> u8;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

#[derive(Debug, Clone)]
pub struct FwParamsResponse {
    pub active_version: String,
    pub pending_version: Option<String>,
}

pub struct GetFwParams;
impl PldmCmd for GetFwParams {
    type Response = FwParamsResponse;
    fn pldm_type(&self) -> u8 { 0x05 } // Firmware Update
    fn command_code(&self) -> u8 { 0x02 }
    fn parse_response(&self, raw: &[u8]) -> io::Result<FwParamsResponse> {
        // 简化——真实实现解码 PLDM FW Update 规范字段
        if raw.len() < 4 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "too short"));
        }
        Ok(FwParamsResponse {
            active_version: String::from_utf8_lossy(&raw[..4]).to_string(),
            pending_version: None,
        })
    }
}

#[derive(Debug, Clone)]
pub struct Descriptor { pub descriptor_type: u16, pub data: Vec<u8> }

#[derive(Debug, Clone)]
pub struct DeviceIdResponse { pub descriptors: Vec<Descriptor> }

pub struct QueryDeviceIds;
impl PldmCmd for QueryDeviceIds {
    type Response = DeviceIdResponse;
    fn pldm_type(&self) -> u8 { 0x05 }
    fn command_code(&self) -> u8 { 0x04 }
    fn parse_response(&self, raw: &[u8]) -> io::Result<DeviceIdResponse> {
        Ok(DeviceIdResponse { descriptors: vec![] }) // stub
    }
}

关键要点

  1. 关联类型 = 编译时契约 —— 命令 trait 上的 type Response 将每个请求锁定到精确的一个响应类型。
  2. 解析被封装 —— 字节布局知识存在于 parse_response 中,而不是分散在调用者中。
  3. 零成本分发 —— 泛型 execute<C: IpmiCmd> 单态化为直接调用,没有虚表。
  4. 一个模式,多个协议 —— IPMI、Redfish、NVMe、PLDM、MCTP 都适合同一个 trait Cmd { type Response; } 形状。
  5. Enum 分发桥接静态和动态 —— 将类型化命令包装在 enum 中用于运行时驱动的脚本,同时不丢失每个分支内的类型安全。
  6. 渐进复杂度增强直觉 —— IPMI(传感器 ID → 读数)、Redfish(端点 → JSON schema)和 NVMe(操作码 → 4 KB 结构覆盖)都使用相同的 trait 形状,但每个节拍增加一层解析复杂度。

一次性类型——通过所有权实现加密保证 🟡

你将学到: Rust 的移动语义如何充当线性类型系统,使 nonce 重用、双重密钥协商和意外熔丝重编程在编译时变得不可能。

交叉引用: ch01(理念)、ch04(capability 令牌)、ch05(type-state)、ch14(测试编译失败)

Nonce 重用灾难

在认证加密(AES-GCM、ChaCha20-Poly1305)中,对同一个 key 重用 nonce 是灾难性的——它会泄露两个明文的 XOR,甚至可能泄露认证密钥本身。这不是理论上的担忧:

  • 2016 年:TLS 中 AES-GCM 的 Forbidden Attack——nonce 重用允许恢复明文
  • 2020 年:多个 IoT 固件更新系统因 RNG 不佳而发现重用 nonce

在 C/C++ 中,nonce 只是一个 uint8_t[12]。没有什么能阻止你使用它两次。

// C — 没有什么能阻止 nonce 重用
uint8_t nonce[12];
generate_nonce(nonce);
encrypt(key, nonce, msg1, out1);   // ✅ 第一次使用
encrypt(key, nonce, msg2, out2);   // 🐛 灾难:相同的 nonce

移动语义作为线性类型

Rust 的所有权系统实际上是一个线性类型系统——一个值只能被使用一次(被移动),除非它实现了 Copy。ring crate 利用了这一点:

// ring::aead::Nonce 是:
// - NOT Clone
// - NOT Copy
// - 使用时按值消费
pub struct Nonce(/* private */);

impl Nonce {
    pub fn try_assume_unique_for_key(value: &[u8]) -> Result<Self, Unspecified> {
        // ...
    }
    // 没有 Clone,没有 Copy——只能使用一次
}

当你将 Nonce 传递给 seal_in_place() 时,它被移动了:

// 伪代码,镜像 ring 的 API 形状
fn seal_in_place(
    key: &SealingKey,
    nonce: Nonce,       // ← 被移动,不是借用
    data: &mut Vec<u8>,
) -> Result<(), Error> {
    // ... 原地加密数据 ...
    // nonce 被消费——不能再使用
    Ok(())
}

尝试重用它会得到:

fn bad_encrypt(key: &SealingKey, data1: &mut Vec<u8>, data2: &mut Vec<u8>) {
    // .unwrap() 是安全的——12 字节数组总是有效的 nonce
    let nonce = Nonce::try_assume_unique_for_key(&[0u8; 12]).unwrap();
    seal_in_place(key, nonce, data1).unwrap();  // ✅ nonce 在这里被移动
    // seal_in_place(key, nonce, data2).unwrap();
    //                    ^^^^^ 错误:使用已移动的值 ❌
}

编译器证明了每个 nonce 只被使用一次。不需要测试。

案例研究:ring 的 Nonce

ring crate 更进一步,提供了 NonceSequence trait——它生成 nonce,也是不可克隆的:

/// 唯一 nonce 的序列。
/// 不是 Clone——一旦绑定到 key,就不能复制。
pub trait NonceSequence {
    fn advance(&mut self) -> Result<Nonce, Unspecified>;
}

/// SealingKey 封装了 NonceSequence——每次 seal() 自动推进。
pub struct SealingKey<N: NonceSequence> {
    key: UnboundKey,   // 在构造期间被消费
    nonce_seq: N,
}

impl<N: NonceSequence> SealingKey<N> {
    pub fn new(key: UnboundKey, nonce_seq: N) -> Self {
        // UnboundKey 被移动——不能同时用于密封和解封
        SealingKey { key, nonce_seq }
    }

    pub fn seal_in_place_append_tag(
        &mut self,       // &mut——独占访问
        aad: Aad<&[u8]>,
        in_out: &mut Vec<u8>,
    ) -> Result<(), Unspecified> {
        let nonce = self.nonce_seq.advance()?; // 自动生成唯一 nonce
        // ... 用 nonce 加密 ...
        Ok(())
    }
}
pub struct UnboundKey;
pub struct Aad<T>(T);
pub struct Unspecified;

所有权链防止了:

  1. Nonce 重用——Nonce 不是 Clone,每次调用时被消费
  2. Key 复制——UnboundKey 被移动到 SealingKey 中,不能同时创建 OpeningKey
  3. Sequence 复制——NonceSequence 不是 Clone,所以没有两个 key 共享同一个计数器

这些都不需要运行时检查。 编译器强制执行所有三项。

案例研究:临时密钥协商

临时 Diffie-Hellman key 必须恰好使用一次(这就是“临时“的含义)。ring 强制执行这一点:

/// 临时私钥。不是 Clone,不是 Copy。
/// 被 agree_ephemeral() 消费。
pub struct EphemeralPrivateKey { /* ... */ }

/// 计算共享密钥——消费私钥。
pub fn agree_ephemeral(
    my_private_key: EphemeralPrivateKey,  // ← 被移动
    peer_public_key: &UnparsedPublicKey,
    error_value: Unspecified,
    kdf: impl FnOnce(&[u8]) -> Result<SharedSecret, Unspecified>,
) -> Result<SharedSecret, Unspecified> {
    // ... DH 计算 ...
    // my_private_key 被消费——永远无法重用
    kdf(&[])
}
pub struct UnparsedPublicKey;
pub struct SharedSecret;
pub struct Unspecified;

调用 agree_ephemeral() 后,私钥在内存中不再存在(它已被 drop)。C++ 开发者需要记住 memset(key, 0, len),并希望编译器不要优化掉它。在 Rust 中,key 简单地消失了。

硬件应用:一次性熔丝编程

服务器平台有OTP(一次性可编程)熔丝用于安全密钥、板序列号和特性位。写入熔丝是不可逆的——用不同数据写入两次会使板子变砖。这是移动语义的完美应用场景:

use std::io;

/// 熔丝写入负载。不是 Clone,不是 Copy。
/// 在熔丝编程时被消费。
pub struct FusePayload {
    address: u32,
    data: Vec<u8>,
    // 私有构造函数——只能通过经过验证的 builder 创建
}

/// 熔丝编程器处于正确状态的证明。
pub struct FuseController {
    /* 硬件句柄 */
}

impl FuseController {
    /// 编程熔丝——消费负载,防止双重写入。
    pub fn program(
        &mut self,
        payload: FusePayload,  // ← 被移动——不能使用两次
    ) -> io::Result<()> {
        // ... 写入 OTP 硬件 ...
        // payload 被消费——尝试用相同的负载再次编程
        // 是编译错误
        Ok(())
    }
}

/// 带有验证的 builder——创建 FusePayload 的唯一方式。
pub struct FusePayloadBuilder {
    address: Option<u32>,
    data: Option<Vec<u8>>,
}

impl FusePayloadBuilder {
    pub fn new() -> Self {
        FusePayloadBuilder { address: None, data: None }
    }

    pub fn address(mut self, addr: u32) -> Self {
        self.address = Some(addr);
        self
    }

    pub fn data(mut self, data: Vec<u8>) -> Self {
        self.data = Some(data);
        self
    }

    pub fn build(self) -> Result<FusePayload, &'static str> {
        let address = self.address.ok_or("需要 address")?;
        let data = self.data.ok_or("需要 data")?;
        if data.len() > 32 { return Err("熔丝数据过长"); }
        Ok(FusePayload { address, data })
    }
}

// 用法:
fn program_board_serial(ctrl: &mut FuseController) -> io::Result<()> {
    let payload = FusePayloadBuilder::new()
        .address(0x100)
        .data(b"SN12345678".to_vec())
        .build()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;

    ctrl.program(payload)?;      // ✅ payload 被消费

    // ctrl.program(payload);    // ❌ 错误:使用已移动的值
    //              ^^^^^^^ 值在移动后使用

    Ok(())
}

硬件应用:一次性校准令牌

某些传感器需要一个校准步骤,每个电源周期必须恰好发生一次。校准令牌强制执行这一点:

/// 在通电时发放一次。不是 Clone,不是 Copy。
pub struct CalibrationToken {
    _private: (),
}

pub struct SensorController {
    calibrated: bool,
}

impl SensorController {
    /// 在通电时调用——返回校准令牌。
    pub fn power_on() -> (Self, CalibrationToken) {
        (
            SensorController { calibrated: false },
            CalibrationToken { _private: () },
        )
    }

    /// 校准传感器——消费令牌。
    pub fn calibrate(&mut self, _token: CalibrationToken) -> io::Result<()> {
        // ... 运行校准序列 ...
        self.calibrated = true;
        Ok(())
    }

    /// 读取传感器——只有在校准后才有意义。
    ///
    /// **局限性:** 移动语义保证是*部分的*。调用者可以
    /// `drop(cal_token)` 而不调用 `calibrate()`——令牌会被
    /// 销毁但校准不会运行。`#[must_use]` 注解(见下文)
    /// 会生成警告,但不是硬性错误。
    ///
    /// 这里运行时 `self.calibrated` 检查是**安全网**。
    /// 对于完全的编译时解决方案,见 ch05 中的 type-state
    /// 模式,其中 `send_command()` 只存在于 `IpmiSession<Active>` 上。
    pub fn read(&self) -> io::Result<f64> {
        if !self.calibrated {
            return Err(io::Error::new(io::ErrorKind::Other, "未校准"));
        }
        Ok(25.0) // 存根
    }
}

fn sensor_workflow() -> io::Result<()> {
    let (mut ctrl, cal_token) = SensorController::power_on();

    // 必须在某处使用 cal_token——它不是 Copy,所以在没有消费它的情况下
    // drop 它会产生警告(或在 #[must_use] 下产生错误)
    ctrl.calibrate(cal_token)?;

    // 现在读取有效:
    let temp = ctrl.read()?;
    println!("Temperature: {temp}°C");

    // 不能再校准——令牌已被消费:
    // ctrl.calibrate(cal_token);  // ❌ 使用已移动的值

    Ok(())
}

何时使用一次性类型

场景使用一次性(移动)语义?
加密 nonce✅ 总是——nonce 重用是灾难性的
临时密钥(DH、ECDH)✅ 总是——重用削弱前向保密性
OTP 熔丝写入✅ 总是——双重写入会损坏硬件
许可证激活码✅ 通常——防止双重激活
校准令牌✅ 通常——每会话强制一次
文件写入句柄⚠️ 有时——取决于协议
数据库事务句柄⚠️ 有时——提交/回滚是一次性的
通用数据缓冲区❌ 这些需要重用——使用 &mut [u8]

一次性所有权流程

flowchart LR
    N["Nonce::new()"] -->|move| E["encrypt(nonce, msg)"]
    E -->|consumed| X["❌ nonce 消失"]
    N -.->|"reuse attempt"| ERR["编译错误:<br/>使用已移动的值"]
    style N fill:#e1f5fe,color:#000
    style E fill:#c8e6c9,color:#000
    style X fill:#ffcdd2,color:#000
    style ERR fill:#ffcdd2,color:#000

练习:一次性固件签名令牌

设计一个 SigningToken,它只能使用一次来签名固件镜像:

  • SigningToken::issue(key_id: &str) -> SigningToken(不是 Clone,不是 Copy)
  • sign(token: SigningToken, image: &[u8]) -> SignedImage(消费令牌)
  • 尝试签名两次应该是编译错误。
解决方案
pub struct SigningToken {
    key_id: String,
    // 不是 Clone,不是 Copy
}

pub struct SignedImage {
    pub signature: Vec<u8>,
    pub key_id: String,
}

impl SigningToken {
    pub fn issue(key_id: &str) -> Self {
        SigningToken { key_id: key_id.to_string() }
    }
}

pub fn sign(token: SigningToken, _image: &[u8]) -> SignedImage {
    // 令牌通过移动被消费——不能重用
    SignedImage {
        signature: vec![0xDE, 0xAD],  // 存根
        key_id: token.key_id,
    }
}

// ✅ 编译通过:
// let tok = SigningToken::issue("release-key");
// let signed = sign(tok, &firmware_bytes);
//
// ❌ 编译错误:
// let signed2 = sign(tok, &other_bytes);  // 错误:使用已移动的值

关键要点

  1. 移动 = 线性使用——一个非 Clone、非 Copy 的类型只能被消费一次;编译器强制执行这一点。
  2. Nonce 重用是灾难性的——Rust 的所有权系统从结构上防止它,而不是依靠纪律。
  3. 模式适用于加密之外——OTP 熔丝、校准令牌、审计条目——任何最多发生一次的事情。
  4. 临时密钥免费获得前向保密性——密钥协商值被移动到派生的秘密中并消失。
  5. 如有疑问,移除 Clone——你以后总是可以添加它;从已发布的 API 中移除它是一个破坏性变更。

Capability 令牌——零成本权限证明 🟡

你将学到: 零大小类型(ZST)如何充当编译时证明令牌,强制执行权限层次结构、电源时序和可撤销权限——全部零运行时成本。

交叉引用: ch03(一次性类型)、ch05(type-state)、ch08(mixins)、ch10(集成)

问题:谁被允许做什么?

在硬件诊断中,某些操作是危险的:

  • 编程 BMC 固件
  • 重置 PCIe 链路
  • 写入 OTP 熔丝
  • 启用高压测试模式

在 C/C++ 中,这些由运行时检查守卫:

// C — 运行时权限检查
int reset_pcie_link(bmc_handle_t bmc, int slot) {
    if (!bmc->is_admin) {        // 运行时检查
        return -EPERM;
    }
    if (!bmc->link_trained) {    // 另一个运行时检查
        return -EINVAL;
    }
    // ... 做危险的事情 ...
    return 0;
}

每个执行危险操作的函数都必须重复这些检查。忘记一个,就产生了权限提升 bug。

零大小类型作为证明令牌

capability 令牌是一个零大小类型(ZST),证明调用者有权执行某个操作。它在运行时成本为零字节——它只存在于类型系统中:

use std::marker::PhantomData;

/// 调用者具有管理员权限的证明。
/// 零大小——完全编译消失。
/// 不是 Clone,不是 Copy——必须显式传递。
pub struct AdminToken {
    _private: (),   // 防止在此模块外构造
}

/// PCIe 链路已训练并就绪的证明。
pub struct LinkTrainedToken {
    _private: (),
}

pub struct BmcController { /* ... */ }

impl BmcController {
    /// 以管理员身份认证——返回 capability 令牌。
    /// 这是创建 AdminToken 的唯一方式。
    pub fn authenticate_admin(
        &mut self,
        credentials: &[u8],
    ) -> Result<AdminToken, &'static str> {
        // ... 验证凭证 ...
        let valid = true;
        if valid {
            Ok(AdminToken { _private: () })
        } else {
            Err("认证失败")
        }
    }

    /// 训练 PCIe 链路——返回已训练的证明。
    pub fn train_link(&mut self) -> Result<LinkTrainedToken, &'static str> {
        // ... 执行链路训练 ...
        Ok(LinkTrainedToken { _private: () })
    }

    /// 重置 PCIe 链路——需要管理员 + 链路训练证明。
    /// 不需要运行时检查——令牌就是证明。
    pub fn reset_pcie_link(
        &mut self,
        _admin: &AdminToken,         // 零成本权限证明
        _trained: &LinkTrainedToken,  // 零成本状态证明
        slot: u32,
    ) -> Result<(), &'static str> {
        println!("正在重置插槽 {slot} 上的 PCIe 链路");
        Ok(())
    }
}

用法——类型系统强制执行工作流程:

fn maintenance_workflow(bmc: &mut BmcController) -> Result<(), &'static str> {
    // 步骤 1:认证——获取管理员证明
    let admin = bmc.authenticate_admin(b"secret")?;

    // 步骤 2:训练链路——获取训练证明
    let trained = bmc.train_link()?;

    // 步骤 3:重置——编译器要求两个令牌
    bmc.reset_pcie_link(&admin, &trained, 0)?;

    Ok(())
}

// 这将无法编译:
fn unprivileged_attempt(bmc: &mut BmcController) -> Result<(), &'static str> {
    let trained = bmc.train_link()?;
    // bmc.reset_pcie_link(???, &trained, 0)?;
    //                     ^^^ 没有 AdminToken——无法调用
    Ok(())
}

AdminToken 和 LinkTrainedToken 在编译后的二进制文件中是零字节。它们只存在于类型检查期间。函数签名 fn reset_pcie_link(&mut self, _admin: &AdminToken, ...) 是一个证明义务——“只有当你能生成 AdminToken 时才能调用”,而生成它的唯一方式是通过 authenticate_admin()。

电源时序权限

服务器电源时序有严格的顺序:standby → auxiliary → main → CPU。 reversing 顺序可能损坏硬件。Capability 令牌强制执行顺序:

/// 状态令牌——每个令牌证明前一步骤已完成。
pub struct StandbyOn { _p: () }
pub struct AuxiliaryOn { _p: () }
pub struct MainOn { _p: () }
pub struct CpuPowered { _p: () }

pub struct PowerController { /* ... */ }

impl PowerController {
    /// 步骤 1:启用 standby 电源。无条件。
    pub fn enable_standby(&mut self) -> Result<StandbyOn, &'static str> {
        println!("Standby 电源开启");
        Ok(StandbyOn { _p: () })
    }

    /// 步骤 2:启用 auxiliary——需要 standby 证明。
    pub fn enable_auxiliary(
        &mut self,
        _standby: &StandbyOn,
    ) -> Result<AuxiliaryOn, &'static str> {
        println!("Auxiliary 电源开启");
        Ok(AuxiliaryOn { _p: () })
    }

    /// 步骤 3:启用 main——需要 auxiliary 证明。
    pub fn enable_main(
        &mut self,
        _aux: &AuxiliaryOn,
    ) -> Result<MainOn, &'static str> {
        println!("Main 电源开启");
        Ok(MainOn { _p: () })
    }

    /// 步骤 4:CPU 上电——需要 main 证明。
    pub fn power_cpu(
        &mut self,
        _main: &MainOn,
    ) -> Result<CpuPowered, &'static str> {
        println!("CPU 上电");
        Ok(CpuPowered { _p: () })
    }
}

fn power_on_sequence(ctrl: &mut PowerController) -> Result<CpuPowered, &'static str> {
    let standby = ctrl.enable_standby()?;
    let aux = ctrl.enable_auxiliary(&standby)?;
    let main = ctrl.enable_main(&aux)?;
    let cpu = ctrl.power_cpu(&main)?;
    Ok(cpu)
}

// 尝试跳过步骤:
// fn wrong_order(ctrl: &mut PowerController) {
//     ctrl.power_cpu(???);  // ❌ 无法在没有 enable_main() 的情况下生成 MainOn
// }

层次化 Capability

真实系统有层次结构——管理员可以做用户能做的所有事情,外加更多。用 trait 层次结构建模:

/// 基础 capability——任何经过认证的人。
pub trait Authenticated {
    fn token_id(&self) -> u64;
}

/// Operator 可以读取传感器并运行非破坏性诊断。
pub trait Operator: Authenticated {}

/// Admin 可以做 operator 能做的所有事情,外加破坏性操作。
pub trait Admin: Operator {}

// 具体令牌:
pub struct UserToken { id: u64 }
pub struct OperatorToken { id: u64 }
pub struct AdminCapToken { id: u64 }

impl Authenticated for UserToken { fn token_id(&self) -> u64 { self.id } }
impl Authenticated for OperatorToken { fn token_id(&self) -> u64 { self.id } }
impl Operator for OperatorToken {}
impl Authenticated for AdminCapToken { fn token_id(&self) -> u64 { self.id } }
impl Operator for AdminCapToken {}
impl Admin for AdminCapToken {}

pub struct Bmc { /* ... */ }

impl Bmc {
    /// 任何经过认证的人都可以读取传感器。
    pub fn read_sensor(&self, _who: &impl Authenticated, id: u32) -> f64 {
        42.0 // 存根
    }

    /// 只有 operator 及以上可以运行诊断。
    pub fn run_diag(&mut self, _who: &impl Operator, test: &str) -> bool {
        true // 存根
    }

    /// 只有 admin 可以刷写固件。
    pub fn flash_firmware(&mut self, _who: &impl Admin, image: &[u8]) -> Result<(), &'static str> {
        Ok(()) // 存根
    }
}

AdminCapToken 可以传递给任何函数——它满足 Authenticated、Operator 和 Admin。UserToken 只能调用 read_sensor()。编译器强制执行整个权限模型,零运行时成本。

生命周期受限的 Capability 令牌

有时 capability 应该是有范围的——只在特定生命周期内有效。Rust 的借用检查器自然处理这一点:

/// 有范围的管理员会话。令牌借用会话,
/// 所以不能超越它。
pub struct AdminSession {
    _active: bool,
}

pub struct ScopedAdminToken<'session> {
    _session: &'session AdminSession,
}

impl AdminSession {
    pub fn begin(credentials: &[u8]) -> Result<Self, &'static str> {
        // ... 认证 ...
        Ok(AdminSession { _active: true })
    }

    /// 创建有范围的令牌——生命周期不超过会话。
    pub fn token(&self) -> ScopedAdminToken<'_> {
        ScopedAdminToken { _session: self }
    }
}

fn scoped_example() -> Result<(), &'static str> {
    let session = AdminSession::begin(b"credentials")?;
    let token = session.token();

    // 在此作用域内使用令牌...
    // 当 session drop 时,令牌被借用检查器失效。
    // 不需要运行时过期检查。

    // drop(session);
    // ❌ 错误:无法移出 `session`,因为它被借用
    //    (被 `token` 借用,持有 &session)
    //
    // 即使我们跳过 drop() 并尝试在
    // session 超出作用域后使用 `token`——同样是错误:生命周期不匹配。

    Ok(())
}

何时使用 Capability 令牌

场景模式
特权硬件操作ZST 证明令牌(AdminToken)
多步骤时序状态令牌链(StandbyOn → AuxiliaryOn → …)
基于角色的访问控制Trait 层次结构(Authenticated → Operator → Admin)
时间受限权限生命周期受限令牌(ScopedAdminToken<'a>)
跨模块权限公共令牌类型,私有构造函数

成本摘要

内容运行时成本
ZST 令牌在内存中0 字节
令牌参数传递被 LLVM 优化掉
Trait 层次结构分发静态分发(单态化)
生命周期强制执行仅编译时

总运行时开销:零。 权限模型只存在于类型系统中。

Capability 令牌层次结构

flowchart TD
    AUTH["authenticate(user, pass)"] -->|返回 | AT["AdminToken"]
    AT -->|"&AdminToken"| FW["firmware_update()"]
    AT -->|"&AdminToken"| RST["reset_pcie_link()"]
    AT -->|降级 | OP["OperatorToken"]
    OP -->|"&OperatorToken"| RD["read_sensors()"]
    OP -.->|"尝试 firmware_update"| ERR["❌ 编译错误"]
    style AUTH fill:#e1f5fe,color:#000
    style AT fill:#c8e6c9,color:#000
    style OP fill:#fff3e0,color:#000
    style FW fill:#e8f5e9,color:#000
    style RST fill:#e8f5e9,color:#000
    style RD fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

练习:分层诊断权限

设计一个三层 capability 系统:ViewerToken、TechToken、EngineerToken。

  • Viewer 可以调用 read_status()
  • Tech 还可以调用 run_quick_diag()
  • Engineer 还可以调用 flash_firmware()
  • 更高层级可以做低层级的所有事情(使用 trait 边界或令牌转换)。
解决方案
// 令牌——零大小,私有构造函数
pub struct ViewerToken { _private: () }
pub struct TechToken { _private: () }
pub struct EngineerToken { _private: () }

// Capability traits——层次化
pub trait CanView {}
pub trait CanDiag: CanView {}
pub trait CanFlash: CanDiag {}

impl CanView for ViewerToken {}
impl CanView for TechToken {}
impl CanView for EngineerToken {}
impl CanDiag for TechToken {}
impl CanDiag for EngineerToken {}
impl CanFlash for EngineerToken {}

pub fn read_status(_tok: &impl CanView) -> String {
    "status: OK".into()
}

pub fn run_quick_diag(_tok: &impl CanDiag) -> String {
    "diag: PASS".into()
}

pub fn flash_firmware(_tok: &impl CanFlash, _image: &[u8]) {
    // 只有 engineer 能到达这里
}

关键要点

  1. ZST 令牌成本为零字节——它们只存在于类型系统中;LLVM 完全优化掉它们。
  2. 私有构造函数 = 不可伪造——只有你模块的 authenticate() 可以铸造令牌。
  3. Trait 层次结构建模权限级别——CanFlash: CanDiag: CanView 镜像真实 RBAC。
  4. 生命周期受限令牌自动撤销——ScopedAdminToken<'session> 不能超越会话。
  5. 与 type-state 结合(ch05) 用于需要认证和时序操作的协议。

协议状态机——真实硬件的类型状态 🔴

你将学到: 类型状态编码如何使协议违规(错误顺序的命令、关闭后使用)成为编译错误,应用于 IPMI 会话生命周期和 PCIe 链路训练。

交叉引用: ch01(第二层——状态正确性)、ch04(令牌)、ch09(phantom 类型)、ch11(技巧 4——typestate builder,技巧 8——async type-state)

问题:协议违规

硬件协议有严格的状态机。IPMI 会话有以下状态:Unauthenticated → Authenticated → Active → Closed。PCIe 链路训练经历 Detect → Polling → Configuration → L0。在错误的状态发送命令会破坏会话或挂起总线。

IPMI 会话状态机:

stateDiagram-v2
    [*] --> Idle
    Idle --> Authenticated : authenticate(user, pass)
    Authenticated --> Active : activate_session()
    Active --> Active : send_command(cmd)
    Active --> Closed : close()
    Closed --> [*]

    note right of Active : send_command() 只在这里存在
    note right of Idle : send_command() → 编译错误

PCIe 链路训练状态机(LTSSM):

stateDiagram-v2
    [*] --> Detect
    Detect --> Polling : receiver detected
    Polling --> Configuration : bit lock + symbol lock
    Configuration --> L0 : link number + lane assigned
    L0 --> L0 : send_tlp() / receive_tlp()
    L0 --> Recovery : error threshold
    Recovery --> L0 : retrained
    Recovery --> Detect : retraining failed

    note right of L0 : TLP 传输只在 L0

在 C/C++ 中,状态用 enum 和运行时检查跟踪:

typedef enum { IDLE, AUTHENTICATED, ACTIVE, CLOSED } session_state_t;

typedef struct {
    session_state_t state;
    uint32_t session_id;
    // ...
} ipmi_session_t;

int ipmi_send_command(ipmi_session_t *s, uint8_t cmd, uint8_t *data, int len) {
    if (s->state != ACTIVE) {        // 运行时检查——容易忘记
        return -EINVAL;
    }
    // ... 发送命令 ...
    return 0;
}

类型状态模式

使用类型状态,每个协议状态是一个不同的类型。转换是消费一个状态并返回另一个状态的方法。编译器防止在错误状态调用方法,因为那些方法在该类型上不存在。

use std::marker::PhantomData;

// 状态——零大小标记类型
pub struct Idle;
pub struct Authenticated;
pub struct Active;
pub struct Closed;

/// IPMI 会话由其当前状态参数化。
/// 状态只存在于类型系统中(PhantomData 是零大小的)。
pub struct IpmiSession<State> {
    transport: String,     // 例如 "192.168.1.100"
    session_id: Option<u32>,
    _state: PhantomData<State>,
}

// 转换:Idle → Authenticated
impl IpmiSession<Idle> {
    pub fn new(host: &str) -> Self {
        IpmiSession {
            transport: host.to_string(),
            session_id: None,
            _state: PhantomData,
        }
    }

    pub fn authenticate(
        self,              // ← 消费 Idle 会话
        user: &str,
        pass: &str,
    ) -> Result<IpmiSession<Authenticated>, String> {
        println!("正在认证 {user},主机 {}", self.transport);
        Ok(IpmiSession {
            transport: self.transport,
            session_id: Some(42),
            _state: PhantomData,
        })
    }
}

// 转换:Authenticated → Active
impl IpmiSession<Authenticated> {
    pub fn activate(self) -> Result<IpmiSession<Active>, String> {
        // session_id 在类型状态转换路径中保证为 Some。
        println!("正在激活会话 {}", self.session_id.unwrap());
        Ok(IpmiSession {
            transport: self.transport,
            session_id: self.session_id,
            _state: PhantomData,
        })
    }
}

// 仅在 Active 状态下可用的操作
impl IpmiSession<Active> {
    pub fn send_command(&mut self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
        // session_id 在 Active 状态下保证为 Some。
        println!("正在会话 {} 上发送命令 0x{cmd:02X}", self.session_id.unwrap());
        vec![0x00] // 存根:完成码 OK
    }

    pub fn close(self) -> IpmiSession<Closed> {
        // session_id 在 Active 状态下保证为 Some。
        println!("正在关闭会话 {}", self.session_id.unwrap());
        IpmiSession {
            transport: self.transport,
            session_id: None,
            _state: PhantomData,
        }
    }
}

fn ipmi_workflow() -> Result<(), String> {
    let session = IpmiSession::new("192.168.1.100");

    // session.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ 错误:IpmiSession<Idle> 上没有方法 `send_command` ❌

    let session = session.authenticate("admin", "password")?;

    // session.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ 错误:IpmiSession<Authenticated> 上没有方法 `send_command` ❌

    let mut session = session.activate()?;

    // ✅ 现在 send_command 存在:
    let response = session.send_command(0x04, 0x2D, &[1]);

    let _closed = session.close();

    // _closed.send_command(0x04, 0x2D, &[]);
    //  ^^^^^^ 错误:IpmiSession<Closed> 上没有方法 `send_command` ❌

    Ok(())
}

任何地方都不需要运行时状态检查。 编译器强制执行:

  • 激活前必须认证
  • 发送命令前必须激活
  • 关闭后不能发送命令

PCIe 链路训练状态机

PCIe 链路训练是 PCIe 规范中定义的多阶段协议。类型状态防止在链路就绪前发送数据:

use std::marker::PhantomData;

// PCIe LTSSM 状态(简化)
pub struct Detect;
pub struct Polling;
pub struct Configuration;
pub struct L0;         // 完全运行
pub struct Recovery;

pub struct PcieLink<State> {
    slot: u32,
    width: u8,          // 协商宽度(x1, x4, x8, x16)
    speed: u8,          // Gen1=1, Gen2=2, Gen3=3, Gen4=4, Gen5=5
    _state: PhantomData<State>,
}

impl PcieLink<Detect> {
    pub fn new(slot: u32) -> Self {
        PcieLink {
            slot, width: 0, speed: 0,
            _state: PhantomData,
        }
    }

    pub fn detect_receiver(self) -> Result<PcieLink<Polling>, String> {
        println!("插槽 {}: 检测到接收器", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: 0, speed: 0,
            _state: PhantomData,
        })
    }
}

impl PcieLink<Polling> {
    pub fn poll_compliance(self) -> Result<PcieLink<Configuration>, String> {
        println!("插槽 {}: 轮询完成,进入配置", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: 0, speed: 0,
            _state: PhantomData,
        })
    }
}

impl PcieLink<Configuration> {
    pub fn negotiate(self, width: u8, speed: u8) -> Result<PcieLink<L0>, String> {
        println!("插槽 {}: 协商为 x{width} Gen{speed}", self.slot);
        Ok(PcieLink {
            slot: self.slot, width, speed,
            _state: PhantomData,
        })
    }
}

impl PcieLink<L0> {
    /// 发送 TLP——只有在链路完全训练后(L0)才可能。
    pub fn send_tlp(&mut self, tlp: &[u8]) -> Vec<u8> {
        println!("插槽 {}: 发送 {} 字节 TLP", self.slot, tlp.len());
        vec![0x00] // 存根
    }

    /// 进入 recovery——返回 Recovery 状态。
    pub fn enter_recovery(self) -> PcieLink<Recovery> {
        PcieLink {
            slot: self.slot, width: self.width, speed: self.speed,
            _state: PhantomData,
        }
    }

    pub fn link_info(&self) -> String {
        format!("x{} Gen{}", self.width, self.speed)
    }
}

impl PcieLink<Recovery> {
    pub fn retrain(self, speed: u8) -> Result<PcieLink<L0>, String> {
        println!("插槽 {}: 以 Gen{speed} 重新训练", self.slot);
        Ok(PcieLink {
            slot: self.slot, width: self.width, speed,
            _state: PhantomData,
        })
    }
}

fn pcie_workflow() -> Result<(), String> {
    let link = PcieLink::new(0);

    // link.send_tlp(&[0x01]);  // ❌ PcieLink<Detect> 上没有方法 `send_tlp`

    let link = link.detect_receiver()?;
    let link = link.poll_compliance()?;
    let mut link = link.negotiate(16, 5)?; // x16 Gen5

    // ✅ 现在可以发送 TLP:
    let _resp = link.send_tlp(&[0x00, 0x01, 0x02]);
    println!("链路:{}", link.link_info());

    // Recovery 和重新训练:
    let recovery = link.enter_recovery();
    let mut link = recovery.retrain(4)?;  // 降级到 Gen4
    let _resp = link.send_tlp(&[0x03]);

    Ok(())
}

组合类型状态与 Capability 令牌

类型状态和 capability 令牌自然地组合。需要活动 IPMI 会话和管理员权限的诊断:

use std::marker::PhantomData;
pub struct Active;
pub struct AdminToken { _p: () }
pub struct IpmiSession<S> { _s: PhantomData<S> }
impl IpmiSession<Active> {
    pub fn send_command(&mut self, _nf: u8, _cmd: u8, _d: &[u8]) -> Vec<u8> { vec![] }
}

/// 运行固件更新——需要:
/// 1. 活动 IPMI 会话(类型状态)
/// 2. 管理员权限(capability 令牌)
pub fn firmware_update(
    session: &mut IpmiSession<Active>,   // 证明会话处于活动状态
    _admin: &AdminToken,                 // 证明调用者是管理员
    image: &[u8],
) -> Result<(), String> {
    // 不需要运行时检查——签名就是检查
    session.send_command(0x2C, 0x01, image);
    Ok(())
}

调用者必须:

  1. 创建会话(Idle)
  2. 认证(Authenticated)
  3. 激活(Active)
  4. 获取 AdminToken
  5. 然后才能调用 firmware_update()

全部在编译时强制执行,零运行时成本。

三部曲 3:固件更新——多阶段 FSM 与组合

固件更新生命周期比会话有更多状态,并且与 capability 令牌和一次性类型(ch03)组合。这是本书最复杂的类型状态示例——如果你对它感到舒适,你已经掌握了这个模式。

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : begin_upload(admin, image)
    Uploading --> Verifying : finish_upload()
    Uploading --> Idle : abort()
    Verifying --> Verified : verify_ok()
    Verifying --> Idle : verify_fail()
    Verified --> Applying : apply(single-use VerifiedImage token)
    Applying --> WaitingReboot : apply_complete()
    WaitingReboot --> [*] : reboot()

    note right of Verified : VerifiedImage 令牌被 apply() 消费
    note right of Uploading : abort() 返回 Idle(安全)
use std::marker::PhantomData;

// ── 状态 ──
pub struct Idle;
pub struct Uploading;
pub struct Verifying;
pub struct Verified;
pub struct Applying;
pub struct WaitingReboot;

// ── 一次性证明:镜像通过验证(ch03)──
pub struct VerifiedImage {
    _private: (),
    pub digest: [u8; 32],
}

// ── Capability 令牌:只有管理员可以发起(ch04)──
pub struct FirmwareAdminToken { _private: () }

pub struct FwUpdate<S> {
    version: String,
    _state: PhantomData<S>,
}

impl FwUpdate<Idle> {
    pub fn new() -> Self {
        FwUpdate { version: String::new(), _state: PhantomData }
    }

    /// 开始上传——需要管理员权限。
    pub fn begin_upload(
        self,
        _admin: &FirmwareAdminToken,
        version: &str,
    ) -> FwUpdate<Uploading> {
        println!("正在上传固件 v{version}...");
        FwUpdate { version: version.to_string(), _state: PhantomData }
    }
}

impl FwUpdate<Uploading> {
    pub fn finish_upload(self) -> FwUpdate<Verifying> {
        println!("上传完成,正在验证 v{}...", self.version);
        FwUpdate { version: self.version, _state: PhantomData }
    }

    /// Abort 返回 Idle——上传过程中的任何时间点都安全。
    pub fn abort(self) -> FwUpdate<Idle> {
        println!("上传已中止。");
        FwUpdate { version: String::new(), _state: PhantomData }
    }
}

impl FwUpdate<Verifying> {
    /// 成功后,生成一次性 VerifiedImage 令牌。
    pub fn verify_ok(self, digest: [u8; 32]) -> (FwUpdate<Verified>, VerifiedImage) {
        println!("验证通过 v{}", self.version);
        (
            FwUpdate { version: self.version, _state: PhantomData },
            VerifiedImage { _private: (), digest },
        )
    }

    pub fn verify_fail(self) -> FwUpdate<Idle> {
        println!("验证失败——返回空闲状态。");
        FwUpdate { version: String::new(), _state: PhantomData }
    }
}

impl FwUpdate<Verified> {
    /// Apply 消费 VerifiedImage 令牌——不能应用两次。
    pub fn apply(self, proof: VerifiedImage) -> FwUpdate<Applying> {
        println!("正在应用 v{} (摘要:{:02x?})", self.version, &proof.digest[..4]);
        // proof 被移动——不能重用
        FwUpdate { version: self.version, _state: PhantomData }
    }
}

impl FwUpdate<Applying> {
    pub fn apply_complete(self) -> FwUpdate<WaitingReboot> {
        println!("应用完成——等待重启。");
        FwUpdate { version: self.version, _state: PhantomData }
    }
}

impl FwUpdate<WaitingReboot> {
    pub fn reboot(self) {
        println!("正在重启到 v{}...", self.version);
    }
}

// ── 用法 ──

fn firmware_workflow() {
    let fw = FwUpdate::new();

    // fw.finish_upload();  // ❌ FwUpdate<Idle> 上没有方法 `finish_upload`

    let admin = FirmwareAdminToken { _private: () }; // 来自认证系统
    let fw = fw.begin_upload(&admin, "2.10.1");
    let fw = fw.finish_upload();

    let digest = [0xAB; 32]; // 验证期间计算
    let (fw, token) = fw.verify_ok(digest);

    let fw = fw.apply(token);
    // fw.apply(token);  // ❌ 使用已移动的值:`token`

    let fw = fw.apply_complete();
    fw.reboot();
}

三个三部曲共同说明的内容:

三部曲协议状态数组合
1IPMI 会话4纯类型状态
2PCIe LTSSM5类型状态 + recovery 分支
3固件更新6类型状态 + capability 令牌(ch04)+ 一次性证明(ch03)

每个三部曲增加一层复杂性。到三部曲 3 时,编译器强制执行状态顺序、管理员权限和一次性应用——三个 bug 类别在一个 FSM 中消除。

何时使用类型状态

协议类型状态值得吗?
IPMI 会话生命周期✅ 是——认证 → 激活 → 命令 → 关闭
PCIe 链路训练✅ 是——检测 → 轮询 → 配置 → L0
TLS 握手✅ 是——ClientHello → ServerHello → Finished
USB 枚举✅ 是——Attached → Powered → Default → Addressed → Configured
简单请求/响应⚠️ 可能不——只有 2 个状态
即发即弃消息❌ 不——没有状态需要跟踪

练习:USB 设备枚举类型状态

建模一个 USB 设备必须经历:Attached → Powered → Default → Addressed → Configured。每个转换应该消费前一个状态并生成下一个状态。send_data() 应该只在 Configured 状态下可用。

解决方案
use std::marker::PhantomData;

pub struct Attached;
pub struct Powered;
pub struct Default;
pub struct Addressed;
pub struct Configured;

pub struct UsbDevice<State> {
    address: u8,
    _state: PhantomData<State>,
}

impl UsbDevice<Attached> {
    pub fn new() -> Self {
        UsbDevice { address: 0, _state: PhantomData }
    }
    pub fn power_on(self) -> UsbDevice<Powered> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Powered> {
    pub fn reset(self) -> UsbDevice<Default> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Default> {
    pub fn set_address(self, addr: u8) -> UsbDevice<Addressed> {
        UsbDevice { address: addr, _state: PhantomData }
    }
}

impl UsbDevice<Addressed> {
    pub fn configure(self) -> UsbDevice<Configured> {
        UsbDevice { address: self.address, _state: PhantomData }
    }
}

impl UsbDevice<Configured> {
    pub fn send_data(&self, _data: &[u8]) {
        // 仅在 Configured 状态下可用
    }
}

关键要点

  1. 类型状态使错误顺序的调用不可能——方法只存在于它们有效的状态上。
  2. 每个转换消费 self——转换后不能保留旧状态。
  3. 与 capability 令牌组合——firmware_update() 需要两者 Session<Active> 和 AdminToken。
  4. 三个三部曲,复杂性递增——IPMI(纯 FSM)、PCIe LTSSM(recovery 分支)和固件更新(FSM + 令牌 + 一次性证明)展示了模式从简单到丰富组合的扩展。
  5. 不要过度应用——两状态请求/响应协议不需要类型状态更简单。
  6. 模式扩展到完整的 Redfish 工作流——ch17 将类型状态应用于 Redfish 会话生命周期,ch18 使用 builder 类型状态进行响应构建。

量纲分析——让编译器检查你的单位 🟢

你将学到: newtype 包装器和 uom crate 如何将编译器变成单位检查引擎,防止摧毁 3.28 亿美元航天器的那类 bug。

交叉引用: ch02(类型化命令使用这些类型)、ch07(有效性边界)、ch10(集成)

火星气候轨道器

1999 年,NASA 的火星气候轨道器丢失了,因为一个团队发送的推力数据单位是磅力秒,而导航团队期望的是牛顿秒。航天器以 57 公里而不是 226 公里的高度进入大气层并解体。损失:3.276 亿美元。

根本原因:两个值都是 double。编译器无法区分它们。

同类 bug 潜伏在每个处理物理量的硬件诊断中:

// C — 都是 double,没有单位检查
double read_temperature(int sensor_id);   // Celsius?Fahrenheit?Kelvin?
double read_voltage(int channel);          // Volts?Millivolts?
double read_fan_speed(int fan_id);         // RPM?弧度每秒?

// Bug:比较 Celsius 和 Fahrenheit
if (read_temperature(0) > read_temperature(1)) { ... }  // 单位可能不同!

物理量的 Newtype

最简单的构造正确性方法:将每个单位包装在它自己的类型中。

use std::fmt;

/// 摄氏温度。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

/// 华氏温度。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);

/// 电压(伏特)。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

/// 电压(毫伏)。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);

/// 风扇转速(RPM)。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);

// 转换是显式的:
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

impl From<Volts> for Millivolts {
    fn from(v: Volts) -> Self {
        Millivolts(v.0 * 1000.0)
    }
}

impl From<Millivolts> for Volts {
    fn from(mv: Millivolts) -> Self {
        Volts(mv.0 / 1000.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Rpm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.0} RPM", self.0)
    }
}

现在编译器捕获单位不匹配:

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

fn check_thermal_limit(temp: Celsius, limit: Celsius) -> bool {
    temp > limit  // ✅ 相同单位——编译通过
}

// fn bad_comparison(temp: Celsius, voltage: Volts) -> bool {
//     temp > voltage  // ❌ 错误:类型不匹配——Celsius vs Volts
// }

零运行时成本——newtype 编译为原始 f64 值。包装器纯粹是类型级别的概念。

硬件量纲的 Newtype 宏

手动编写 newtype 会变得重复。宏消除了样板代码:

/// 为物理量生成 newtype。
macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);

        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }

        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }

        impl std::ops::Add for $Name {
            type Output = Self;
            fn add(self, rhs: Self) -> Self { $Name(self.0 + rhs.0) }
        }

        impl std::ops::Sub for $Name {
            type Output = Self;
            fn sub(self, rhs: Self) -> Self { $Name(self.0 - rhs.0) }
        }
    };
}

// 用法:
quantity!(Celsius, "°C");
quantity!(Fahrenheit, "°F");
quantity!(Volts, "V");
quantity!(Millivolts, "mV");
quantity!(Rpm, "RPM");
quantity!(Watts, "W");
quantity!(Amperes, "A");
quantity!(Pascals, "Pa");
quantity!(Hertz, "Hz");
quantity!(Bytes, "B");

每一行生成一个完整的类型,带有 Display、Add、Sub 和比较运算符。全部零运行时成本。

物理注意事项: 宏为所有量生成 Add,包括 Celsius。添加绝对温度(25°C + 30°C = 55°C)在物理上没有意义——你需要一个独立的 TemperatureDelta 类型来表示差值。uom crate(稍后展示)正确处理这一点。对于简单的传感器诊断(只需比较和显示),你可以从温度类型中省略 Add/Sub,并将它们保留给加法有意义的量(Watts、Volts、Bytes)。如果你需要差值算术,定义一个 CelsiusDelta(f64) newtype 并实现 impl Add<CelsiusDelta> for Celsius。

应用示例:传感器流水线

典型的诊断读取原始 ADC 值,将其转换为物理单位,并与阈值比较。使用量纲类型,每个步骤都进行类型检查:

macro_rules! quantity {
    ($Name:ident, $unit:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $Name(pub f64);
        impl $Name {
            pub fn new(value: f64) -> Self { $Name(value) }
            pub fn value(self) -> f64 { self.0 }
        }
        impl std::fmt::Display for $Name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unit)
            }
        }
    };
}
quantity!(Celsius, "°C");
quantity!(Volts, "V");
quantity!(Rpm, "RPM");

/// 原始 ADC 读数——还不是物理量。
#[derive(Debug, Clone, Copy)]
pub struct AdcReading {
    pub channel: u8,
    pub raw: u16,   // 12 位 ADC 值(0–4095)
}

/// 用于将 ADC 转换为物理单位的校准系数。
pub struct TemperatureCalibration {
    pub offset: f64,
    pub scale: f64,   // 每 ADC 计数的°C
}

pub struct VoltageCalibration {
    pub reference_mv: f64,
    pub divider_ratio: f64,
}

impl TemperatureCalibration {
    /// 将原始 ADC 转换为 Celsius。返回类型保证输出是 Celsius。
    pub fn convert(&self, adc: AdcReading) -> Celsius {
        Celsius::new(adc.raw as f64 * self.scale + self.offset)
    }
}

impl VoltageCalibration {
    /// 将原始 ADC 转换为 Volts。返回类型保证输出是 Volts。
    pub fn convert(&self, adc: AdcReading) -> Volts {
        Volts::new(adc.raw as f64 * self.reference_mv / 4096.0 / self.divider_ratio / 1000.0)
    }
}

/// 阈值检查——只有在单位匹配时才编译。
pub struct Threshold<T: PartialOrd> {
    pub warning: T,
    pub critical: T,
}

#[derive(Debug, PartialEq)]
pub enum ThresholdResult {
    Normal,
    Warning,
    Critical,
}

impl<T: PartialOrd> Threshold<T> {
    pub fn check(&self, value: &T) -> ThresholdResult {
        if *value >= self.critical {
            ThresholdResult::Critical
        } else if *value >= self.warning {
            ThresholdResult::Warning
        } else {
            ThresholdResult::Normal
        }
    }
}

fn sensor_pipeline_example() {
    let temp_cal = TemperatureCalibration { offset: -50.0, scale: 0.0625 };
    let temp_threshold = Threshold {
        warning: Celsius::new(85.0),
        critical: Celsius::new(100.0),
    };

    let adc = AdcReading { channel: 0, raw: 2048 };
    let temp: Celsius = temp_cal.convert(adc);

    let result = temp_threshold.check(&temp);
    println!("Temperature: {temp}, Status: {result:?}");

    // 这无法编译——不能用 Celsius 读数检查 Volts 阈值:
    // let volt_threshold = Threshold {
    //     warning: Volts::new(11.4),
    //     critical: Volts::new(10.8),
    // };
    // volt_threshold.check(&temp);  // ❌ 错误:期望 &Volts,找到 &Celsius
}

整个流水线都是静态类型检查的:

  • ADC 读数是原始计数(不是单位)
  • 校准生成类型化的量(Celsius、Volts)
  • 阈值对量类型是泛型的
  • 将 Celsius 与 Volts 比较是编译错误

uom Crate

对于生产用途,uom crate 提供了全面的量纲分析系统,具有数百个单位、自动转换和零运行时开销:

// Cargo.toml: uom = { version = "0.36", features = ["f64"] }
//
// use uom::si::f64::*;
// use uom::si::thermodynamic_temperature::degree_celsius;
// use uom::si::electric_potential::volt;
// use uom::si::power::watt;
//
// let temp = ThermodynamicTemperature::new::<degree_celsius>(85.0);
// let voltage = ElectricPotential::new::<volt>(12.0);
// let power = Power::new::<watt>(250.0);
//
// // temp + voltage;  // ❌ 编译错误——不能将温度与电压相加
// // power > temp;    // ❌ 编译错误——不能将功率与温度比较

当你需要自动导出单位支持(例如,Watts = Volts × Amperes)时使用 uom。当你只需要简单的量而不需要导出单位算术时使用手工编写的 newtype。

何时使用量纲类型

场景推荐
传感器读数(温度、电压、风扇)✅ 总是——防止单位混淆
阈值比较✅ 总是——泛型 Threshold<T>
跨子系统数据交换✅ 总是——在 API 边界强制执行契约
内部计算(全程相同单位)⚠️ 可选——较少 bug
字符串/显示格式化❌ 在量类型上使用 Display impl

传感器流水线类型流程

flowchart LR
    RAW["raw: &[u8]"] -->|解析 | C["Celsius(f64)"]
    RAW -->|解析 | R["Rpm(u32)"]
    RAW -->|解析 | V["Volts(f64)"]
    C -->|阈值检查 | TC["Threshold<Celsius>"]
    R -->|阈值检查 | TR["Threshold<Rpm>"]
    C -.->|"C + R"| ERR["❌ 类型不匹配"]
    style RAW fill:#e1f5fe,color:#000
    style C fill:#c8e6c9,color:#000
    style R fill:#fff3e0,color:#000
    style V fill:#e8eaf6,color:#000
    style TC fill:#c8e6c9,color:#000
    style TR fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

练习:功率预算计算器

创建 Watts(f64) 和 Amperes(f64) newtype。实现:

  • Watts::from_vi(volts: Volts, amps: Amperes) -> Watts(P = V × I)
  • 一个 PowerBudget 跟踪总瓦特数并拒绝超过配置限制的添加。
  • 尝试 Watts + Celsius 应该是编译错误。
解决方案
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

impl Watts {
    pub fn from_vi(volts: Volts, amps: Amperes) -> Self {
        Watts(volts.0 * amps.0)
    }
}

impl std::ops::Add for Watts {
    type Output = Watts;
    fn add(self, rhs: Watts) -> Watts {
        Watts(self.0 + rhs.0)
    }
}

pub struct PowerBudget {
    total: Watts,
    limit: Watts,
}

impl PowerBudget {
    pub fn new(limit: Watts) -> Self {
        PowerBudget { total: Watts(0.0), limit }
    }
    pub fn add(&mut self, w: Watts) -> Result<(), String> {
        let new_total = Watts(self.total.0 + w.0);
        if new_total > self.limit {
            return Err(format!("预算超出:{:?} > {:?}", new_total, self.limit));
        }
        self.total = new_total;
        Ok(())
    }
}

// ❌ 编译错误:Watts + Celsius → "类型不匹配"
// let bad = Watts(100.0) + Celsius(50.0);

关键要点

  1. Newtype 以零成本防止单位混淆——Celsius 和 Rpm 内部都是 f64,但编译器将它们视为不同的类型。
  2. 火星气候轨道器 bug 是不可能的——在期望 Newtons 的地方传递 Pounds 是编译错误。
  3. quantity! 宏减少样板代码——为每个单位生成 Display、算术和阈值逻辑。
  4. uom crate 处理导出单位——当你需要 Watts = Volts × Amperes 自动计算时使用它。
  5. Threshold 对量是泛型的——Threshold<Celsius> 不能意外地与 Threshold<Rpm> 比较。

有效性边界——解析,不要验证 🟡

你将学到: 如何在系统边界恰好验证一次数据,在专用类型中携带有效性证明,并且永不重新检查——应用于 IPMI FRU 记录(扁平字节)、Redfish JSON(结构化文档)和 IPMI SEL 记录(多态二进制带嵌套分发),以及完整的端到端 walkthrough。

交叉引用: ch02(类型化命令)、ch06(量纲类型)、ch11(技巧 2——密封 traits,技巧 3——#[non_exhaustive],技巧 5——FromStr)、ch14(proptest)

问题:散弹枪式验证

在典型代码中,验证无处不在。每个接收数据的函数都重新检查它“以防万一“:

// C — 验证分散在代码库各处
int process_fru_data(uint8_t *data, int len) {
    if (data == NULL) return -1;          // 检查:非空
    if (len < 8) return -1;              // 检查:最小长度
    if (data[0] != 0x01) return -1;      // 检查:格式版本
    if (checksum(data, len) != 0) return -1; // 检查:校验和

    // ... 还有 10 个函数重复相同的检查 ...
}

这种模式(“散弹枪式验证”)有两个问题:

  1. 冗余——相同的检查出现在数十个地方
  2. 不完整——在一个函数中忘记一个检查就产生了 bug

解析,不要验证

构造正确性的方法:在边界处验证一次,然后在类型中携带有效性证明。

/// 来自线路的原始字节——尚未验证。
#[derive(Debug)]
pub struct RawFruData(Vec<u8>);

案例研究:IPMI FRU 数据

#[derive(Debug)]
pub struct RawFruData(Vec<u8>);

/// 有效 IPMI FRU 数据。只能通过 TryFrom 创建,
/// 它强制执行所有不变量。一旦你有了 ValidFru,
/// 所有数据保证正确。
#[derive(Debug)]
pub struct ValidFru {
    format_version: u8,
    internal_area_offset: u8,
    chassis_area_offset: u8,
    board_area_offset: u8,
    product_area_offset: u8,
    data: Vec<u8>,
}

#[derive(Debug)]
pub enum FruError {
    TooShort { actual: usize, minimum: usize },
    BadFormatVersion(u8),
    ChecksumMismatch { expected: u8, actual: u8 },
    InvalidAreaOffset { area: &'static str, offset: u8 },
}

impl std::fmt::Display for FruError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TooShort { actual, minimum } =>
                write!(f, "FRU 数据太短:{actual} 字节(最小 {minimum})"),
            Self::BadFormatVersion(v) =>
                write!(f, "不支持的 FRU 格式版本:{v}"),
            Self::ChecksumMismatch { expected, actual } =>
                write!(f, "校验和不匹配:期望 0x{expected:02X},得到 0x{actual:02X}"),
            Self::InvalidAreaOffset { area, offset } =>
                write!(f, "无效的 {area} 区域偏移:{offset}"),
        }
    }
}

impl TryFrom<RawFruData> for ValidFru {
    type Error = FruError;

    fn try_from(raw: RawFruData) -> Result<Self, FruError> {
        let data = raw.0;

        // 1. 长度检查
        if data.len() < 8 {
            return Err(FruError::TooShort {
                actual: data.len(),
                minimum: 8,
            });
        }

        // 2. 格式版本
        if data[0] != 0x01 {
            return Err(FruError::BadFormatVersion(data[0]));
        }

        // 3. 校验和(头部是前 8 个字节,校验和在第 7 字节)
        let checksum: u8 = data[..8].iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
        if checksum != 0 {
            return Err(FruError::ChecksumMismatch {
                expected: 0,
                actual: checksum,
            });
        }

        // 4. 区域偏移必须在边界内
        for (name, idx) in [
            ("internal", 1), ("chassis", 2),
            ("board", 3), ("product", 4),
        ] {
            let offset = data[idx];
            if offset != 0 && (offset as usize * 8) >= data.len() {
                return Err(FruError::InvalidAreaOffset {
                    area: name,
                    offset,
                });
            }
        }

        // 所有检查通过——构造验证后的类型
        Ok(ValidFru {
            format_version: data[0],
            internal_area_offset: data[1],
            chassis_area_offset: data[2],
            board_area_offset: data[3],
            product_area_offset: data[4],
            data,
        })
    }
}

impl ValidFru {
    /// 不需要验证——类型保证正确性。
    pub fn board_area(&self) -> Option<&[u8]> {
        if self.board_area_offset == 0 {
            return None;
        }
        let start = self.board_area_offset as usize * 8;
        Some(&self.data[start..])  // 安全——解析期间边界已检查
    }

    pub fn product_area(&self) -> Option<&[u8]> {
        if self.product_area_offset == 0 {
            return None;
        }
        let start = self.product_area_offset as usize * 8;
        Some(&self.data[start..])
    }

    pub fn format_version(&self) -> u8 {
        self.format_version
    }
}

任何接受 &ValidFru 的函数知道数据格式良好。不需要重新检查:

pub struct ValidFru { board_area_offset: u8, data: Vec<u8> }
impl ValidFru {
    pub fn board_area(&self) -> Option<&[u8]> { None }
}

/// 这个函数不需要验证 FRU 数据。
/// 类型签名保证它已经有效。
fn extract_board_serial(fru: &ValidFru) -> Option<String> {
    let board = fru.board_area()?;
    // ... 从板区域解析序列号 ...
    // 不需要边界检查——ValidFru 保证偏移在范围内
    Some("ABC123".to_string()) // 存根
}

fn extract_board_manufacturer(fru: &ValidFru) -> Option<String> {
    let board = fru.board_area()?;
    // 仍然不需要验证——相同的保证
    Some("Acme Corp".to_string()) // 存根
}

有效性的 Redfish JSON

相同的模式适用于 Redfish API 响应。解析一次,在类型中携带有效性:

use std::collections::HashMap;

/// 来自 Redfish 端点的原始 JSON 字符串。
pub struct RawRedfishResponse(pub String);

/// 有效 Redfish Thermal 响应。
/// 所有必需字段保证存在并在范围内。
#[derive(Debug)]
pub struct ValidThermalResponse {
    pub temperatures: Vec<ValidTemperatureReading>,
    pub fans: Vec<ValidFanReading>,
}

#[derive(Debug)]
pub struct ValidTemperatureReading {
    pub name: String,
    pub reading_celsius: f64,     // 保证非 NaN,在传感器范围内
    pub upper_critical: f64,
    pub status: HealthStatus,
}

#[derive(Debug)]
pub struct ValidFanReading {
    pub name: String,
    pub reading_rpm: u32,        // 对于存在的风扇保证 > 0
    pub status: HealthStatus,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HealthStatus {
    Ok,
    Warning,
    Critical,
}

#[derive(Debug)]
pub enum RedfishValidationError {
    MissingField(&'static str),
    OutOfRange { field: &'static str, value: f64 },
    InvalidStatus(String),
}

impl std::fmt::Display for RedfishValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MissingField(name) => write!(f, "缺少必需字段:{name}"),
            Self::OutOfRange { field, value } =>
                write!(f, "字段 {field} 超出范围:{value}"),
            Self::InvalidStatus(s) => write!(f, "无效健康状态:{s}"),
        }
    }
}

// 一旦验证,下游代码从不重新检查:
fn check_thermal_health(thermal: &ValidThermalResponse) -> bool {
    // 不需要检查缺失字段或 NaN 值。
    // ValidThermalResponse 保证所有读数合理。
    thermal.temperatures.iter().all(|t| {
        t.reading_celsius < t.upper_critical && t.status != HealthStatus::Critical
    }) && thermal.fans.iter().all(|f| {
        f.reading_rpm > 0 && f.status != HealthStatus::Critical
    })
}

多态验证:IPMI SEL 记录

前两个案例研究验证了扁平结构——固定字节布局(FRU)和已知 JSON 模式(Redfish)。真实世界数据通常是多态的:后面字节的解释取决于前面的字节。IPMI 系统事件日志(SEL)记录是典型的例子。

问题的形状

每个 SEL 记录恰好 16 字节。但这些字节的含义取决于分发链:

字节 2:记录类型
  ├─ 0x02 → 系统事件
  │    字节 10[6:4]:事件类型
  │      ├─ 0x01       → 阈值事件(读数 + 阈值在数据字节 2-3)
  │      ├─ 0x02-0x0C  → 离散事件(偏移字段中的位)
  │      └─ 0x6F       → 传感器特定(含义取决于字节 7 中的传感器类型)
  │           字节 7:传感器类型
  │             ├─ 0x01 → 温度事件
  │             ├─ 0x02 → 电压事件
  │             ├─ 0x04 → 风扇事件
  │             ├─ 0x07 → 处理器事件
  │             ├─ 0x0C → 内存事件
  │             ├─ 0x08 → 电源事件
  │             └─ ...  → (IPMI 2.0 表 42-3 中有 42 种传感器类型)
  ├─ 0xC0-0xDF → OEM 带时间戳
  └─ 0xE0-0xFF → OEM 不带时间戳

在 C 中,这是 switch 套 switch 再套 switch,每一层共享相同的 uint8_t *data 指针。忘记一层,误读规范表,或索引错误的字节——bug 是静默的。

// C — 多态解析问题
void process_sel_entry(uint8_t *data, int len) {
    if (data[2] == 0x02) {  // 系统事件
        uint8_t event_type = (data[10] >> 4) & 0x07;
        if (event_type == 0x01) {  // 阈值
            uint8_t reading = data[11];   // 🐛 或者是 data[13]?
            uint8_t threshold = data[12]; // 🐛 规范说字节 12 是触发器,不是阈值
            printf("Temp: %d crossed %d\n", reading, threshold);
        } else if (event_type == 0x6F) {  // 传感器特定
            uint8_t sensor_type = data[7];
            if (sensor_type == 0x0C) {  // 内存
                // 🐛 忘记检查事件数据 1 偏移位
                printf("Memory ECC 错误\n");
            }
            // 🐛 没有 else——静默丢弃 30+ 种其他传感器类型
        }
    }
    // 🐛 OEM 记录类型静默忽略
}

步骤 1——解析外框

第一个 TryFrom 分发记录类型——联合的最外层:

/// 原始 16 字节 SEL 记录,直接来自 `Get SEL Entry` (IPMI 命令 0x43)。
pub struct RawSelRecord(pub [u8; 16]);

/// 有效 SEL 记录——记录类型已分发,所有字段已检查。
pub enum ValidSelRecord {
    SystemEvent(SystemEventRecord),
    OemTimestamped(OemTimestampedRecord),
    OemNonTimestamped(OemNonTimestampedRecord),
}

#[derive(Debug)]
pub struct OemTimestampedRecord {
    pub record_id: u16,
    pub timestamp: u32,
    pub manufacturer_id: [u8; 3],
    pub oem_data: [u8; 6],
}

#[derive(Debug)]
pub struct OemNonTimestampedRecord {
    pub record_id: u16,
    pub oem_data: [u8; 13],
}

#[derive(Debug)]
pub enum SelParseError {
    UnknownRecordType(u8),
    UnknownSensorType(u8),
    UnknownEventType(u8),
    InvalidEventData { reason: &'static str },
}

impl std::fmt::Display for SelParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnknownRecordType(t) => write!(f, "未知记录类型:0x{t:02X}"),
            Self::UnknownSensorType(t) => write!(f, "未知传感器类型:0x{t:02X}"),
            Self::UnknownEventType(t) => write!(f, "未知事件类型:0x{t:02X}"),
            Self::InvalidEventData { reason } => write!(f, "无效事件数据:{reason}"),
        }
    }
}

impl TryFrom<RawSelRecord> for ValidSelRecord {
    type Error = SelParseError;

    fn try_from(raw: RawSelRecord) -> Result<Self, SelParseError> {
        let d = &raw.0;
        let record_id = u16::from_le_bytes([d[0], d[1]]);

        match d[2] {
            0x02 => {
                let system = parse_system_event(record_id, d)?;
                Ok(ValidSelRecord::SystemEvent(system))
            }
            0xC0..=0xDF => {
                Ok(ValidSelRecord::OemTimestamped(OemTimestampedRecord {
                    record_id,
                    timestamp: u32::from_le_bytes([d[3], d[4], d[5], d[6]]),
                    manufacturer_id: [d[7], d[8], d[9]],
                    oem_data: [d[10], d[11], d[12], d[13], d[14], d[15]],
                }))
            }
            0xE0..=0xFF => {
                Ok(ValidSelRecord::OemNonTimestamped(OemNonTimestampedRecord {
                    record_id,
                    oem_data: [d[3], d[4], d[5], d[6], d[7], d[8], d[9],
                               d[10], d[11], d[12], d[13], d[14], d[15]],
                }))
            }
            other => Err(SelParseError::UnknownRecordType(other)),
        }
    }
}

在此边界之后,每个消费者匹配 enum。编译器强制执行处理所有三种记录类型——你不能“忘记“OEM 记录。

步骤 2——解析系统事件:传感器类型 → 类型化事件

内部分发将事件数据字节转换为按传感器类型索引的 sum type。这是 C 的 switch 套 switch 变成嵌套 enum 的地方:

#[derive(Debug)]
pub struct SystemEventRecord {
    pub record_id: u16,
    pub timestamp: u32,
    pub generator: GeneratorId,
    pub sensor_type: SensorType,
    pub sensor_number: u8,
    pub event_direction: EventDirection,
    pub event: TypedEvent,      // ← 关键:事件数据是*类型化*的
}

#[derive(Debug)]
pub enum GeneratorId {
    Software(u8),
    Ipmb { slave_addr: u8, channel: u8, lun: u8 },
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EventDirection { Assertion, Deassertion }

// ──── 传感器/事件类型层次结构 ────

/// IPMI 表 42-3 中的传感器类型。非穷举因为未来
/// IPMI 修订版和 OEM 范围将添加变体(见 ch11 技巧 3)。
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SensorType {
    Temperature,    // 0x01
    Voltage,        // 0x02
    Current,        // 0x03
    Fan,            // 0x04
    PhysicalSecurity, // 0x05
    Processor,      // 0x07
    PowerSupply,    // 0x08
    Memory,         // 0x0C
    SystemEvent,    // 0x12
    Watchdog2,      // 0x23
}

/// 多态载荷——每个变体携带自己的类型化数据。
#[derive(Debug)]
pub enum TypedEvent {
    Threshold(ThresholdEvent),
    SensorSpecific(SensorSpecificEvent),
    Discrete { offset: u8, event_data: [u8; 3] },
}

/// 阈值事件携带触发读数和阈值。
/// 两者都是原始传感器值(线性化前),保持为 u8。
/// SDR 线性化后,它们成为量纲类型(ch06)。
#[derive(Debug)]
pub struct ThresholdEvent {
    pub crossing: ThresholdCrossing,
    pub trigger_reading: u8,
    pub threshold_value: u8,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ThresholdCrossing {
    LowerNonCriticalLow,
    LowerNonCriticalHigh,
    LowerCriticalLow,
    LowerCriticalHigh,
    LowerNonRecoverableLow,
    LowerNonRecoverableHigh,
    UpperNonCriticalLow,
    UpperNonCriticalHigh,
    UpperCriticalLow,
    UpperCriticalHigh,
    UpperNonRecoverableLow,
    UpperNonRecoverableHigh,
}

/// 传感器特定事件——每个传感器类型获得自己的变体
/// 带有该传感器定义事件的穷举 enum。
#[derive(Debug)]
pub enum SensorSpecificEvent {
    Temperature(TempEvent),
    Voltage(VoltageEvent),
    Fan(FanEvent),
    Processor(ProcessorEvent),
    PowerSupply(PowerSupplyEvent),
    Memory(MemoryEvent),
    PhysicalSecurity(PhysicalSecurityEvent),
    Watchdog(WatchdogEvent),
}

// ──── 每个传感器类型的事件 enums(来自 IPMI 表 42-3)───

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MemoryEvent {
    CorrectableEcc,
    UncorrectableEcc,
    Parity,
    MemoryBoardScrubFailed,
    MemoryDeviceDisabled,
    CorrectableEccLogLimit,
    PresenceDetected,
    ConfigurationError,
    Spare,
    Throttled,
    CriticalOvertemperature,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PowerSupplyEvent {
    PresenceDetected,
    Failure,
    PredictiveFailure,
    InputLost,
    InputOutOfRange,
    InputLostOrOutOfRange,
    ConfigurationError,
    InactiveStandby,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TempEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VoltageEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FanEvent {
    UpperNonCritical,
    UpperCritical,
    UpperNonRecoverable,
    LowerNonCritical,
    LowerCritical,
    LowerNonRecoverable,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProcessorEvent {
    Ierr,
    ThermalTrip,
    Frb1BistFailure,
    Frb2HangInPost,
    Frb3ProcessorStartupFailure,
    ConfigurationError,
    UncorrectableMachineCheck,
    PresenceDetected,
    Disabled,
    TerminatorPresenceDetected,
    Throttled,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PhysicalSecurityEvent {
    ChassisIntrusion,
    DriveIntrusion,
    IOCardAreaIntrusion,
    ProcessorAreaIntrusion,
    LanLeashedLost,
    UnauthorizedDocking,
    FanAreaIntrusion,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WatchdogEvent {
    BiosReset,
    OsReset,
    OsShutdown,
    OsPowerDown,
    OsPowerCycle,
    BiosNmi,
    Timer,
}

步骤 3——解析器连接

fn parse_system_event(record_id: u16, d: &[u8]) -> Result<SystemEventRecord, SelParseError> {
    let timestamp = u32::from_le_bytes([d[3], d[4], d[5], d[6]]);

    let generator = if d[7] & 0x01 == 0 {
        GeneratorId::Ipmb {
            slave_addr: d[7] & 0xFE,
            channel: (d[8] >> 4) & 0x0F,
            lun: d[8] & 0x03,
        }
    } else {
        GeneratorId::Software(d[7])
    };

    let sensor_type = parse_sensor_type(d[10])?;
    let sensor_number = d[11];
    let event_direction = if d[12] & 0x80 != 0 {
        EventDirection::Deassertion
    } else {
        EventDirection::Assertion
    };

    let event_type_code = d[12] & 0x7F;
    let event_data = [d[13], d[14], d[15]];

    let event = match event_type_code {
        0x01 => {
            // 阈值——事件数据字节 2 是触发读数,字节 3 是阈值
            let offset = event_data[0] & 0x0F;
            TypedEvent::Threshold(ThresholdEvent {
                crossing: parse_threshold_crossing(offset)?,
                trigger_reading: event_data[1],
                threshold_value: event_data[2],
            })
        }
        0x6F => {
            // 传感器特定——在传感器类型上分发
            let offset = event_data[0] & 0x0F;
            let specific = parse_sensor_specific(&sensor_type, offset)?;
            TypedEvent::SensorSpecific(specific)
        }
        0x02..=0x0C => {
            // 通用离散
            TypedEvent::Discrete { offset: event_data[0] & 0x0F, event_data }
        }
        other => return Err(SelParseError::UnknownEventType(other)),
    };

    Ok(SystemEventRecord {
        record_id,
        timestamp,
        generator,
        sensor_type,
        sensor_number,
        event_direction,
        event,
    })
}

fn parse_sensor_type(code: u8) -> Result<SensorType, SelParseError> {
    match code {
        0x01 => Ok(SensorType::Temperature),
        0x02 => Ok(SensorType::Voltage),
        0x03 => Ok(SensorType::Current),
        0x04 => Ok(SensorType::Fan),
        0x05 => Ok(SensorType::PhysicalSecurity),
        0x07 => Ok(SensorType::Processor),
        0x08 => Ok(SensorType::PowerSupply),
        0x0C => Ok(SensorType::Memory),
        0x12 => Ok(SensorType::SystemEvent),
        0x23 => Ok(SensorType::Watchdog2),
        other => Err(SelParseError::UnknownSensorType(other)),
    }
}

fn parse_threshold_crossing(offset: u8) -> Result<ThresholdCrossing, SelParseError> {
    match offset {
        0x00 => Ok(ThresholdCrossing::LowerNonCriticalLow),
        0x01 => Ok(ThresholdCrossing::LowerNonCriticalHigh),
        0x02 => Ok(ThresholdCrossing::LowerCriticalLow),
        0x03 => Ok(ThresholdCrossing::LowerCriticalHigh),
        0x04 => Ok(ThresholdCrossing::LowerNonRecoverableLow),
        0x05 => Ok(ThresholdCrossing::LowerNonRecoverableHigh),
        0x06 => Ok(ThresholdCrossing::UpperNonCriticalLow),
        0x07 => Ok(ThresholdCrossing::UpperNonCriticalHigh),
        0x08 => Ok(ThresholdCrossing::UpperCriticalLow),
        0x09 => Ok(ThresholdCrossing::UpperCriticalHigh),
        0x0A => Ok(ThresholdCrossing::UpperNonRecoverableLow),
        0x0B => Ok(ThresholdCrossing::UpperNonRecoverableHigh),
        _ => Err(SelParseError::InvalidEventData {
            reason: "阈值偏移超出范围",
        }),
    }
}

fn parse_sensor_specific(
    sensor_type: &SensorType,
    offset: u8,
) -> Result<SensorSpecificEvent, SelParseError> {
    match sensor_type {
        SensorType::Memory => {
            let ev = match offset {
                0x00 => MemoryEvent::CorrectableEcc,
                0x01 => MemoryEvent::UncorrectableEcc,
                0x02 => MemoryEvent::Parity,
                0x03 => MemoryEvent::MemoryBoardScrubFailed,
                0x04 => MemoryEvent::MemoryDeviceDisabled,
                0x05 => MemoryEvent::CorrectableEccLogLimit,
                0x06 => MemoryEvent::PresenceDetected,
                0x07 => MemoryEvent::ConfigurationError,
                0x08 => MemoryEvent::Spare,
                0x09 => MemoryEvent::Throttled,
                0x0A => MemoryEvent::CriticalOvertemperature,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "未知内存事件偏移",
                }),
            };
            Ok(SensorSpecificEvent::Memory(ev))
        }
        SensorType::PowerSupply => {
            let ev = match offset {
                0x00 => PowerSupplyEvent::PresenceDetected,
                0x01 => PowerSupplyEvent::Failure,
                0x02 => PowerSupplyEvent::PredictiveFailure,
                0x03 => PowerSupplyEvent::InputLost,
                0x04 => PowerSupplyEvent::InputOutOfRange,
                0x05 => PowerSupplyEvent::InputLostOrOutOfRange,
                0x06 => PowerSupplyEvent::ConfigurationError,
                0x07 => PowerSupplyEvent::InactiveStandby,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "未知电源事件偏移",
                }),
            };
            Ok(SensorSpecificEvent::PowerSupply(ev))
        }
        SensorType::Processor => {
            let ev = match offset {
                0x00 => ProcessorEvent::Ierr,
                0x01 => ProcessorEvent::ThermalTrip,
                0x02 => ProcessorEvent::Frb1BistFailure,
                0x03 => ProcessorEvent::Frb2HangInPost,
                0x04 => ProcessorEvent::Frb3ProcessorStartupFailure,
                0x05 => ProcessorEvent::ConfigurationError,
                0x06 => ProcessorEvent::UncorrectableMachineCheck,
                0x07 => ProcessorEvent::PresenceDetected,
                0x08 => ProcessorEvent::Disabled,
                0x09 => ProcessorEvent::TerminatorPresenceDetected,
                0x0A => ProcessorEvent::Throttled,
                _ => return Err(SelParseError::InvalidEventData {
                    reason: "未知处理器事件偏移",
                }),
            };
            Ok(SensorSpecificEvent::Processor(ev))
        }
        // 模式对 Temperature、Voltage、Fan 等重复。
        // 每个传感器类型将其偏移映射到专用 enum。
        _ => Err(SelParseError::InvalidEventData {
            reason: "此传感器类型未实现传感器特定分发",
        }),
    }
}

步骤 4——消费类型化 SEL 记录

解析后,下游代码模式匹配嵌套 enums。编译器强制执行穷举处理——没有静默穿透,没有忘记的传感器类型:

/// 确定 SEL 事件是否应触发硬件警报。
/// 编译器确保每个变体都被处理。
fn should_alert(record: &ValidSelRecord) -> bool {
    match record {
        ValidSelRecord::SystemEvent(sys) => match &sys.event {
            TypedEvent::Threshold(t) => {
                // 任何临界或不可恢复的阈值穿越 → 警报
                matches!(t.crossing,
                    ThresholdCrossing::UpperCriticalLow
                    | ThresholdCrossing::UpperCriticalHigh
                    | ThresholdCrossing::LowerCriticalLow
                    | ThresholdCrossing::LowerCriticalHigh
                    | ThresholdCrossing::UpperNonRecoverableLow
                    | ThresholdCrossing::UpperNonRecoverableHigh
                    | ThresholdCrossing::LowerNonRecoverableLow
                    | ThresholdCrossing::LowerNonRecoverableHigh
                )
            }
            TypedEvent::SensorSpecific(ss) => match ss {
                SensorSpecificEvent::Memory(m) => matches!(m,
                    MemoryEvent::UncorrectableEcc
                    | MemoryEvent::Parity
                    | MemoryEvent::CriticalOvertemperature
                ),
                SensorSpecificEvent::PowerSupply(p) => matches!(p,
                    PowerSupplyEvent::Failure
                    | PowerSupplyEvent::InputLost
                ),
                SensorSpecificEvent::Processor(p) => matches!(p,
                    ProcessorEvent::Ierr
                    | ProcessorEvent::ThermalTrip
                    | ProcessorEvent::UncorrectableMachineCheck
                ),
                // 未来版本中添加的新传感器类型变体?
                // ❌ 编译错误:非穷举模式
                _ => false,
            },
            TypedEvent::Discrete { .. } => false,
        },
        // OEM 记录在此策略中不可警报
        ValidSelRecord::OemTimestamped(_) => false,
        ValidSelRecord::OemNonTimestamped(_) => false,
    }
}

/// 生成人类可读的描述。
/// 每个分支产生特定的消息——没有"未知事件"回退。
fn describe(record: &ValidSelRecord) -> String {
    match record {
        ValidSelRecord::SystemEvent(sys) => {
            let sensor = format!("{:?} 传感器 #{}", sys.sensor_type, sys.sensor_number);
            let dir = match sys.event_direction {
                EventDirection::Assertion => "断言",
                EventDirection::Deassertion => "释放",
            };
            match &sys.event {
                TypedEvent::Threshold(t) => {
                    format!("{sensor}: {:?} {dir} (读数:0x{:02X}, 阈值:0x{:02X})",
                        t.crossing, t.trigger_reading, t.threshold_value)
                }
                TypedEvent::SensorSpecific(ss) => {
                    format!("{sensor}: {ss:?} {dir}")
                }
                TypedEvent::Discrete { offset, .. } => {
                    format!("{sensor}: 离散偏移 {offset:#x} {dir}")
                }
            }
        }
        ValidSelRecord::OemTimestamped(oem) =>
            format!("OEM 记录 0x{:04X} (厂商 {:02X}{:02X}{:02X})",
                oem.record_id,
                oem.manufacturer_id[0], oem.manufacturer_id[1], oem.manufacturer_id[2]),
        ValidSelRecord::OemNonTimestamped(oem) =>
            format!("OEM 无时间戳记录 0x{:04X}", oem.record_id),
    }
}

Walkthrough:端到端 SEL 处理

这是完整的流程——从线路上的原始字节到警报决策——展示每个类型化交接:

/// 处理 BMC 的所有 SEL 条目,产生类型化警报。
fn process_sel_log(raw_entries: &[[u8; 16]]) -> Vec<String> {
    let mut alerts = Vec::new();

    for (i, raw_bytes) in raw_entries.iter().enumerate() {
        // ─── 边界:原始字节 → 有效记录 ───
        let raw = RawSelRecord(*raw_bytes);
        let record = match ValidSelRecord::try_from(raw) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("SEL 条目 {i}: 解析错误:{e}");
                continue;
            }
        };

        // ─── 从这里开始,一切都是类型化的 ───

        // 1. 描述事件(穷举匹配——每个变体覆盖)
        let description = describe(&record);
        println!("SEL[{i}]: {description}");

        // 2. 检查警报策略(穷举匹配——编译器证明完整性)
        if should_alert(&record) {
            alerts.push(description);
        }

        // 3. 从阈值事件提取量纲读数
        if let ValidSelRecord::SystemEvent(sys) = &record {
            if let TypedEvent::Threshold(t) = &sys.event {
                // 编译器知道 t.trigger_reading 是阈值事件读数,
                // 不是任意字节。SDR 线性化后(ch06),这成为:
                //   let temp: Celsius = linearize(t.trigger_reading, &sdr);
                // 然后 Celsius 不能与 Rpm 比较。
                println!(
                    "  → 原始读数:0x{:02X}, 原始阈值:0x{:02X}",
                    t.trigger_reading, t.threshold_value
                );
            }
        }
    }

    alerts
}

fn main() {
    // 示例:两个 SEL 条目(虚构说明)
    let sel_data: Vec<[u8; 16]> = vec![
        // 条目 1:系统事件,内存传感器 #3,传感器特定,
        //          偏移 0x00 = CorrectableEcc,断言
        [
            0x01, 0x00,       // 记录 ID: 1
            0x02,             // 记录类型:系统事件
            0x00, 0x00, 0x00, 0x00, // 时间戳(存根)
            0x20,             // 生成器:IPMB 从地址 0x20
            0x00,             // 通道/lun
            0x04,             // 事件消息 rev
            0x0C,             // 传感器类型:内存 (0x0C)
            0x03,             // 传感器编号:3
            0x6F,             // 事件方向:断言,事件类型:传感器特定
            0x00,             // 事件数据 1:偏移 0x00 = CorrectableEcc
            0x00, 0x00,       // 事件数据 2-3
        ],
        // 条目 2:系统事件,温度传感器 #1,阈值,
        //          偏移 0x09 = UpperCriticalHigh,读数=95,阈值=90
        [
            0x02, 0x00,       // 记录 ID: 2
            0x02,             // 记录类型:系统事件
            0x00, 0x00, 0x00, 0x00, // 时间戳(存根)
            0x20,             // 生成器
            0x00,             // 通道/lun
            0x04,             // 事件消息 rev
            0x01,             // 传感器类型:温度 (0x01)
            0x01,             // 传感器编号:1
            0x01,             // 事件方向:断言,事件类型:阈值 (0x01)
            0x09,             // 事件数据 1:偏移 0x09 = UpperCriticalHigh
            0x5F,             // 事件数据 2:触发读数 (95 原始)
            0x5A,             // 事件数据 3:阈值 (90 原始)
        ],
    ];

    let alerts = process_sel_log(&sel_data);
    println!("\n=== 警报 ({}) ===", alerts.len());
    for alert in &alerts {
        println!("  🚨 {alert}");
    }
}

预期输出:

SEL[0]: 内存传感器 #3: Memory(CorrectableEcc) 断言
SEL[1]: 温度传感器 #1: UpperCriticalHigh 断言 (读数:0x5F, 阈值:0x5A)
  → 原始读数:0x5F, 原始阈值:0x5A

=== 警报 (1) ===
  🚨 温度传感器 #1: UpperCriticalHigh 断言 (读数:0x5F, 阈值:0x5A)

条目 0(可纠正 ECC)被记录但不警报。条目 1(上限临界温度)触发警报。两个决策都由穷举模式匹配强制执行——编译器证明每个传感器类型和阈值穿越都已处理。

从解析事件到 Redfish 健康:消费流水线

上面的 walkthrough 以警报结束——但在真实 BMC 中,解析的 SEL 记录流入 Redfish 健康汇总 (ch18)。当前的交接是有损的 bool:

// ❌ 有损——丢弃每子系统细节
pub struct SelSummary {
    pub has_critical_events: bool,
    pub total_entries: u32,
}

这丢失了类型系统刚给我们的所有内容:哪个子系统受影响,什么严重级别,以及读数是否携带动力量纲数据。让我们构建完整的流水线。

步骤 1——SDR 线性化:原始字节 → 量纲类型(ch06)

阈值 SEL 事件在事件数据字节 2-3 中携带原始传感器读数。IPMI SDR(传感器数据记录)提供线性化公式。线性化后,原始字节成为量纲类型:

/// SDR 线性化系数对于单个传感器。
/// 完整公式见 IPMI 规范第 36.3 节。
pub struct SdrLinearization {
    pub sensor_type: SensorType,
    pub m: i16,        // 乘数
    pub b: i16,        // 偏移
    pub r_exp: i8,     // 结果指数(10 的幂)
    pub b_exp: i8,     // B 指数
}

/// 线性化传感器读数附带其单位。
/// 返回类型取决于传感器类型——编译器
/// 强制执行温度传感器产生 Celsius,不是 Rpm。
#[derive(Debug, Clone)]
pub enum LinearizedReading {
    Temperature(Celsius),
    Voltage(Volts),
    Fan(Rpm),
    Current(Amps),
    Power(Watts),
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amps(pub f64);

impl SdrLinearization {
    /// 应用 IPMI 线性化公式:
    ///   y = (M × raw + B × 10^B_exp) × 10^R_exp
    /// 返回基于传感器类型的量纲类型。
    pub fn linearize(&self, raw: u8) -> LinearizedReading {
        let y = (self.m as f64 * raw as f64
                + self.b as f64 * 10_f64.powi(self.b_exp as i32))
                * 10_f64.powi(self.r_exp as i32);

        match self.sensor_type {
            SensorType::Temperature => LinearizedReading::Temperature(Celsius(y)),
            SensorType::Voltage     => LinearizedReading::Voltage(Volts(y)),
            SensorType::Fan         => LinearizedReading::Fan(Rpm(y as u32)),
            SensorType::Current     => LinearizedReading::Current(Amps(y)),
            SensorType::PowerSupply => LinearizedReading::Power(Watts(y)),
            // 其他传感器类型——按需扩展
            _ => LinearizedReading::Temperature(Celsius(y)),
        }
    }
}

这样,SEL walkthrough 中的原始字节 0x5F(95 十进制)成为 Celsius(95.0)——编译器防止与 Rpm 或 Watts 比较。

步骤 2——每子系统健康分类

不是将所有内容折叠成 has_critical_events: bool,将每个解析的 SEL 事件分类为每子系统健康桶:

/// 最差健康值——Ord 给我们免费的 `.max()`。
/// (完整定义在 ch18;这里为 SEL 流水线重现。)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HealthValue { OK, Warning, Critical }

/// 来自单个 SEL 事件的健康贡献,按子系统分类。
#[derive(Debug, Clone)]
pub enum SubsystemHealth {
    Processor(HealthValue),
    Memory(HealthValue),
    PowerSupply(HealthValue),
    Thermal(HealthValue),
    Fan(HealthValue),
    Storage(HealthValue),
    Security(HealthValue),
}

/// 将类型化 SEL 事件分类为每子系统健康。
/// 穷举匹配确保每个传感器类型贡献。
fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth {
    match &record.event {
        TypedEvent::Threshold(t) => {
            // 阈值严重性取决于穿越级别
            let health = match t.crossing {
                // 非临界 → 警告
                ThresholdCrossing::UpperNonCriticalLow
                | ThresholdCrossing::UpperNonCriticalHigh
                | ThresholdCrossing::LowerNonCriticalLow
                | ThresholdCrossing::LowerNonCriticalHigh => HealthValue::Warning,

                // 临界或不可恢复 → 临界
                ThresholdCrossing::UpperCriticalLow
                | ThresholdCrossing::UpperCriticalHigh
                | ThresholdCrossing::LowerCriticalLow
                | ThresholdCrossing::LowerCriticalHigh
                | ThresholdCrossing::UpperNonRecoverableLow
                | ThresholdCrossing::UpperNonRecoverableHigh
                | ThresholdCrossing::LowerNonRecoverableLow
                | ThresholdCrossing::LowerNonRecoverableHigh => HealthValue::Critical,
            };

            // 路由到正确的子系统基于传感器类型
            match record.sensor_type {
                SensorType::Temperature => SubsystemHealth::Thermal(health),
                SensorType::Voltage     => SubsystemHealth::PowerSupply(health),
                SensorType::Current     => SubsystemHealth::PowerSupply(health),
                SensorType::Fan         => SubsystemHealth::Fan(health),
                SensorType::Processor   => SubsystemHealth::Processor(health),
                SensorType::PowerSupply => SubsystemHealth::PowerSupply(health),
                SensorType::Memory      => SubsystemHealth::Memory(health),
                _                       => SubsystemHealth::Thermal(health),
            }
        }

        TypedEvent::SensorSpecific(ss) => match ss {
            SensorSpecificEvent::Memory(m) => {
                let health = match m {
                    MemoryEvent::UncorrectableEcc
                    | MemoryEvent::Parity
                    | MemoryEvent::CriticalOvertemperature => HealthValue::Critical,

                    MemoryEvent::CorrectableEccLogLimit
                    | MemoryEvent::MemoryBoardScrubFailed
                    | MemoryEvent::Throttled => HealthValue::Warning,

                    MemoryEvent::CorrectableEcc
                    | MemoryEvent::PresenceDetected
                    | MemoryEvent::MemoryDeviceDisabled
                    | MemoryEvent::ConfigurationError
                    | MemoryEvent::Spare => HealthValue::OK,
                };
                SubsystemHealth::Memory(health)
            }

            SensorSpecificEvent::PowerSupply(p) => {
                let health = match p {
                    PowerSupplyEvent::Failure
                    | PowerSupplyEvent::InputLost => HealthValue::Critical,

                    PowerSupplyEvent::PredictiveFailure
                    | PowerSupplyEvent::InputOutOfRange
                    | PowerSupplyEvent::InputLostOrOutOfRange
                    | PowerSupplyEvent::ConfigurationError => HealthValue::Warning,

                    PowerSupplyEvent::PresenceDetected
                    | PowerSupplyEvent::InactiveStandby => HealthValue::OK,
                };
                SubsystemHealth::PowerSupply(health)
            }

            SensorSpecificEvent::Processor(p) => {
                let health = match p {
                    ProcessorEvent::Ierr
                    | ProcessorEvent::ThermalTrip
                    | ProcessorEvent::UncorrectableMachineCheck => HealthValue::Critical,

                    ProcessorEvent::Frb1BistFailure
                    | ProcessorEvent::Frb2HangInPost
                    | ProcessorEvent::Frb3ProcessorStartupFailure
                    | ProcessorEvent::ConfigurationError
                    | ProcessorEvent::Disabled => HealthValue::Warning,

                    ProcessorEvent::PresenceDetected
                    | ProcessorEvent::TerminatorPresenceDetected
                    | ProcessorEvent::Throttled => HealthValue::OK,
                };
                SubsystemHealth::Processor(health)
            }

            SensorSpecificEvent::PhysicalSecurity(_) =>
                SubsystemHealth::Security(HealthValue::Warning),

            SensorSpecificEvent::Watchdog(_) =>
                SubsystemHealth::Processor(HealthValue::Warning),

            // 温度、电压、风扇传感器特定事件
            SensorSpecificEvent::Temperature(_) =>
                SubsystemHealth::Thermal(HealthValue::Warning),
            SensorSpecificEvent::Voltage(_) =>
                SubsystemHealth::PowerSupply(HealthValue::Warning),
            SensorSpecificEvent::Fan(_) =>
                SubsystemHealth::Fan(HealthValue::Warning),
        },

        TypedEvent::Discrete { .. } => {
            // 通用离散——按传感器类型分类为警告
            match record.sensor_type {
                SensorType::Processor => SubsystemHealth::Processor(HealthValue::Warning),
                SensorType::Memory    => SubsystemHealth::Memory(HealthValue::Warning),
                _                     => SubsystemHealth::Thermal(HealthValue::OK),
            }
        }
    }
}

每个 match 臂是穷举的——添加新的 MemoryEvent 变体,编译器强制你决定其严重性。添加新的 SensorSpecificEvent 变体,每个消费者必须分类它。这是解析部分 enum 树的回报。

步骤 3——聚合为类型化 SEL 摘要

用保留每子系统健康的结构化摘要替换有损的 bool:

use std::collections::HashMap;

/// 丰富 SEL 摘要——从类型化事件派生的每子系统健康。
/// 这是交给 Redfish 服务器(ch18)进行健康汇总的内容。
#[derive(Debug, Clone)]
pub struct TypedSelSummary {
    pub total_entries: u32,
    pub processor_health: HealthValue,
    pub memory_health: HealthValue,
    pub power_health: HealthValue,
    pub thermal_health: HealthValue,
    pub fan_health: HealthValue,
    pub storage_health: HealthValue,
    pub security_health: HealthValue,
    /// 来自阈值事件的量纲读数(线性化后)。
    pub threshold_readings: Vec<LinearizedThresholdEvent>,
}

/// 附带线性化读数的阈值事件。
#[derive(Debug, Clone)]
pub struct LinearizedThresholdEvent {
    pub sensor_type: SensorType,
    pub sensor_number: u8,
    pub crossing: ThresholdCrossing,
    pub trigger_reading: LinearizedReading,
    pub threshold_value: LinearizedReading,
}

/// 从解析的 SEL 记录构建 TypedSelSummary。
/// 这是消费流水线:解析(上面的步骤 0)→ 分类 → 聚合。
pub fn summarize_sel(
    records: &[ValidSelRecord],
    sdr_table: &HashMap<u8, SdrLinearization>,
) -> TypedSelSummary {
    let mut processor = HealthValue::OK;
    let mut memory = HealthValue::OK;
    let mut power = HealthValue::OK;
    let mut thermal = HealthValue::OK;
    let mut fan = HealthValue::OK;
    let mut storage = HealthValue::OK;
    let mut security = HealthValue::OK;
    let mut threshold_readings = Vec::new();
    let mut count = 0u32;

    for record in records {
        count += 1;

        let ValidSelRecord::SystemEvent(sys) = record else {
            continue; // OEM 记录不贡献健康
        };

        // ── 分类事件 → 每子系统健康 ──
        let health = classify_event_health(sys);
        match &health {
            SubsystemHealth::Processor(h) => processor = processor.max(*h),
            SubsystemHealth::Memory(h)    => memory = memory.max(*h),
            SubsystemHealth::PowerSupply(h) => power = power.max(*h),
            SubsystemHealth::Thermal(h)   => thermal = thermal.max(*h),
            SubsystemHealth::Fan(h)       => fan = fan.max(*h),
            SubsystemHealth::Storage(h)   => storage = storage.max(*h),
            SubsystemHealth::Security(h)  => security = security.max(*h),
        }

        // ── 如果 SDR 可用则线性化阈值读数 ──
        if let TypedEvent::Threshold(t) = &sys.event {
            if let Some(sdr) = sdr_table.get(&sys.sensor_number) {
                threshold_readings.push(LinearizedThresholdEvent {
                    sensor_type: sys.sensor_type,
                    sensor_number: sys.sensor_number,
                    crossing: t.crossing,
                    trigger_reading: sdr.linearize(t.trigger_reading),
                    threshold_value: sdr.linearize(t.threshold_value),
                });
            }
        }
    }

    TypedSelSummary {
        total_entries: count,
        processor_health: processor,
        memory_health: memory,
        power_health: power,
        thermal_health: thermal,
        fan_health: fan,
        storage_health: storage,
        security_health: security,
        threshold_readings,
    }
}

步骤 4——完整流水线:原始字节 → Redfish 健康

这是完整的消费流水线,展示从原始 SEL 字节到 Redfish 就绪健康值的每个类型化交接:

flowchart LR
    RAW["原始 [u8; 16]<br/>SEL 条目"]
    PARSE["TryFrom:<br/>ValidSelRecord<br/>(enum 树)"]
    CLASSIFY["classify_event_health<br/>(穷举匹配)"]
    LINEARIZE["SDR 线性化<br/>原始 → Celsius/Rpm/Watts"]
    SUMMARY["TypedSelSummary<br/>(每子系统健康<br/>+ 量纲读数)"]
    REDFISH["ch18: 健康汇总<br/>→ Status.Health JSON"]

    RAW -->|"ch07 §解析"| PARSE
    PARSE -->|"类型化事件"| CLASSIFY
    PARSE -->|"阈值字节"| LINEARIZE
    CLASSIFY -->|"SubsystemHealth"| SUMMARY
    LINEARIZE -->|"LinearizedReading"| SUMMARY
    SUMMARY -->|"TypedSelSummary"| REDFISH

    style RAW fill:#fff3e0,color:#000
    style PARSE fill:#e1f5fe,color:#000
    style CLASSIFY fill:#f3e5f5,color:#000
    style LINEARIZE fill:#e8f5e9,color:#000
    style SUMMARY fill:#c8e6c9,color:#000
    style REDFISH fill:#bbdefb,color:#000
use std::collections::HashMap;

fn full_sel_pipeline() {
    // ── 来自 BMC 的原始 SEL 数据 ──
    let raw_entries: Vec<[u8; 16]> = vec![
        // 内存可纠正 ECC 在传感器 #3
        [0x01,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x0C, 0x03, 0x6F, 0x00, 0x00,0x00],
        // 温度上限临界在传感器 #1,读数=95,阈值=90
        [0x02,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x01, 0x01, 0x01, 0x09, 0x5F,0x5A],
        // PSU 故障在传感器 #5
        [0x03,0x00, 0x02, 0x00,0x00,0x00,0x00,
         0x20,0x00, 0x04, 0x08, 0x05, 0x6F, 0x01, 0x00,0x00],
    ];

    // ── 步骤 0:边界解析(ch07 TryFrom)──
    let records: Vec<ValidSelRecord> = raw_entries.iter()
        .filter_map(|raw| ValidSelRecord::try_from(RawSelRecord(*raw)).ok())
        .collect();

    // ── 步骤 1-3:分类 + 线性化 + 聚合 ──
    let mut sdr_table = HashMap::new();
    sdr_table.insert(1u8, SdrLinearization {
        sensor_type: SensorType::Temperature,
        m: 1, b: 0, r_exp: 0, b_exp: 0,  // 此示例 1:1 映射
    });

    let summary = summarize_sel(&records, &sdr_table);

    // ── 结果:结构化的、类型化的、Redfish 就绪的 ──
    println!("SEL 摘要:");
    println!("  总条目数:{}", summary.total_entries);
    println!("  处理器:{:?}", summary.processor_health);  // OK
    println!("  内存:{:?}", summary.memory_health);      // OK (可纠正 → OK)
    println!("  电源:{:?}", summary.power_health);       // Critical (PSU 故障)
    println!("  热力:{:?}", summary.thermal_health);     // Critical (上限临界)
    println!("  风扇:{:?}", summary.fan_health);         // OK
    println!("  安全:{:?}", summary.security_health);    // OK

    // 从阈值事件保留的量纲读数:
    for r in &summary.threshold_readings {
        println!("  阈值:传感器 {:?} #{} — {:?} 穿越 {:?}",
            r.sensor_type, r.sensor_number,
            r.trigger_reading, r.crossing);
        // trigger_reading 是 LinearizedReading::Temperature(Celsius(95.0))
        // ——不是原始字节,不是无类型的 f64
    }

    // ── 此摘要直接流入 ch18 的健康汇总 ──
    // compute_system_health() 现在可以使用每子系统值
    // 而不是单个 `has_critical_events: bool`
}

预期输出:

SEL 摘要:
  总条目数:3
  处理器:OK
  内存:OK
  电源:Critical
  热力:Critical
  风扇:OK
  安全:OK
  阈值:传感器 Temperature #1 — Temperature(Celsius(95.0)) 穿越 UpperCriticalHigh

消费流水线证明的内容

阶段模式强制执行的内容
解析有效性边界 (ch07)每个消费者使用类型化 enums,永不使用原始字节
分类穷举匹配每个传感器类型和事件变体映射到健康值——不能忘记
线性化量纲分析 (ch06)原始字节 0x5F 成为 Celsius(95.0),不是 f64——不能与 RPM 混淆
聚合类型化 fold每子系统健康使用 HealthValue::max()——Ord 保证正确性
交接结构化摘要ch18 接收 TypedSelSummary 带有 7 个子系统健康值,不是 bool

与无类型 C 流水线比较:

步骤CRust
解析记录类型switch 可能穿透match 在 enum 上——穷举
分类严重性手动 if 链,忘记 PSU穷举 match——缺少变体时编译错误
线性化读数double——无单位Celsius / Rpm / Watts——不同的类型
聚合健康bool has_critical7 个类型化子系统字段
交接给 Redfish无类型 json_object_set("Health", "OK")TypedSelSummary → 类型化健康汇总 (ch18)

Rust 流水线不仅防止更多 bug——它产生更丰富的输出。C 流水线在每个阶段丢失信息(多态 → 扁平,量纲 → 无类型,每子系统 → 单个 bool)。Rust 流水线保留所有内容,因为类型系统使保留结构比丢弃更容易。

编译器证明的内容

C 中的 BugRust 如何防止
忘记检查记录类型match 在 ValidSelRecord 上——必须处理所有三个变体
触发读数的错误字节索引一次解析到 ThresholdEvent.trigger_reading——消费者从不接触原始字节
缺少传感器类型的 caseSensorSpecificEvent 匹配是穷举的——缺少变体时编译错误
静默丢弃 OEM 记录Enum 变体存在——必须处理或显式 _ => 忽略
比较阈值读数 (°C) 与风扇偏移SDR 线性化后,Celsius ≠ Rpm(ch06)
添加新的传感器类型,忘记警报逻辑#[non_exhaustive] + 穷举匹配 → 下游 crate 中编译错误
事件数据在两个代码路径中解析不同单个 parse_system_event() 边界——一个事实来源

三部曲模式

回顾本章的三个案例研究,注意渐进的弧线:

案例研究输入形状解析复杂性关键技术
FRU(字节)扁平,固定布局一个 TryFrom,检查字段有效性边界类型
Redfish(JSON)结构化,已知模式一个 TryFrom,检查字段 + 嵌套相同技术,不同传输
SEL(多态字节)嵌套判别联合分发链:记录类型 → 事件类型 → 传感器类型Enum 树 + 穷举匹配

所有三个中的原则是相同的:在边界处验证一次,在类型中携带证明,永不重新检查。 SEL 案例研究显示这个原则可以扩展到任意复杂的多态数据——类型系统处理嵌套分发就像扁平字段验证一样自然。

组合有效性类型

有效性类型组合——一个有效字段的结构体本身也是有效的:

#[derive(Debug)]
pub struct ValidFru { format_version: u8 }
#[derive(Debug)]
pub struct ValidThermalResponse { }

/// 完全有效的系统快照。
/// 每个字段独立验证;组合也是有效的。
#[derive(Debug)]
pub struct ValidSystemSnapshot {
    pub fru: ValidFru,
    pub thermal: ValidThermalResponse,
    // 每个字段携带自己的有效性保证。
    // 不需要 "validate_snapshot()" 函数。
}

/// 因为 ValidSystemSnapshot 由有效部分组成,
/// 任何接收它的函数可以信任*所有*数据。
fn generate_health_report(snapshot: &ValidSystemSnapshot) {
    println!("FRU 版本:{}", snapshot.fru.format_version);
    // 不需要验证——类型保证一切
}

关键洞察

在边界处验证。在类型中携带证明。永不重新检查。

这消除了整类 bug:“在这个函数中忘记验证”。如果函数接受 &ValidFru,数据是有效的。句号。

何时使用有效性边界类型

数据源使用有效性边界类型?
来自 BMC 的 IPMI FRU 数据✅ 总是——复杂二进制格式
Redfish JSON 响应✅ 总是——许多必需字段
PCIe 配置空间✅ 总是——寄存器布局严格
SMBIOS 表✅ 总是——带校验和的版本化格式
用户提供的测试参数✅ 总是——防止注入
内部函数调用❌ 通常不——类型已经约束
日志消息❌ 不——尽力而为,不是安全关键

有效性边界流程

flowchart LR
    RAW["原始字节 / JSON"] -->|"TryFrom / serde"| V{"有效?"}
    V -->|是 | VT["ValidFru / ValidRedfish"]
    V -->|否 | E["Err(ParseError)"]
    VT -->|"&ValidFru"| F1["fn process()"] & F2["fn report()"] & F3["fn store()"]
    style RAW fill:#fff3e0,color:#000
    style V fill:#e1f5fe,color:#000
    style VT fill:#c8e6c9,color:#000
    style E fill:#ffcdd2,color:#000
    style F1 fill:#e8f5e9,color:#000
    style F2 fill:#e8f5e9,color:#000
    style F3 fill:#e8f5e9,color:#000

练习:有效 SMBIOS 表

为 SMBIOS Type 17(内存设备)记录设计一个 ValidSmbiosType17 类型:

  • 原始输入是 &[u8];最小长度 21 字节,字节 0 必须是 0x11。
  • 字段:handle: u16、size_mb: u16、speed_mhz: u16。
  • 使用 TryFrom<&[u8]> 以便所有下游函数接受 &ValidSmbiosType17。
解决方案
#[derive(Debug)]
pub struct ValidSmbiosType17 {
    pub handle: u16,
    pub size_mb: u16,
    pub speed_mhz: u16,
}

impl TryFrom<&[u8]> for ValidSmbiosType17 {
    type Error = String;
    fn try_from(raw: &[u8]) -> Result<Self, Self::Error> {
        if raw.len() < 21 {
            return Err(format!("太短:{} < 21", raw.len()));
        }
        if raw[0] != 0x11 {
            return Err(format!("错误类型:0x{:02X} != 0x11", raw[0]));
        }
        Ok(ValidSmbiosType17 {
            handle: u16::from_le_bytes([raw[1], raw[2]]),
            size_mb: u16::from_le_bytes([raw[12], raw[13]]),
            speed_mhz: u16::from_le_bytes([raw[19], raw[20]]),
        })
    }
}

// 下游函数接受验证后的类型——不重新检查
pub fn report_dimm(dimm: &ValidSmbiosType17) -> String {
    format!("DIMM 句柄 0x{:04X}: {}MB @ {}MHz",
        dimm.handle, dimm.size_mb, dimm.speed_mhz)
}

关键要点

  1. 在边界处解析一次——TryFrom 恰好验证一次原始数据;所有下游代码信任类型。
  2. 消除散弹枪式验证——如果函数接受 &ValidFru,数据是有效的。句号。
  3. 模式从扁平扩展到多态——FRU(扁平字节)、Redfish(结构化 JSON)和 SEL(嵌套判别联合)都在递增复杂性下使用相同技术。
  4. 穷举匹配是验证——对于像 SEL 这样的多态数据,编译器的 enum 穷举检查防止“忘记传感器类型“这类 bug,零运行时成本。
  5. 消费流水线保留结构——解析 → 分类 → 线性化 → 聚合保持每子系统健康和量纲读数完整,而 C 有损地缩减为单个 bool。类型系统使保留信息比丢弃更容易。
  6. serde 是自然的边界——#[derive(Deserialize)] 带 #[serde(try_from)] 在解析时验证 JSON。
  7. 组合有效类型——ValidServerHealth 可以要求 ValidFru + ValidThermal + ValidPower。
  8. 与 proptest 配对(ch14)——模糊测试 TryFrom 边界以确保没有有效输入被拒绝且没有无效输入溜过。
  9. 这些模式组合成完整的 Redfish 工作流——ch17 在客户端应用有效性边界(将 JSON 响应解析为类型化结构体),而 ch18 在服务器端反转模式(builder 类型状态确保序列化前每个必需字段存在)。这里构建的 SEL 消费流水线直接流入 ch18 的 TypedSelSummary 健康汇总。

Capability 混合——编译时硬件契约 🟡

你将学到: 原料 trait(总线 capability)如何与混合 trait 和 blanket impl 结合,消除诊断代码重复,同时保证每个硬件依赖在编译时得到满足。

交叉引用: ch04(capability 令牌)、ch09(phantom 类型)、ch10(集成)

问题:诊断代码重复

服务器平台在子系统间共享诊断模式。风扇诊断、温度监控和电源时序都遵循类似的工作流程,但操作不同的硬件总线。没有抽象会导致复制粘贴:

// C — 在子系统间重复逻辑
int run_fan_diag(spi_bus_t *spi, i2c_bus_t *i2c) {
    // ... 50 行 SPI 传感器读取 ...
    // ... 30 行 I2C 寄存器检查 ...
    // ... 20 行阈值比较(与 CPU 诊断相同)...
}

int run_cpu_temp_diag(i2c_bus_t *i2c, gpio_t *gpio) {
    // ... 30 行 I2C 寄存器检查(与风扇诊断相同)...
    // ... 15 行 GPIO 中断检查 ...
    // ... 20 行阈值比较(与风扇诊断相同)...
}

阈值比较逻辑是相同的,但由于总线类型不同,你无法提取它。使用 capability 混合,每个硬件总线是一个原料 trait,当正确的原料存在时,诊断行为会自动提供。

原料 Trait(硬件 Capability)

每个总线或外设都是 trait 上的关联类型。诊断控制器声明它有哪些总线:

/// SPI 总线 capability。
pub trait HasSpi {
    type Spi: SpiBus;
    fn spi(&self) -> &Self::Spi;
}

/// I2C 总线 capability。
pub trait HasI2c {
    type I2c: I2cBus;
    fn i2c(&self) -> &Self::I2c;
}

/// GPIO 引脚访问 capability。
pub trait HasGpio {
    type Gpio: GpioController;
    fn gpio(&self) -> &Self::Gpio;
}

/// IPMI 访问 capability。
pub trait HasIpmi {
    type Ipmi: IpmiClient;
    fn ipmi(&self) -> &Self::Ipmi;
}

// 总线 trait 定义:
pub trait SpiBus {
    fn transfer(&self, data: &[u8]) -> Vec<u8>;
}

pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}

pub trait GpioController {
    fn read_pin(&self, pin: u32) -> bool;
    fn set_pin(&self, pin: u32, value: bool);
}

pub trait IpmiClient {
    fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>;
}

混合 Trait(诊断行为)

混合为任何具有所需 capability 的类型自动提供行为:

pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec<u8>; }
pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}
pub trait GpioController { fn read_pin(&self, pin: u32) -> bool; }
pub trait IpmiClient { fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>; }
pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; }
pub trait HasI2c { type I2c: I2cBus; fn i2c(&self) -> &Self::I2c; }
pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; }
pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; }

/// 风扇诊断混合——为任何具有 SPI + I2C 的类型自动实现。
pub trait FanDiagMixin: HasSpi + HasI2c {
    fn read_fan_speed(&self, fan_id: u8) -> u32 {
        // 通过 SPI 读取转速计
        let cmd = [0x80 | fan_id, 0x00];
        let response = self.spi().transfer(&cmd);
        u32::from_be_bytes([0, 0, response[0], response[1]])
    }

    fn set_fan_pwm(&self, fan_id: u8, duty_percent: u8) {
        // 通过 I2C 控制器设置 PWM
        self.i2c().write_register(0x2E, fan_id, duty_percent);
    }

    fn run_fan_diagnostic(&self) -> bool {
        // 完整诊断:读取所有风扇,检查阈值
        for fan_id in 0..6 {
            let speed = self.read_fan_speed(fan_id);
            if speed < 1000 || speed > 20000 {
                println!("风扇 {fan_id}:FAIL ({speed} RPM)");
                return false;
            }
        }
        true
    }
}

// Blanket impl——任何具有 SPI + I2C 的类型免费获得 FanDiagMixin
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}

/// 温度监控混合——需要 I2C + GPIO。
pub trait TempMonitorMixin: HasI2c + HasGpio {
    fn read_temperature(&self, sensor_addr: u8) -> f64 {
        let raw = self.i2c().read_register(sensor_addr, 0x00);
        raw as f64 * 0.5  // 每 LSB 0.5°C
    }

    fn check_thermal_alert(&self, alert_pin: u32) -> bool {
        self.gpio().read_pin(alert_pin)
    }

    fn run_thermal_diagnostic(&self) -> bool {
        for addr in [0x48, 0x49, 0x4A] {
            let temp = self.read_temperature(addr);
            if temp > 95.0 {
                println!("传感器 0x{addr:02X}:CRITICAL ({temp}°C)");
                return false;
            }
            if self.check_thermal_alert(addr as u32) {
                println!("传感器 0x{addr:02X}:ALERT 引脚断言");
                return false;
            }
        }
        true
    }
}

impl<T: HasI2c + HasGpio> TempMonitorMixin for T {}

/// 电源时序混合——需要 I2C + IPMI。
pub trait PowerSeqMixin: HasI2c + HasIpmi {
    fn read_voltage_rail(&self, rail: u8) -> f64 {
        let raw = self.i2c().read_register(0x40, rail);
        raw as f64 * 0.01  // 每 LSB 10mV
    }

    fn check_power_good(&self) -> bool {
        let resp = self.ipmi().send_raw(0x04, 0x2D, &[0x01]);
        !resp.is_empty() && resp[0] == 0x00
    }
}

impl<T: HasI2c + HasIpmi> PowerSeqMixin for T {}

具体控制器——混合搭配

具体诊断控制器声明它的能力,并自动继承所有匹配的混合:

pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec<u8>; }
pub trait I2cBus {
    fn read_register(&self, addr: u8, reg: u8) -> u8;
    fn write_register(&self, addr: u8, reg: u8, value: u8);
}
pub trait GpioController {
    fn read_pin(&self, pin: u32) -> bool;
    fn set_pin(&self, pin: u32, value: bool);
}
pub trait IpmiClient { fn send_raw(&self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8>; }
pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; }
pub trait HasI2c { type I2c: I2cBus; fn i2c(&self) -> &Self::I2c; }
pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; }
pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; }
pub trait FanDiagMixin: HasSpi + HasI2c {}
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}
pub trait TempMonitorMixin: HasI2c + HasGpio {}
impl<T: HasI2c + HasGpio> TempMonitorMixin for T {}
pub trait PowerSeqMixin: HasI2c + HasIpmi {}
impl<T: HasI2c + HasIpmi> PowerSeqMixin for T {}

// 具体总线实现(示意存根)
pub struct LinuxSpi { bus: u8 }
impl SpiBus for LinuxSpi {
    fn transfer(&self, data: &[u8]) -> Vec<u8> { vec![0; data.len()] }
}

pub struct LinuxI2c { bus: u8 }
impl I2cBus for LinuxI2c {
    fn read_register(&self, _addr: u8, _reg: u8) -> u8 { 42 }
    fn write_register(&self, _addr: u8, _reg: u8, _value: u8) {}
}

pub struct LinuxGpio;
impl GpioController for LinuxGpio {
    fn read_pin(&self, _pin: u32) -> bool { false }
    fn set_pin(&self, _pin: u32, _value: bool) {}
}

pub struct IpmiToolClient;
impl IpmiClient for IpmiToolClient {
    fn send_raw(&self, _netfn: u8, _cmd: u8, _data: &[u8]) -> Vec<u8> { vec![0x00] }
}

/// BaseBoardController 具有所有总线 → 获得所有混合。
pub struct BaseBoardController {
    spi: LinuxSpi,
    i2c: LinuxI2c,
    gpio: LinuxGpio,
    ipmi: IpmiToolClient,
}

impl HasSpi for BaseBoardController {
    type Spi = LinuxSpi;
    fn spi(&self) -> &LinuxSpi { &self.spi }
}

impl HasI2c for BaseBoardController {
    type I2c = LinuxI2c;
    fn i2c(&self) -> &LinuxI2c { &self.i2c }
}

impl HasGpio for BaseBoardController {
    type Gpio = LinuxGpio;
    fn gpio(&self) -> &LinuxGpio { &self.gpio }
}

impl HasIpmi for BaseBoardController {
    type Ipmi = IpmiToolClient;
    fn ipmi(&self) -> &IpmiToolClient { &self.ipmi }
}

// BaseBoardController 现在自动具有:
// - FanDiagMixin    (因为它 HasSpi + HasI2c)
// - TempMonitorMixin(因为它 HasI2C + HasGpio)
// - PowerSeqMixin   (因为它 HasI2C + HasIpmi)
// 不需要手动实现——blanket impl 完成所有工作。

构造正确性方面

混合模式是构造正确的,因为:

  1. 没有 SPI 就无法调用 read_fan_speed() ——该方法只存在于实现 HasSpi + HasI2c 的类型上
  2. 你不能忘记总线 ——如果从 BaseBoardController 中移除 HasSpi,FanDiagMixin 方法在编译时消失
  3. Mock 测试是自动的 ——用 MockSpi 替换 LinuxSpi,所有混合逻辑与 mock 一起工作
  4. 新平台只需声明能力 ——只有 I2C 的 GPU 子卡获得 TempMonitorMixin(如果它也有 GPIO),但没有 FanDiagMixin(没有 SPI)

何时使用 Capability 混合

场景使用混合?
跨职能诊断行为✅ 是——防止复制粘贴
多总线硬件控制器✅ 是——声明能力,获取行为
平台特定测试台✅ 是——mock 能力进行测试
单总线简单外设⚠️ 开销可能不值得
纯业务逻辑(无硬件)❌ 更简单的模式足够

混合 Trait 架构

flowchart TD
    subgraph "原料 Trait"
        SPI["HasSpi"]
        I2C["HasI2c"]
        GPIO["HasGpio"]
    end
    subgraph "混合 Trait(blanket impl)"
        FAN["FanDiagMixin"]
        TEMP["TempMonitorMixin"]
    end
    SPI & I2C -->|"需要两者"| FAN
    I2C & GPIO -->|"需要两者"| TEMP
    subgraph "具体类型"
        BBC["BaseBoardController"]
    end
    BBC -->|"实现 HasSpi + HasI2c + HasGpio"| FAN & TEMP
    style SPI fill:#e1f5fe,color:#000
    style I2C fill:#e1f5fe,color:#000
    style GPIO fill:#e1f5fe,color:#000
    style FAN fill:#c8e6c9,color:#000
    style TEMP fill:#c8e6c9,color:#000
    style BBC fill:#fff3e0,color:#000

练习:网络诊断混合

为网络诊断设计混合系统:

  • 原料 trait:HasEthernet、HasIpmi
  • 混合:LinkHealthMixin(需要 HasEthernet),具有 check_link_status(&self)
  • 混合:RemoteDiagMixin(需要 HasEthernet + HasIpmi),具有 remote_health_check(&self)
  • 具体类型:NicController 实现两个原料。
解决方案
pub trait HasEthernet {
    fn eth_link_up(&self) -> bool;
}

pub trait HasIpmi {
    fn ipmi_ping(&self) -> bool;
}

pub trait LinkHealthMixin: HasEthernet {
    fn check_link_status(&self) -> &'static str {
        if self.eth_link_up() { "link: UP" } else { "link: DOWN" }
    }
}
impl<T: HasEthernet> LinkHealthMixin for T {}

pub trait RemoteDiagMixin: HasEthernet + HasIpmi {
    fn remote_health_check(&self) -> &'static str {
        if self.eth_link_up() && self.ipmi_ping() {
            "remote: HEALTHY"
        } else {
            "remote: DEGRADED"
        }
    }
}
impl<T: HasEthernet + HasIpmi> RemoteDiagMixin for T {}

pub struct NicController;
impl HasEthernet for NicController {
    fn eth_link_up(&self) -> bool { true }
}
impl HasIpmi for NicController {
    fn ipmi_ping(&self) -> bool { true }
}
// NicController 自动获得两个混合方法

关键要点

  1. 原料 trait 声明硬件能力 ——HasSpi、HasI2c、HasGpio 是关联类型 trait。
  2. 混合 trait 通过 blanket impl 提供行为 ——impl<T: HasSpi + HasI2c> FanDiagMixin for T {}。
  3. 添加新平台 = 列出它的能力 ——编译器提供所有匹配的混合方法。
  4. 移除总线 = 使用它的地方都是编译错误 ——你不会忘记更新下游代码。
  5. Mock 测试是免费的 ——用 MockSpi 替换 LinuxSpi;所有混合逻辑不变工作。

Phantom 类型用于资源跟踪 🟡

你将学到: PhantomData 标记如何在类型级别编码寄存器宽度、DMA 方向和文件描述符状态——以零运行时成本防止整个资源不匹配 bug 类别。

交叉引用: ch05(type-state)、ch06(量纲类型)、ch08(混合)、ch10(集成)

问题:混淆资源

硬件资源在代码中看起来相似但不可互换:

  • 32 位寄存器和 16 位寄存器都是“寄存器“
  • 用于读取的 DMA 缓冲区和用于写入的 DMA 缓冲区看起来都是 *mut u8
  • 打开的文件描述符和关闭的都是 i32

在 C 中:

// C — 所有寄存器看起来都一样
uint32_t read_reg32(volatile void *base, uint32_t offset);
uint16_t read_reg16(volatile void *base, uint32_t offset);

// Bug:用 32 位函数读取 16 位寄存器
uint32_t status = read_reg32(pcie_bar, LINK_STATUS_REG);  // 应该是 reg16!

Phantom 类型参数

phantom 类型是出现在结构体定义中但不在任何字段中的类型参数。它纯粹为了携带类型级信息而存在:

use std::marker::PhantomData;

// 寄存器宽度标记——零大小
pub struct Width8;
pub struct Width16;
pub struct Width32;
pub struct Width64;

/// 寄存器句柄,按其宽度参数化。
/// PhantomData<W> 成本为零字节——它只是编译时标记。
pub struct Register<W> {
    base: usize,
    offset: usize,
    _width: PhantomData<W>,
}

impl Register<Width8> {
    pub fn read(&self) -> u8 {
        // ... 从 base + offset 读取 1 字节 ...
        0 // 存根
    }
    pub fn write(&self, _value: u8) {
        // ... 写入 1 字节 ...
    }
}

impl Register<Width16> {
    pub fn read(&self) -> u16 {
        // ... 从 base + offset 读取 2 字节 ...
        0 // 存根
    }
    pub fn write(&self, _value: u16) {
        // ... 写入 2 字节 ...
    }
}

impl Register<Width32> {
    pub fn read(&self) -> u32 {
        // ... 从 base + offset 读取 4 字节 ...
        0 // 存根
    }
    pub fn write(&self, _value: u32) {
        // ... 写入 4 字节 ...
    }
}

/// PCIe 配置空间寄存器定义。
pub struct PcieConfig {
    base: usize,
}

impl PcieConfig {
    pub fn vendor_id(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x00, _width: PhantomData }
    }

    pub fn device_id(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x02, _width: PhantomData }
    }

    pub fn command(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x04, _width: PhantomData }
    }

    pub fn status(&self) -> Register<Width16> {
        Register { base: self.base, offset: 0x06, _width: PhantomData }
    }

    pub fn bar0(&self) -> Register<Width32> {
        Register { base: self.base, offset: 0x10, _width: PhantomData }
    }
}

fn pcie_example() {
    let cfg = PcieConfig { base: 0xFE00_0000 };

    let vid: u16 = cfg.vendor_id().read();    // 返回 u16 ✅
    let bar: u32 = cfg.bar0().read();         // 返回 u32 ✅

    // 不能混淆它们:
    // let bad: u32 = cfg.vendor_id().read(); // ❌ 错误:期望 u16
    // cfg.bar0().write(0u16);                // ❌ 错误:期望 u32
}

DMA 缓冲区访问控制

DMA 缓冲区有方向:有些用于设备到主机(读取),另一些用于主机到设备(写入)。使用错误的方向会损坏数据或导致总线错误:

use std::marker::PhantomData;

// 方向标记
pub struct ToDevice;     // 主机写入,设备读取
pub struct FromDevice;   // 设备写入,主机读取

/// 具有方向强制的 DMA 缓冲区。
pub struct DmaBuffer<Dir> {
    ptr: *mut u8,
    len: usize,
    dma_addr: u64,  // 设备的物理地址
    _dir: PhantomData<Dir>,
}

impl DmaBuffer<ToDevice> {
    /// 用要发送到设备的数据填充缓冲区。
    pub fn write_data(&mut self, data: &[u8]) {
        assert!(data.len() <= self.len);
        // SAFETY: ptr 对 self.len 字节有效(在构造时分配),
        // 且 data.len() <= self.len(上面已断言)。
        unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), self.ptr, data.len()) }
    }

    /// 获取 DMA 地址供设备读取。
    pub fn device_addr(&self) -> u64 {
        self.dma_addr
    }
}

impl DmaBuffer<FromDevice> {
    /// 读取设备写入缓冲区的数据。
    pub fn read_data(&self) -> &[u8] {
        // SAFETY: ptr 对 self.len 字节有效,且设备
        // 已完成写入(调用者确保 DMA 传输完成)。
        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
    }

    /// 获取 DMA 地址供设备写入。
    pub fn device_addr(&self) -> u64 {
        self.dma_addr
    }
}

// 不能写入 FromDevice 缓冲区:
// fn oops(buf: &mut DmaBuffer<FromDevice>) {
//     buf.write_data(&[1, 2, 3]);  // ❌ 没有 `write_data` 方法在 DmaBuffer<FromDevice> 上
// }

// 不能从 ToDevice 缓冲区读取:
// fn oops2(buf: &DmaBuffer<ToDevice>) {
//     let data = buf.read_data();  // ❌ 没有 `read_data` 方法在 DmaBuffer<ToDevice> 上
// }

文件描述符所有权

一个常见的 bug:在文件描述符关闭后使用它。Phantom 类型可以跟踪打开/关闭状态:

use std::marker::PhantomData;

pub struct Open;
pub struct Closed;

/// 具有状态跟踪的文件描述符。
pub struct Fd<State> {
    raw: i32,
    _state: PhantomData<State>,
}

impl Fd<Open> {
    pub fn open(path: &str) -> Result<Self, String> {
        // ... 打开文件 ...
        Ok(Fd { raw: 3, _state: PhantomData }) // 存根
    }

    pub fn read(&self, buf: &mut [u8]) -> Result<usize, String> {
        // ... 从 fd 读取 ...
        Ok(0) // 存根
    }

    pub fn write(&self, data: &[u8]) -> Result<usize, String> {
        // ... 写入 fd ...
        Ok(data.len()) // 存根
    }

    /// 关闭 fd——返回 Closed 句柄。
    /// Open 句柄被消费,防止关闭后使用。
    pub fn close(self) -> Fd<Closed> {
        // ... 关闭 fd ...
        Fd { raw: self.raw, _state: PhantomData }
    }
}

impl Fd<Closed> {
    // 没有 read() 或 write() 方法——它们在 Fd<Closed> 上不存在。
    // 这使得关闭后使用成为编译错误。

    pub fn raw_fd(&self) -> i32 {
        self.raw
    }
}

fn fd_example() -> Result<(), String> {
    let fd = Fd::open("/dev/ipmi0")?;
    let mut buf = [0u8; 256];
    fd.read(&mut buf)?;

    let closed = fd.close();

    // closed.read(&mut buf)?;  // ❌ 没有 `read` 方法在 Fd<Closed> 上
    // closed.write(&[1])?;     // ❌ 没有 `write` 方法在 Fd<Closed> 上

    Ok(())
}

结合 Phantom 类型与早期模式

Phantom 类型与我们所见的一切组合:

use std::marker::PhantomData;
pub struct Width32;
pub struct Width16;
pub struct Register<W> { _w: PhantomData<W> }
impl Register<Width16> { pub fn read(&self) -> u16 { 0 } }
impl Register<Width32> { pub fn read(&self) -> u32 { 0 } }
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

/// 结合 phantom 类型(寄存器宽度)与量纲类型(Celsius)。
fn read_temp_sensor(reg: &Register<Width16>) -> Celsius {
    let raw = reg.read();  // phantom 类型保证 u16
    Celsius(raw as f64 * 0.0625)  // 返回类型保证 Celsius
}

// 编译器强制执行:
// 1. 寄存器是 16 位的(phantom 类型)
// 2. 结果是 Celsius(newtype)
// 两者都是零运行时成本。

何时使用 Phantom 类型

场景使用 phantom 参数?
寄存器宽度编码✅ 总是——防止宽度不匹配
DMA 缓冲区方向✅ 总是——防止数据损坏
文件描述符状态✅ 总是——防止关闭后使用
内存区域权限(R/W/X)✅ 总是——强制执行访问控制
通用容器(Vec、HashMap)❌ 不——使用具体类型参数
运行时可变属性❌ 不——phantom 类型仅编译时有效

Phantom 类型资源矩阵

flowchart TD
    subgraph "宽度标记"
        W8["Width8"] 
        W16["Width16"]
        W32["Width32"]
    end
    subgraph "方向标记"
        RD["Read"]
        WR["Write"]
    end
    subgraph "类型化资源"
        R1["Register<Width16>"]
        R2["DmaBuffer<Read>"]
        R3["DmaBuffer<Write>"]
    end
    W16 --> R1
    RD --> R2
    WR --> R3
    R2 -.->|"写入尝试"| ERR["❌ 编译错误"]
    style W8 fill:#e1f5fe,color:#000
    style W16 fill:#e1f5fe,color:#000
    style W32 fill:#e1f5fe,color:#000
    style RD fill:#c8e6c9,color:#000
    style WR fill:#fff3e0,color:#000
    style R1 fill:#e8eaf6,color:#000
    style R2 fill:#c8e6c9,color:#000
    style R3 fill:#fff3e0,color:#000
    style ERR fill:#ffcdd2,color:#000

练习:内存区域权限

为具有读、写和执行权限的内存区域设计 phantom 类型:

  • MemRegion<ReadOnly> 具有 fn read(&self, offset: usize) -> u8
  • MemRegion<ReadWrite> 具有 read 和 write
  • MemRegion<Executable> 具有 read 和 fn execute(&self)
  • 写入 ReadOnly 或执行 ReadWrite 应该无法编译。
解决方案
use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;
pub struct Executable;

pub struct MemRegion<Perm> {
    base: *mut u8,
    len: usize,
    _perm: PhantomData<Perm>,
}

// 所有权限类型都可读取
impl<P> MemRegion<P> {
    pub fn read(&self, offset: usize) -> u8 {
        assert!(offset < self.len);
        // SAFETY: offset < self.len(上面已断言),base 对 len 字节有效。
        unsafe { *self.base.add(offset) }
    }
}

impl MemRegion<ReadWrite> {
    pub fn write(&mut self, offset: usize, val: u8) {
        assert!(offset < self.len);
        // SAFETY: offset < self.len(上面已断言),base 对 len 字节有效,
        // 且 &mut self 确保独占访问。
        unsafe { *self.base.add(offset) = val; }
    }
}

impl MemRegion<Executable> {
    pub fn execute(&self) {
        // 跳转到基地址(概念上)
    }
}

// ❌ region_ro.write(0, 0xFF);  // 编译错误:没有 `write` 方法
// ❌ region_rw.execute();       // 编译错误:没有 `execute` 方法

关键要点

  1. PhantomData 以零大小携带类型级信息 ——标记只存在于编译器中。
  2. 寄存器宽度不匹配成为编译错误 ——Register<Width16> 返回 u16,而不是 u32。
  3. DMA 方向在结构上强制执行 ——DmaBuffer<Read> 没有 write() 方法。
  4. 与量纲类型结合(ch06) ——Register<Width16> 可以通过解析步骤返回 Celsius。
  5. Phantom 类型仅编译时有效 ——它们不适用于运行时可变属性;对这些使用枚举。

Const Fn —— 编译时正确性证明 🟠

你将学到: const fn 和 assert! 如何将编译器变成证明引擎——在编译时验证 SRAM 内存映射、寄存器布局、协议帧、位域掩码、时钟树和查找表,零运行时成本。

交叉引用: ch04(capability 令牌)、ch06(量纲分析)、ch09(phantom 类型)

问题:虚假的内存映射

在嵌入式和系统编程中,内存映射是一切的基础——它们定义了 bootloader、固件、数据段和栈的位置。边界出错,两个子系统会静默相互损坏。在 C 中,这些映射通常是 #define 常量,没有结构关系:

/* STM32F4 SRAM 布局——256 KB 在 0x20000000 */
#define SRAM_BASE       0x20000000
#define SRAM_SIZE       (256 * 1024)

#define BOOT_BASE       0x20000000
#define BOOT_SIZE       (16 * 1024)

#define FW_BASE         0x20004000
#define FW_SIZE         (128 * 1024)

#define DATA_BASE       0x20024000
#define DATA_SIZE       (80 * 1024)     /* 有人把这项从 64K 增加到 80K */

#define STACK_BASE      0x20038000
#define STACK_SIZE      (48 * 1024)     /* 0x20038000 + 48K = 0x20044000 —— 超出 SRAM 末尾! */

bug:16 + 128 + 80 + 48 = 272 KB,但 SRAM 只有 256 KB。栈超出物理内存末尾 16 KB。没有编译器警告,没有链接器错误,没有运行时检查——只有当栈增长到未映射空间时的静默损坏。

每个失败模式都是在部署后才发现的 —— 可能表现为仅在重栈使用时才出现的神秘崩溃,发生在数据段调整后数周。

Const Fn:将编译器变成证明引擎

Rust 的 const fn 函数可以在编译时运行。当 const fn 在编译时求值期间 panic 时,panic 变成编译错误。与 assert! 结合,这将编译器变成你的不变量的定理证明器:

pub const fn checked_add(a: u32, b: u32) -> u32 {
    let sum = a as u64 + b as u64;
    assert!(sum <= u32::MAX as u64, "溢出");
    sum as u32
}

// ✅ 编译通过——100 + 200 适合 u32
const X: u32 = checked_add(100, 200);

// ❌ 编译错误:"溢出"
// const Y: u32 = checked_add(u32::MAX, 1);

fn main() {
    println!("{X}");
}

关键见解: const fn + assert! = 证明义务。每个断言是一个编译器必须验证的定理。如果证明失败,程序无法编译。不需要测试套件,不需要代码审查捕获——编译器本身就是审计员。

构建已验证的 SRAM 内存映射

Region 类型

Region 表示连续的内存块。它的构造函数是一个 const fn,强制执行基本有效性:

#[derive(Debug, Clone, Copy)]
pub struct Region {
    pub base: u32,
    pub size: u32,
}

impl Region {
    /// 创建区域。如果不变量失败,在编译时 panic。
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "区域大小必须非零");
        assert!(
            base as u64 + size as u64 <= u32::MAX as u64,
            "区域超出 32 位地址空间"
        );
        Self { base, size }
    }

    pub const fn end(&self) -> u32 {
        self.base + self.size
    }

    /// 如果 `inner` 完全适合 `self` 则为真。
    pub const fn contains(&self, inner: &Region) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }

    /// 如果两个区域共享任何地址则为真。
    pub const fn overlaps(&self, other: &Region) -> bool {
        self.base < other.end() && other.base < self.end()
    }

    /// 如果 `addr` 落在此区域内则为真。
    pub const fn contains_addr(&self, addr: u32) -> bool {
        addr >= self.base && addr < self.end()
    }
}

// 每个 Region 诞生时都是有效的——你无法构造无效的区域
const R: Region = Region::new(0x2000_0000, 1024);

fn main() {
    println!("区域:{:#010X}..{:#010X}", R.base, R.end());
}

已验证的内存映射

现在我们将区域组合成完整的 SRAM 映射。构造函数证明六个无重叠不变量和四个包含不变量——全部在编译时:

#[derive(Debug, Clone, Copy)]
pub struct Region { pub base: u32, pub size: u32 }
impl Region {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "区域大小必须非零");
        assert!(base as u64 + size as u64 <= u32::MAX as u64, "溢出");
        Self { base, size }
    }
    pub const fn end(&self) -> u32 { self.base + self.size }
    pub const fn contains(&self, inner: &Region) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }
    pub const fn overlaps(&self, other: &Region) -> bool {
        self.base < other.end() && other.base < self.end()
    }
}
pub struct SramMap {
    pub total:      Region,
    pub bootloader: Region,
    pub firmware:   Region,
    pub data:       Region,
    pub stack:      Region,
}

impl SramMap {
    pub const fn verified(
        total: Region,
        bootloader: Region,
        firmware: Region,
        data: Region,
        stack: Region,
    ) -> Self {
        // ── 包含:每个子区域适合总 SRAM ──
        assert!(total.contains(&bootloader), "bootloader 超出 SRAM");
        assert!(total.contains(&firmware),   "firmware 超出 SRAM");
        assert!(total.contains(&data),       "数据段超出 SRAM");
        assert!(total.contains(&stack),      "栈超出 SRAM");

        // ── 无重叠:没有一对子区域共享地址 ──
        assert!(!bootloader.overlaps(&firmware), "bootloader/firmware 重叠");
        assert!(!bootloader.overlaps(&data),     "bootloader/data 重叠");
        assert!(!bootloader.overlaps(&stack),    "bootloader/栈重叠");
        assert!(!firmware.overlaps(&data),       "firmware/data 重叠");
        assert!(!firmware.overlaps(&stack),      "firmware/栈重叠");
        assert!(!data.overlaps(&stack),          "数据/栈重叠");

        Self { total, bootloader, firmware, data, stack }
    }
}

// ✅ 所有 10 个不变量在编译时验证——零运行时成本
const SRAM: SramMap = SramMap::verified(
    Region::new(0x2000_0000, 256 * 1024),   // 256 KB 总 SRAM
    Region::new(0x2000_0000,  16 * 1024),   // bootloader: 16 KB
    Region::new(0x2000_4000, 128 * 1024),   // firmware:  128 KB
    Region::new(0x2002_4000,  64 * 1024),   // data:       64 KB
    Region::new(0x2003_4000,  48 * 1024),   // stack:      48 KB
);

fn main() {
    println!("SRAM:  {:#010X} — {} KB", SRAM.total.base, SRAM.total.size / 1024);
    println!("Boot:  {:#010X} — {} KB", SRAM.bootloader.base, SRAM.bootloader.size / 1024);
    println!("FW:    {:#010X} — {} KB", SRAM.firmware.base, SRAM.firmware.size / 1024);
    println!("Data:  {:#010X} — {} KB", SRAM.data.base, SRAM.data.size / 1024);
    println!("Stack: {:#010X} — {} KB", SRAM.stack.base, SRAM.stack.size / 1024);
}

十个编译时检查,零运行时指令。二进制文件只包含已验证的常量。

破坏映射

假设有人将数据段从 64 KB 增加到 80 KB 而不调整其他任何东西:

// ❌ 无法编译
const BAD_SRAM: SramMap = SramMap::verified(
    Region::new(0x2000_0000, 256 * 1024),
    Region::new(0x2000_0000,  16 * 1024),
    Region::new(0x2000_4000, 128 * 1024),
    Region::new(0x2002_4000,  80 * 1024),   // 80 KB —— 16 KB 太大
    Region::new(0x2003_8000,  48 * 1024),   // 栈被推到 SRAM 末尾之外
);

编译器报告:

error[E0080]: evaluation of constant value failed
  --> src/main.rs:38:9
   |
38 |         assert!(total.contains(&stack), "stack exceeds SRAM");
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         the evaluated program panicked at 'stack exceeds SRAM'

原本是神秘现场失败的 bug 现在是编译错误。 不需要单元测试,不需要代码审查捕获——编译器证明它不可能。与 C 对比,同样的 bug 会静默发布,并在数月后作为栈损坏出现在现场。

使用 Phantom 类型分层访问控制

将 const fn 验证与 phantom 类型访问权限(ch09)结合,在类型级别强制执行读/写约束:

use std::marker::PhantomData;

pub struct ReadOnly;
pub struct ReadWrite;

pub struct TypedRegion<Access> {
    base: u32,
    size: u32,
    _access: PhantomData<Access>,
}

impl<A> TypedRegion<A> {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "区域大小必须非零");
        Self { base, size, _access: PhantomData }
    }
}

// 读取可用于任何访问级别
fn read_word<A>(region: &TypedRegion<A>, offset: u32) -> u32 {
    assert!(offset + 4 <= region.size, "读取越界");
    // 真实固件:unsafe { core::ptr::read_volatile((region.base + offset) as *const u32) }
    0 // 存根
}

// 写入需要 ReadWrite——函数签名强制执行它
fn write_word(region: &TypedRegion<ReadWrite>, offset: u32, value: u32) {
    assert!(offset + 4 <= region.size, "写入越界");
    // 真实固件:unsafe { core::ptr::write_volatile(...) }
    let _ = value; // 存根
}

const BOOTLOADER: TypedRegion<ReadOnly>  = TypedRegion::new(0x2000_0000, 16 * 1024);
const DATA:       TypedRegion<ReadWrite> = TypedRegion::new(0x2002_4000, 64 * 1024);

fn main() {
    read_word(&BOOTLOADER, 0);      // ✅ 从只读区域读取
    read_word(&DATA, 0);            // ✅ 从读写区域读取
    write_word(&DATA, 0, 42);       // ✅ 写入读写区域
    // write_word(&BOOTLOADER, 0, 42); // ❌ 编译错误:期望 ReadWrite,找到 ReadOnly
}

bootloader 区域在物理上是可写的(它是 SRAM),但类型系统防止意外写入。硬件能力和软件权限之间的这种区别正是正确构造的含义。

指针溯源:证明地址属于区域

更进一步,我们可以创建已验证的地址——在静态上被证明位于特定区域内的值:

#[derive(Debug, Clone, Copy)]
pub struct Region { pub base: u32, pub size: u32 }
impl Region {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0);
        assert!(base as u64 + size as u64 <= u32::MAX as u64);
        Self { base, size }
    }
    pub const fn end(&self) -> u32 { self.base + self.size }
    pub const fn contains_addr(&self, addr: u32) -> bool {
        addr >= self.base && addr < self.end()
    }
}
/// 在编译时被证明位于区域内的地址。
pub struct VerifiedAddr {
    addr: u32, // 私有的——只能通过已检查的构造函数创建
}

impl VerifiedAddr {
    /// 如果 `addr` 在 `region` 外,在编译时 panic。
    pub const fn new(region: &Region, addr: u32) -> Self {
        assert!(region.contains_addr(addr), "地址不在区域内");
        Self { addr }
    }

    pub const fn raw(&self) -> u32 {
        self.addr
    }
}

const DATA: Region = Region::new(0x2002_4000, 64 * 1024);

// ✅ 在编译时被证明在数据区域内
const STATUS_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_4000);
const CONFIG_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_5000);

// ❌ 无法编译:地址在 bootloader 区域内,不在数据区
// const BAD_ADDR: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2000_0000);

fn main() {
    println!("状态寄存器在 {:#010X}", STATUS_WORD.raw());
    println!("配置寄存器在 {:#010X}", CONFIG_WORD.raw());
}

在编译时建立溯源 —— 访问这些地址时不需要运行时边界检查。构造函数是私有的,所以只有编译器证明有效时 VerifiedAddr 才能存在。

内存映射之外

const fn 证明模式适用于任何你有编译时已知值和结构不变量的地方。上面的 SRAM 映射证明了区域间属性(包含、无重叠)。同样的技术可以扩展到越来越细粒度的域:

flowchart TD
    subgraph coarse["粗粒度"]
        MEM["内存映射<br/>区域不重叠"]
        REG["寄存器映射<br/>偏移对齐且不相交"]
    end

    subgraph fine["细粒度"]
        BIT["位域布局<br/>掩码在寄存器内不相交"]
        FRAME["协议帧<br/>字段连续,总数 ≤ 最大值"]
    end

    subgraph derived["派生值链"]
        PLL["时钟树/PLL<br/>每个中间频率在范围内"]
        LUT["查找表<br/>在编译时计算和验证"]
    end

    MEM --> REG --> BIT
    MEM --> FRAME
    REG --> PLL
    PLL --> LUT

    style MEM fill:#c8e6c9,color:#000
    style REG fill:#c8e6c9,color:#000
    style BIT fill:#e1f5fe,color:#000
    style FRAME fill:#e1f5fe,color:#000
    style PLL fill:#fff3e0,color:#000
    style LUT fill:#fff3e0,color:#000

每个子节遵循相同的模式:定义一个带有 const fn 构造函数的类型,编码不变量,然后使用 const _: () = { ... } 或 const 绑定来触发验证。

寄存器映射

硬件寄存器块有固定的偏移和宽度。错误对齐或重叠的寄存器定义总是 bug:

#[derive(Debug, Clone, Copy)]
pub struct Register {
    pub offset: u32,
    pub width: u32,
}

impl Register {
    pub const fn new(offset: u32, width: u32) -> Self {
        assert!(
            width == 1 || width == 2 || width == 4,
            "寄存器宽度必须是 1、2 或 4 字节"
        );
        assert!(offset % width == 0, "寄存器必须自然对齐");
        Self { offset, width }
    }

    pub const fn end(&self) -> u32 {
        self.offset + self.width
    }
}

const fn disjoint(a: &Register, b: &Register) -> bool {
    a.end() <= b.offset || b.end() <= a.offset
}

// UART 外设寄存器
const DATA:   Register = Register::new(0x00, 4);
const STATUS: Register = Register::new(0x04, 4);
const CTRL:   Register = Register::new(0x08, 4);
const BAUD:   Register = Register::new(0x0C, 4);

// 编译时证明:没有寄存器重叠另一个
const _: () = {
    assert!(disjoint(&DATA,   &STATUS));
    assert!(disjoint(&DATA,   &CTRL));
    assert!(disjoint(&DATA,   &BAUD));
    assert!(disjoint(&STATUS, &CTRL));
    assert!(disjoint(&STATUS, &BAUD));
    assert!(disjoint(&CTRL,   &BAUD));
};

fn main() {
    println!("UART DATA:   offset={:#04X}, width={}", DATA.offset, DATA.width);
    println!("UART STATUS: offset={:#04X}, width={}", STATUS.offset, STATUS.width);
}

注意 const _: () = { ... }; 习语——一个未命名常量,唯一目的是运行编译时断言。如果任何断言失败,常量无法求值,编译停止。

小练习:SPI 寄存器银行

给定这些 SPI 控制器寄存器,添加 const fn 断言证明:

  1. 每个寄存器自然对齐(offset % width == 0)
  2. 没有两个寄存器重叠
  3. 所有寄存器适合 64 字节寄存器块
提示

重用 UART 示例中的 Register 和 disjoint 函数。定义三到四个 const Register 值(例如,CTRL 在偏移 0x00 宽度 4,STATUS 在 0x04 宽度 4,TX_DATA 在 0x08 宽度 1,RX_DATA 在 0x0C 宽度 1)并断言这三个属性。

协议帧布局

网络或总线协议帧在特定偏移处有字段。then() 方法使连续性结构化——间隙和重叠通过构造不可能:

#[derive(Debug, Clone, Copy)]
pub struct Field {
    pub offset: usize,
    pub size: usize,
}

impl Field {
    pub const fn new(offset: usize, size: usize) -> Self {
        assert!(size > 0, "字段大小必须非零");
        Self { offset, size }
    }

    pub const fn end(&self) -> usize {
        self.offset + self.size
    }

    /// 创建紧接在此字段之后的下一个字段。
    pub const fn then(&self, size: usize) -> Field {
        Field::new(self.end(), size)
    }
}

const MAX_FRAME: usize = 256;

const HEADER:  Field = Field::new(0, 4);
const SEQ_NUM: Field = HEADER.then(2);
const PAYLOAD: Field = SEQ_NUM.then(246);
const CRC:     Field = PAYLOAD.then(4);

// 编译时证明:帧适合最大大小
const _: () = assert!(CRC.end() <= MAX_FRAME, "帧超出最大大小");

fn main() {
    println!("Header:  [{}..{})", HEADER.offset, HEADER.end());
    println!("SeqNum:  [{}..{})", SEQ_NUM.offset, SEQ_NUM.end());
    println!("Payload: [{}..{})", PAYLOAD.offset, PAYLOAD.end());
    println!("CRC:     [{}..{})", CRC.offset, CRC.end());
    println!("Total:   {}/{} 字节", CRC.end(), MAX_FRAME);
}

字段通过构造是连续的——每个正好在前一个结束的地方开始。最终断言证明帧适合协议的最大大小。

用于泛型验证的内联 Const 块

从 Rust 1.79 开始,const { ... } 块让你在使用点验证 const 泛型参数——完美用于 DMA 缓冲区大小约束或对齐要求:

fn dma_transfer<const N: usize>(buf: &[u8; N]) {
    const { assert!(N % 4 == 0, "DMA 缓冲区大小必须是 4 字节对齐") };
    const { assert!(N <= 65536, "DMA 传输超出最大大小") };
    // ... 发起传输 ...
}

dma_transfer(&[0u8; 1024]);   // ✅ 1024 可被 4 整除且 ≤ 65536
// dma_transfer(&[0u8; 1023]); // ❌ 编译错误:不是 4 字节对齐

断言在函数单态化时求值——每个带有不同 N 的调用点都有自己的编译时检查。

寄存器内的位域布局

寄存器映射证明寄存器互不重叠——但单个寄存器内的位呢?控制寄存器将多个字段打包到一个字中。如果两个字段共享位位置,读写会静默相互损坏。在 C 中,这通常通过手动审查掩码常量来捕获(或不被捕获)。

const fn 可以证明每个字段的掩码/移位对与同一寄存器中的每个其他字段不相交:

#[derive(Debug, Clone, Copy)]
pub struct BitField {
    pub mask: u32,
    pub shift: u8,
}

impl BitField {
    pub const fn new(shift: u8, width: u8) -> Self {
        assert!(width > 0, "位域宽度必须非零");
        assert!(shift as u32 + width as u32 <= 32, "位域超出 32 位寄存器");
        // 构建掩码:从位 `shift` 开始的 `width` 个 1
        let mask = ((1u64 << width as u64) - 1) as u32;
        Self { mask: mask << shift as u32, shift }
    }

    pub const fn positioned_mask(&self) -> u32 {
        self.mask
    }

    pub const fn encode(&self, value: u32) -> u32 {
        assert!(value & !( self.mask >> self.shift as u32 ) == 0, "值超出字段宽度");
        value << self.shift as u32
    }
}

const fn fields_disjoint(a: &BitField, b: &BitField) -> bool {
    a.positioned_mask() & b.positioned_mask() == 0
}

// SPI 控制寄存器字段:enable[0], mode[1:2], clock_div[4:7], irq_en[8]
const SPI_EN:     BitField = BitField::new(0, 1);   // 位 0
const SPI_MODE:   BitField = BitField::new(1, 2);   // 位 1–2
const SPI_CLKDIV: BitField = BitField::new(4, 4);   // 位 4–7
const SPI_IRQ:    BitField = BitField::new(8, 1);   // 位 8

// 编译时证明:没有字段共享位位置
const _: () = {
    assert!(fields_disjoint(&SPI_EN,   &SPI_MODE));
    assert!(fields_disjoint(&SPI_EN,   &SPI_CLKDIV));
    assert!(fields_disjoint(&SPI_EN,   &SPI_IRQ));
    assert!(fields_disjoint(&SPI_MODE, &SPI_CLKDIV));
    assert!(fields_disjoint(&SPI_MODE, &SPI_IRQ));
    assert!(fields_disjoint(&SPI_CLKDIV, &SPI_IRQ));
};

fn main() {
    let ctrl = SPI_EN.encode(1)
             | SPI_MODE.encode(0b10)
             | SPI_CLKDIV.encode(0b0110)
             | SPI_IRQ.encode(1);
    println!("SPI_CTRL = {:#010b} ({:#06X})", ctrl, ctrl);
}

这补充了上面的寄存器映射模式——寄存器映射证明寄存器间的不相交性,而位域布局证明寄存器内的不相交性。一起它们提供从寄存器块到单个位的完整覆盖。

时钟树/PLL 配置

微控制器通过乘法器/除法器链派生外设时钟。PLL 产生 f_vco = f_in × N / M,VCO 频率必须保持在硬件指定的范围内。为特定板获取任何参数错误,芯片输出垃圾时钟或拒绝锁定。这些约束是 const fn 的完美选择:

#[derive(Debug, Clone, Copy)]
pub struct PllConfig {
    pub input_khz: u32,     // 外部振荡器
    pub m: u32,             // 输入除法器
    pub n: u32,             // VCO 乘法器
    pub p: u32,             // 系统时钟除法器
}

impl PllConfig {
    pub const fn verified(input_khz: u32, m: u32, n: u32, p: u32) -> Self {
        // 输入除法器产生 PLL 输入频率
        let pll_input = input_khz / m;
        assert!(pll_input >= 1_000 && pll_input <= 2_000,
            "PLL 输入必须是 1–2 MHz");

        // VCO 频率必须在硬件限制内
        let vco = pll_input as u64 * n as u64;
        assert!(vco >= 192_000 && vco <= 432_000,
            "VCO 必须是 192–432 MHz");

        // 系统时钟除法器必须是偶数(硬件约束)
        assert!(p == 2 || p == 4 || p == 6 || p == 8,
            "P 必须是 2、4、6 或 8");

        // 最终系统时钟
        let sysclk = vco / p as u64;
        assert!(sysclk <= 168_000,
            "系统时钟超出 168 MHz 最大值");

        Self { input_khz, m, n, p }
    }

    pub const fn vco_khz(&self) -> u32 {
        (self.input_khz / self.m) * self.n
    }

    pub const fn sysclk_khz(&self) -> u32 {
        self.vco_khz() / self.p
    }
}

// STM32F4 带有 8 MHz HSE 晶体 → 168 MHz 系统时钟
const PLL: PllConfig = PllConfig::verified(8_000, 8, 336, 2);

// ❌ 无法编译:VCO = 480 MHz 超出 432 MHz 限制
// const BAD: PllConfig = PllConfig::verified(8_000, 8, 480, 2);

fn main() {
    println!("VCO:    {} MHz", PLL.vco_khz() / 1_000);
    println!("SYSCLK: {} MHz", PLL.sysclk_khz() / 1_000);
}

取消注释 BAD 常量会产生编译时错误,指出违反的约束:

error[E0080]: evaluation of constant value failed
  --> src/main.rs:18:9
   |
18 |         assert!(vco >= 192_000 && vco <= 432_000,
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         the evaluated program panicked at 'VCO must be 192–432 MHz'

编译器在推导链的中间捕获约束违反——不是在末尾。如果你改为违反系统时钟限制(sysclk > 168 MHz),错误消息将指向该断言。

派生值约束链将单个 const fn 变成多阶段证明。 每个中间值有自己的硬件强制范围。更改一个参数(例如,换成 25 MHz 晶体)立即暴露任何下游违反。

编译时查找表

const fn 可以在编译时计算整个查找表,将它们放在 .rodata 中,零启动成本。这对于 CRC 表、三角函数、编码映射和纠错码特别有价值——任何你通常使用构建脚本或代码生成的地方:

const fn crc32_table() -> [u32; 256] {
    let mut table = [0u32; 256];
    let mut i: usize = 0;
    while i < 256 {
        let mut crc = i as u32;
        let mut j = 0;
        while j < 8 {
            if crc & 1 != 0 {
                crc = (crc >> 1) ^ 0xEDB8_8320; // 标准 CRC-32 多项式
            } else {
                crc >>= 1;
            }
            j += 1;
        }
        table[i] = crc;
        i += 1;
    }
    table
}

/// 完整 CRC-32 表——在编译时计算,放在 .rodata 中
const CRC32_TABLE: [u32; 256] = crc32_table();

/// 在运行时使用预计算表计算字节切片的 CRC-32。
fn crc32(data: &[u8]) -> u32 {
    let mut crc: u32 = !0;
    for &byte in data {
        let index = ((crc ^ byte as u32) & 0xFF) as usize;
        crc = (crc >> 8) ^ CRC32_TABLE[index];
    }
    !crc
}

// 烟雾测试:众所周知的 "123456789" 的 CRC-32
const _: () = {
    // 在编译时验证单个表条目
    assert!(CRC32_TABLE[0] == 0x0000_0000);
    assert!(CRC32_TABLE[1] == 0x7707_3096);
};

fn main() {
    let check = crc32(b"123456789");
    // "123456789" 的已知 CRC-32 是 0xCBF43926
    assert_eq!(check, 0xCBF4_3926);
    println!("CRC-32 of '123456789' = {:#010X} ✓", check);
    println!("表大小:{} entries × 4 bytes = {} bytes in .rodata",
        CRC32_TABLE.len(), CRC32_TABLE.len() * 4);
}

crc32_table() 函数完全在编译期间运行。生成的 1 KB 表被嵌入到二进制的只读数据段中——没有分配器,没有初始化代码,没有启动成本。与使用代码生成器或在启动时计算表的 C 方法对比。Rust 版本被证明是正确的(const _ 断言验证已知值)并且被证明是完整的(如果函数无法生成有效表,编译器将拒绝程序)。

何时使用 Const Fn 证明

场景推荐
内存映射、寄存器偏移、分区表✅ 总是
带有固定字段的协议帧布局✅ 总是
寄存器内的位域掩码✅ 总是
时钟树/PLL 参数链✅ 总是
查找表(CRC、三角、编码)✅ 总是——零启动成本
具有交叉值不变量的常量(无重叠、总和 ≤ 边界)✅ 总是
具有域约束的配置值✅ 当值在编译时已知
从用户输入或文件计算的值❌ 使用运行时验证
高度动态结构(树、图)❌ 使用基于属性的测试
单值范围检查⚠️ 考虑 newtype + From 替代(ch07)

成本总结

什么运行时成本
const fn 断言(assert!、panic!)仅编译时——0 条指令
const _: () = { ... } 验证块仅编译时——不在二进制文件中
Region、Register、Field 结构体纯数据——与原始整数相同的布局
内联 const { } 泛型验证在编译时单态化——0 成本
查找表(crc32_table())在编译时计算——放在 .rodata 中
Phantom 类型访问标记(TypedRegion<RW>)零大小——被优化掉

每一行都是零运行时成本——证明只存在于编译期间。生成的二进制文件只包含已验证的常量和查找表,没有断言检查代码。

练习:Flash 分区映射

为从 0x0800_0000 开始的 1 MB NOR flash 设计一个已验证的 flash 分区映射。要求:

  1. 四个分区:bootloader(64 KB)、application(640 KB)、config(64 KB)、OTA staging(256 KB)
  2. 每个分区必须4 KB 对齐(flash 擦除粒度):base 和 size 必须是 4096 的倍数
  3. 没有分区可以重叠另一个
  4. 所有分区必须适合 flash
  5. 添加一个 const fn total_used() 返回所有分区大小的总和,并断言它等于 1 MB
解决方案
#[derive(Debug, Clone, Copy)]
pub struct FlashRegion {
    pub base: u32,
    pub size: u32,
}

impl FlashRegion {
    pub const fn new(base: u32, size: u32) -> Self {
        assert!(size > 0, "分区大小必须非零");
        assert!(base % 4096 == 0, "分区 base 必须是 4 KB 对齐");
        assert!(size % 4096 == 0, "分区大小必须是 4 KB 对齐");
        assert!(
            base as u64 + size as u64 <= u32::MAX as u64,
            "分区超出地址空间"
        );
        Self { base, size }
    }

    pub const fn end(&self) -> u32 { self.base + self.size }

    pub const fn contains(&self, inner: &FlashRegion) -> bool {
        inner.base >= self.base && inner.end() <= self.end()
    }

    pub const fn overlaps(&self, other: &FlashRegion) -> bool {
        self.base < other.end() && other.base < self.end()
    }
}

pub struct FlashMap {
    pub total:  FlashRegion,
    pub boot:   FlashRegion,
    pub app:    FlashRegion,
    pub config: FlashRegion,
    pub ota:    FlashRegion,
}

impl FlashMap {
    pub const fn verified(
        total: FlashRegion,
        boot: FlashRegion,
        app: FlashRegion,
        config: FlashRegion,
        ota: FlashRegion,
    ) -> Self {
        assert!(total.contains(&boot),   "bootloader 超出 flash");
        assert!(total.contains(&app),    "application 超出 flash");
        assert!(total.contains(&config), "config 超出 flash");
        assert!(total.contains(&ota),    "OTA staging 超出 flash");

        assert!(!boot.overlaps(&app),    "boot/app 重叠");
        assert!(!boot.overlaps(&config), "boot/config 重叠");
        assert!(!boot.overlaps(&ota),    "boot/ota 重叠");
        assert!(!app.overlaps(&config),  "app/config 重叠");
        assert!(!app.overlaps(&ota),     "app/ota 重叠");
        assert!(!config.overlaps(&ota),  "config/ota 重叠");

        Self { total, boot, app, config, ota }
    }

    pub const fn total_used(&self) -> u32 {
        self.boot.size + self.app.size + self.config.size + self.ota.size
    }
}

const FLASH: FlashMap = FlashMap::verified(
    FlashRegion::new(0x0800_0000, 1024 * 1024),  // 1 MB 总计
    FlashRegion::new(0x0800_0000,   64 * 1024),   // bootloader: 64 KB
    FlashRegion::new(0x0801_0000,  640 * 1024),   // application: 640 KB
    FlashRegion::new(0x080B_0000,   64 * 1024),   // config: 64 KB
    FlashRegion::new(0x080C_0000,  256 * 1024),   // OTA staging: 256 KB
);

// flash 的每个字节都有交代
const _: () = assert!(
    FLASH.total_used() == 1024 * 1024,
    "分区必须正好填满 flash"
);

fn main() {
    println!("Flash map: {} KB used / {} KB total",
        FLASH.total_used() / 1024,
        FLASH.total.size / 1024);
}
flowchart LR
    subgraph compile["编译时——零运行时成本"]
        direction TB
        RGN["Region::new()<br/>✅ size &gt; 0<br/>✅ 无溢出"]
        MAP["SramMap::verified()<br/>✅ 包含<br/>✅ 无重叠"]
        ACC["TypedRegion&lt;RW&gt;<br/>✅ 访问控制"]
        PROV["VerifiedAddr::new()<br/>✅ 溯源"]
    end

    subgraph runtime["运行时"]
        HW["硬件访问<br/>无边界检查<br/>无权限检查"]
    end

    RGN --> MAP --> ACC --> PROV --> HW

    style RGN fill:#c8e6c9,color:#000
    style MAP fill:#c8e6c9,color:#000
    style ACC fill:#e1f5fe,color:#000
    style PROV fill:#e1f5fe,color:#000
    style HW fill:#fff3e0,color:#000

关键要点

  1. const fn + assert! = 编译时证明义务 —— 如果断言在 const 求值期间失败,程序无法编译。不需要测试,不需要代码审查捕获——编译器证明它。

  2. 内存映射是理想的候选 —— 子区域包含、无重叠、总大小边界和对齐约束都可以表示为 const fn 断言。C 的 #define 方法不提供这些保证。

  3. Phantom 类型分层在上 —— 结合 const fn(值验证)与 phantom 类型访问标记(权限验证),零运行时成本进行纵深防御。

  4. 溯源可以在编译时建立 —— VerifiedAddr 在编译时证明地址属于特定区域,消除每次访问的运行时边界检查。

  5. 模式泛化到内存之外 —— 寄存器映射、位域掩码、协议帧、时钟树、DMA 参数——任何你有编译时已知值和结构不变量的地方。

  6. 位域和时钟树是理想的候选 —— 寄存器内位的不相交性和派生值约束链(VCO 范围、除法器限制)正是 const fn 轻松证明的那种不变量。

  7. const fn 取代代码生成器和构建脚本用于查找表 —— CRC 表、三角函数、编码映射——在编译时计算,放在 .rodata 中,零启动成本,没有外部工具。

  8. 内联 const { } 块验证泛型参数 —— 从 Rust 1.79 开始,你可以在调用点强制执行 const 泛型参数的约束,在任何代码运行之前捕获误用。

Send & Sync — 编译期并发证明 🟠

你将学到: Rust 的 Send 和 Sync auto-traits 如何将编译器变成并发审计器——在编译期证明哪些类型可以跨越线程边界、哪些可以共享,零运行时开销。

交叉引用: ch04(capability 令牌)、ch09(phantom 类型)、ch15(const fn 证明)

问题:没有安全网的并发访问

在系统编程中,外设、共享缓冲区和全局状态会从多个上下文中访问——主循环、中断处理程序、DMA 回调和工作线程。在 C 语言中,编译器完全不提供任何强制执行:

/* 共享传感器缓冲区——从主循环和 ISR 访问 */
volatile uint32_t sensor_buf[64];
volatile uint32_t buf_index = 0;

void SENSOR_IRQHandler(void) {
    sensor_buf[buf_index++] = read_sensor();  /* 竞争:buf_index 读 + 写 */
}

void process_sensors(void) {
    for (uint32_t i = 0; i < buf_index; i++) {  /* buf_index 在循环中间改变 */
        process(sensor_buf[i]);                   /* 读取中途数据被覆盖 */
    }
    buf_index = 0;                                /* ISR 在这两行之间触发 */
}

volatile 关键字防止编译器优化掉读取操作,但它对数据竞争毫无作用。两个上下文可以同时读写 buf_index,产生撕裂值(torn values)、丢失更新或缓冲区溢出。同样的问题出现在 pthread_mutex_t 上——编译器会欣然让你忘记加锁:

pthread_mutex_t lock;
int shared_counter;

void increment(void) {
    shared_counter++;  /* 哎呀——忘了 pthread_mutex_lock(&lock) */
}

每个并发 bug 都在运行时才发现——通常是在生产环境下高负载时,且间歇性出现。

Send 和 Sync 证明的内容

Rust 定义了两个编译器自动派生的 marker traits:

Trait证明非正式含义
SendT 类型的值可以安全地移动到另一个线程“这可以跨越线程边界”
Sync共享引用 &T 可以安全地被多个线程使用“这可以从多个线程读取”

这些是auto-traits——编译器通过检查每个字段自动派生。一个结构体是 Send 当且仅当它的所有字段都是 Send。一个结构体是 Sync 当且仅当它的所有字段都是 Sync。如果任何字段选择退出,整个结构体就退出。不需要标注,没有运行时开销——证明是结构性的。

flowchart TD
    STRUCT["Your struct"]
    INSPECT["Compiler inspects<br/>every field"]
    ALL_SEND{"All fields<br/>Send?"}
    ALL_SYNC{"All fields<br/>Sync?"}
    SEND_YES["Send ✅<br/><i>can cross thread boundaries</i>"]
    SEND_NO["!Send ❌<br/><i>confined to one thread</i>"]
    SYNC_YES["Sync ✅<br/><i>shareable across threads</i>"]
    SYNC_NO["!Sync ❌<br/><i>no concurrent references</i>"]

    STRUCT --> INSPECT
    INSPECT --> ALL_SEND
    INSPECT --> ALL_SYNC
    ALL_SEND -->|Yes| SEND_YES
    ALL_SEND -->|"Any field !Send<br/>(e.g., Rc, *const T)"| SEND_NO
    ALL_SYNC -->|Yes| SYNC_YES
    ALL_SYNC -->|"Any field !Sync<br/>(e.g., Cell, RefCell)"| SYNC_NO

    style SEND_YES fill:#c8e6c9,color:#000
    style SYNC_YES fill:#c8e6c9,color:#000
    style SEND_NO fill:#ffcdd2,color:#000
    style SYNC_NO fill:#ffcdd2,color:#000

编译器就是审计器。 在 C 语言中,线程安全标注存在于注释和头文件文档中——仅供参考,从不强制执行。在 Rust 中,Send 和 Sync 从类型本身的结构派生。添加一个 Cell<f32> 字段会自动使包含的结构体变成 !Sync。不需要程序员采取行动,也无法忘记。

这两个 trait 通过一个关键恒等式联系:

T 是 Sync 当且仅当 &T 是 Send。

这很直观:如果共享引用可以安全地发送到另一个线程,那么底层类型就安全支持并发读取。

选择退出的类型

某些类型故意是 !Send 或 !Sync:

类型SendSync原因
u32、String、Vec<T>✅✅无内部可变性,无裸指针
Cell<T>、RefCell<T>✅❌无同步的内部可变性
Rc<T>❌❌引用计数不是原子的
*const T、*mut T❌❌裸指针没有安全保证
Arc<T>(其中 T: Send + Sync)✅✅原子引用计数
Mutex<T>(其中 T: Send)✅✅锁序列化所有访问

此表中的每个 ❌ 都是编译期不变量。你不能意外地将 Rc 发送到另一个线程——编译器会拒绝它。

!Send 外设句柄

在嵌入式系统中,外设寄存器块位于固定的内存地址,应该只从单个执行上下文访问。裸指针天生就是 !Send 和 !Sync,所以包装一个会自动使包含的类型退出这两个 trait:

/// 内存映射 UART 外设的句柄。
/// 裸指针使其自动成为 !Send 和 !Sync。
pub struct Uart {
    regs: *const u32,
}

impl Uart {
    pub fn new(base: usize) -> Self {
        Self { regs: base as *const u32 }
    }

    pub fn write_byte(&self, byte: u8) {
        // 在真实固件中:unsafe { write_volatile(self.regs.add(DATA_OFFSET), byte as u32) }
        println!("UART TX: {:#04X}", byte);
    }
}

fn main() {
    let uart = Uart::new(0x4000_1000);
    uart.write_byte(b'A');  // ✅ 在创建的线程上使用

    // ❌ 无法编译:Uart 是 !Send
    // std::thread::spawn(move || {
    //     uart.write_byte(b'B');
    // });
}

注释掉的 thread::spawn 会产生:

error[E0277]: `*const u32` cannot be sent between threads safely
   |
   |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^ within `Uart`, the trait `Send` is not
   |                        implemented for `*const u32`

没有裸指针?使用 PhantomData。 有时一个类型没有裸指针,但仍应限制在一个线程中——例如,从 C 库获得的文件描述符索引或句柄:

use std::marker::PhantomData;

/// 来自 C 库的不透明句柄。PhantomData<*const ()> 使其
/// 成为 !Send + !Sync,即使内部的 fd 只是普通整数。
pub struct LibHandle {
    fd: i32,
    _not_send: PhantomData<*const ()>,
}

impl LibHandle {
    pub fn open(path: &str) -> Self {
        let _ = path;
        Self { fd: 42, _not_send: PhantomData }
    }

    pub fn fd(&self) -> i32 { self.fd }
}

fn main() {
    let handle = LibHandle::open("/dev/sensor0");
    println!("fd = {}", handle.fd());

    // ❌ 无法编译:LibHandle 是 !Send
    // std::thread::spawn(move || { let _ = handle.fd(); });
}

这相当于 C 语言的“请阅读文档说明这个句柄不是线程安全的“的编译期版本。在 Rust 中,编译器强制执行它。

Mutex 将 !Sync 转换为 Sync

Cell<T> 和 RefCell<T> 提供无需任何同步的内部可变性——所以它们是 !Sync。但有时你确实需要在线程间共享可变状态。Mutex<T> 添加了缺失的同步,编译器会识别这一点:

如果 T: Send,那么 Mutex<T>: Send + Sync。

锁序列化所有访问,所以 !Sync 的内部类型变成可安全共享。编译器在结构上证明这一点——没有运行时检查“程序员是否忘记加锁“:

use std::sync::{Arc, Mutex};
use std::cell::Cell;

/// 使用 Cell 的内部可变性的传感器缓存。
/// Cell<u32> 是 !Sync——不能直接跨线程共享。
struct SensorCache {
    last_reading: Cell<u32>,
    reading_count: Cell<u32>,
}

fn main() {
    // Mutex 使 SensorCache 安全共享——编译器证明
    let cache = Arc::new(Mutex::new(SensorCache {
        last_reading: Cell::new(0),
        reading_count: Cell::new(0),
    }));

    let handles: Vec<_> = (0..4).map(|i| {
        let c = Arc::clone(&cache);
        std::thread::spawn(move || {
            let guard = c.lock().unwrap();  // 访问前必须加锁
            guard.last_reading.set(i * 10);
            guard.reading_count.set(guard.reading_count.get() + 1);
        })
    }).collect();

    for h in handles { h.join().unwrap(); }

    let guard = cache.lock().unwrap();
    println!("Last reading: {}", guard.last_reading.get());
    println!("Total reads:  {}", guard.reading_count.get());
}

与 C 版本对比:pthread_mutex_lock 是程序员可能忘记的运行时调用。在这里,类型系统使得不通过 Mutex 访问 SensorCache 在结构上无法表示。证明是结构性的——唯一的运行时成本是锁本身。

Mutex 不仅同步——它证明同步。 Mutex::lock() 返回一个 MutexGuard,它 Deref 为 &T。没有办法不通过锁就获得内部数据的引用。API 使得“忘记加锁“在结构上无法表示。

函数约束即定理

std::thread::spawn 有这个签名:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,

Send + 'static 约束不仅是实现细节——它是一个定理:

“任何传递给 spawn 的闭包和返回值都在编译期被证明安全地在另一个线程上运行,没有悬空引用。”

你可以将相同的模式应用到你自己的 API:

use std::sync::mpsc;

/// 在后台线程上运行任务并返回其结果。
/// 约束证明:闭包及其结果是线程安全的。
fn run_on_background<F, T>(task: F) -> T
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,
{
    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(task());
    });
    rx.recv().expect("background task panicked")
}

fn main() {
    // ✅ u32 是 Send,闭包没有捕获非 Send 的内容
    let result = run_on_background(|| 6 * 7);
    println!("Result: {result}");

    // ✅ String 是 Send
    let greeting = run_on_background(|| String::from("hello from background"));
    println!("{greeting}");

    // ❌ 无法编译:Rc 是 !Send
    // use std::rc::Rc;
    // let data = Rc::new(42);
    // run_on_background(move || *data);
}

取消注释 Rc 示例会产生精确的诊断:

error[E0277]: `Rc<i32>` cannot be sent between threads safely
   --> src/main.rs
    |
    |     run_on_background(move || *data);
    |     ^^^^^^^^^^^^^^^^^^ `Rc<i32>` cannot be sent between threads safely
    |
note: required by a bound in `run_on_background`
    |
    |     F: FnOnce() -> T + Send + 'static,
    |                        ^^^^ required by this bound

编译器追踪违反回到精确的约束——并告诉程序员为什么。与 C 的 pthread_create 对比:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);

void *arg 接受任何内容——线程安全与否。C 编译器无法区分非原子引用计数和普通整数。Rust 的 trait 约束在类型级别进行区分。

何时使用 Send/Sync 证明

场景方法
包装裸指针的外设句柄自动 !Send + !Sync——无需操作
C 库的句柄(整数 fd/handle)添加 PhantomData<*const ()> 实现 !Send + !Sync
锁后面的共享配置Arc<Mutex<T>>——编译器证明访问安全
跨线程消息传递mpsc::channel——Send 约束自动强制执行
任务生成器或线程池 API在签名中要求 F: Send + 'static
单线程资源(如 GPU 上下文)PhantomData<*const ()> 防止共享
类型应该是 Send 但包含裸指针unsafe impl Send 带文档化的安全性证明

成本摘要

内容运行时成本
Send / Sync 自动派生仅编译期——0 字节
PhantomData<*const ()> 字段零大小——优化掉
!Send / !Sync 强制执行仅编译期——无运行时检查
F: Send + 'static 函数约束单态化——静态分派,无装箱
Mutex<T> 锁运行时锁(共享可变性不可避免)
Arc<T> 引用计数原子递增/递减(共享所有权不可避免)

前四行是零成本——它们只存在于类型系统中,编译后消失。Mutex 和 Arc 带有不可避免的运行时成本,但这些成本是任何正确的并发程序必须支付的最低成本——Rust 只是确保你支付它们。

练习:DMA 传输守卫

设计一个 DmaTransfer<T>,在 DMA 传输进行时持有缓冲区。要求:

  1. DmaTransfer 必须是 !Send——DMA 控制器使用绑定到这个内核内存总线的物理地址
  2. DmaTransfer 必须是 !Sync——DMA 写入时并发读取会看到撕裂数据
  3. 提供一个 wait() 方法消费守卫并返回缓冲区——所有权证明传输完成
  4. 缓冲区类型 T 必须实现 DmaSafe marker trait
解答
use std::marker::PhantomData;

/// 可用作 DMA 缓冲区的类型的 marker trait。
/// 在真实固件中:类型必须是 repr(C) 且无填充。
trait DmaSafe {}

impl DmaSafe for [u8; 64] {}
impl DmaSafe for [u8; 256] {}

/// 表示进行中的 DMA 传输的守卫。
/// !Send + !Sync:不能发送到另一个线程或共享。
pub struct DmaTransfer<T: DmaSafe> {
    buffer: T,
    channel: u8,
    _no_send_sync: PhantomData<*const ()>,
}

impl<T: DmaSafe> DmaTransfer<T> {
    /// 开始 DMA 传输。缓冲区被消费——其他人无法触碰它。
    pub fn start(buffer: T, channel: u8) -> Self {
        // 在真实固件中:配置 DMA 通道,设置源/目标,开始传输
        println!("DMA channel {} started", channel);
        Self {
            buffer,
            channel,
            _no_send_sync: PhantomData,
        }
    }

    /// 等待传输完成并返回缓冲区。
    /// 消费 self——守卫在此之后不再存在。
    pub fn wait(self) -> T {
        // 在真实固件中:轮询 DMA 状态寄存器直到完成
        println!("DMA channel {} complete", self.channel);
        self.buffer
    }
}

fn main() {
    let buf = [0u8; 64];

    // 开始传输——buf 被移入守卫
    let transfer = DmaTransfer::start(buf, 2);

    // ❌ buf 不再可访问——所有权防止 DMA 期间使用
    // println!("{:?}", buf);

    // ❌ 无法编译:DmaTransfer 是 !Send
    // std::thread::spawn(move || { transfer.wait(); });

    // ✅ 在原始线程上等待,取回缓冲区
    let buf = transfer.wait();
    println!("Buffer recovered: {} bytes", buf.len());
}
flowchart TB
    subgraph compiler["Compile Time — Auto-Derived Proofs"]
        direction TB
        SEND["Send<br/>✅ safe to move across threads"]
        SYNC["Sync<br/>✅ safe to share references"]
        NOTSEND["!Send<br/>❌ confined to one thread"]
        NOTSYNC["!Sync<br/>❌ no concurrent sharing"]
    end

    subgraph types["Type Taxonomy"]
        direction TB
        PLAIN["Primitives, String, Vec<br/>Send + Sync"]
        CELL["Cell, RefCell<br/>Send + !Sync"]
        RC["Rc, raw pointers<br/>!Send + !Sync"]
        MUTEX["Mutex&lt;T&gt;<br/>restores Sync"]
        ARC["Arc&lt;T&gt;<br/>shared ownership + Send"]
    end

    subgraph runtime["Runtime"]
        SAFE["Thread-safe access<br/>No data races<br/>No forgotten locks"]
    end

    SEND --> PLAIN
    NOTSYNC --> CELL
    NOTSEND --> RC
    CELL --> MUTEX --> SAFE
    RC --> ARC --> SAFE
    PLAIN --> SAFE

    style SEND fill:#c8e6c9,color:#000
    style SYNC fill:#c8e6c9,color:#000
    style NOTSEND fill:#ffcdd2,color:#000
    style NOTSYNC fill:#ffcdd2,color:#000
    style PLAIN fill:#c8e6c9,color:#000
    style CELL fill:#fff3e0,color:#000
    style RC fill:#ffcdd2,color:#000
    style MUTEX fill:#e1f5fe,color:#000
    style ARC fill:#e1f5fe,color:#000
    style SAFE fill:#c8e6c9,color:#000

关键要点

  1. Send 和 Sync 是关于并发安全性的编译期证明——编译器通过检查每个字段在结构上派生它们。无需标注,无运行时成本,无需显式选择。

  2. 裸指针自动选择退出——任何包含 *const T 或 *mut T 的类型变成 !Send + !Sync。这使得外设句柄自然地限制在线程内。

  3. PhantomData<*const ()> 是显式选择退出——当一个类型没有裸指针但仍应限制在线程内时(C 库句柄、GPU 上下文),phantom 字段可以完成这项工作。

  4. Mutex<T> 恢复 Sync 并带证明——编译器在结构上证明所有访问都通过锁。与 C 的 pthread_mutex_t 不同,你不可能忘记加锁。

  5. 函数约束是定理——生成器签名中的 F: Send + 'static 是编译期证明义务:每个调用点必须证明其闭包是线程安全的。与 C 的 void *arg 对比,它接受任何内容。

  6. 该模式补充所有其他正确性技术——typestate 证明协议排序,phantom 类型证明权限,const fn 证明值不变量,Send/Sync 证明并发安全性。它们一起覆盖完整的正确性表面。

整合一切——完整诊断平台 🟡

你将学到: 七个核心模式(ch02–ch09)如何组合成单个工作诊断流程——认证、会话、类型化命令、审计令牌、量纲结果、验证数据和 phantom 类型寄存器——零总运行时开销。

交叉引用: 每个核心模式章节(ch02–ch09)、ch14(测试这些保证)

目标

本章将第 2-9 章的七个模式组合成一个单一的、现实的诊断工作流。我们将构建一个服务器健康检查,它:

  1. 认证(capability 令牌——ch04)
  2. 打开 IPMI 会话(type-state——ch05)
  3. 发送类型化命令(类型化命令——ch02)
  4. 使用一次性令牌进行审计日志(一次性类型——ch03)
  5. 返回量纲结果(量纲分析——ch06)
  6. 验证 FRU 数据(验证边界——ch07)
  7. 读取类型化寄存器(phantom 类型——ch09)
use std::marker::PhantomData;
use std::io;
// ──── 模式 1:量纲类型(ch06)────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

// ──── 模式 2:类型化命令(ch02)────

/// 与 ch02 相同的 trait 形状,使用(而非关联常量)
/// 以保持一致性。关联常量(`const NETFN: u8`)是另一种
/// 当值真正在每个类型上固定时同样有效的替代方案。
pub trait IpmiCmd {
    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>;
}

pub struct ReadTemp { pub sensor_id: u8 }
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))
    }
}

pub struct ReadFanSpeed { pub fan_id: u8 }
impl IpmiCmd for ReadFanSpeed {
    type Response = Rpm;
    fn net_fn(&self) -> u8 { 0x04 }
    fn cmd_byte(&self) -> u8 { 0x2D }
    fn payload(&self) -> Vec<u8> { vec![self.fan_id] }
    fn parse_response(&self, raw: &[u8]) -> io::Result<Rpm> {
        if raw.len() < 2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "need 2 bytes"));
        }
        Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as f64))
    }
}

// ──── 模式 3:Capability 令牌(ch04)────

pub struct AdminToken { _private: () }

pub fn authenticate(user: &str, pass: &str) -> Result<AdminToken, &'static str> {
    if user == "admin" && pass == "secret" {
        Ok(AdminToken { _private: () })
    } else {
        Err("authentication failed")
    }
}

// ──── 模式 4:Type-State 会话(ch05)────

pub struct Idle;
pub struct Active;

pub struct Session<State> {
    host: String,
    _state: PhantomData<State>,
}

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

    pub fn activate(
        self,
        _admin: &AdminToken,  // ← 需要 capability 令牌
    ) -> Result<Session<Active>, String> {
        println!("会话已激活在 {}", self.host);
        Ok(Session { host: self.host, _state: PhantomData })
    }
}

impl Session<Active> {
    /// 执行类型化命令——仅在 Active 会话上可用。
    /// 返回 io::Result 以传播传输错误(与 ch02 一致)。
    pub fn execute<C: IpmiCmd>(&mut self, cmd: &C) -> io::Result<C::Response> {
        let raw_response = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?;
        cmd.parse_response(&raw_response)
    }

    fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result<Vec<u8>> {
        Ok(vec![42, 0x1E]) // 存根:原始 IPMI 响应
    }

    pub fn close(self) { println!("会话已关闭"); }
}

// ──── 模式 5:一次性审计令牌(ch03)────

/// 每次诊断运行获得唯一的审计令牌。
/// 不是 Clone,不是 Copy——确保每个审计条目是唯一的。
pub struct AuditToken {
    run_id: u64,
}

impl AuditToken {
    pub fn issue(run_id: u64) -> Self {
        AuditToken { run_id }
    }

    /// 消费令牌以写入审计日志条目。
    pub fn log(self, message: &str) {
        println!("[审计 run_id={}] {}", self.run_id, message);
        // 令牌被消费——不能用相同的 run_id 记录两次
    }
}

// ──── 模式 6:验证边界(ch07)────
// 简化自 ch07 的完整 ValidFru——仅为本章
// 复合示例所需的字段。参见 ch07 获取完整的 TryFrom<RawFruData> 版本。

pub struct ValidFru {
    pub board_serial: String,
    pub product_name: String,
}

impl ValidFru {
    pub fn parse(raw: &[u8]) -> Result<Self, &'static str> {
        if raw.len() < 8 { return Err("FRU 太短"); }
        if raw[0] != 0x01 { return Err("错误的 FRU 版本"); }
        Ok(ValidFru {
            board_serial: "SN12345".to_string(),  // 存根
            product_name: "ServerX".to_string(),
        })
    }
}

// ──── 模式 7:Phantom 类型寄存器(ch09)────

pub struct Width16;
pub struct Reg<W> { offset: u16, _w: PhantomData<W> }

impl Reg<Width16> {
    pub fn read(&self) -> u16 { 0x8086 } // 存根
}

pub struct PcieDev {
    pub vendor_id: Reg<Width16>,
    pub device_id: Reg<Width16>,
}

impl PcieDev {
    pub fn new() -> Self {
        PcieDev {
            vendor_id: Reg { offset: 0x00, _w: PhantomData },
            device_id: Reg { offset: 0x02, _w: PhantomData },
        }
    }
}

// ──── 复合工作流────

fn full_diagnostic() -> Result<(), String> {
    // 1. 认证 → 获取 capability 令牌
    let admin = authenticate("admin", "secret")
        .map_err(|e| e.to_string())?;

    // 2. 连接并激活会话(type-state:Idle → Active)
    let session = Session::connect("192.168.1.100");
    let mut session = session.activate(&admin)?;  // 需要 AdminToken

    // 3. 发送类型化命令(响应类型匹配命令)
    let temp: Celsius = session.execute(&ReadTemp { sensor_id: 0 })
        .map_err(|e| e.to_string())?;
    let fan: Rpm = session.execute(&ReadFanSpeed { fan_id: 1 })
        .map_err(|e| e.to_string())?;

    // 类型不匹配将被捕获:
    // let wrong: Volts = session.execute(&ReadTemp { sensor_id: 0 })?;
    //  ❌ 错误:期望 Celsius,找到 Volts

    // 4. 读取 phantom 类型 PCIe 寄存器
    let pcie = PcieDev::new();
    let vid: u16 = pcie.vendor_id.read();  // 保证 u16

    // 5. 在边界验证 FRU 数据
    let raw_fru = vec![0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xFD];
    let fru = ValidFru::parse(&raw_fru)
        .map_err(|e| e.to_string())?;

    // 6. 发放一次性审计令牌
    let audit = AuditToken::issue(1001);

    // 7. 生成报告(所有数据都是类型化和验证的)
    let report = format!(
        "服务器:{} (SN: {}), VID: 0x{:04X}, CPU: {:?}, 风扇:{:?}",
        fru.product_name, fru.board_serial, vid, temp, fan,
    );

    // 8. 消费审计令牌——不能记录两次
    audit.log(&report);
    // audit.log("oops");  // ❌ 使用已移动的值

    // 9. 关闭会话(type-state:Active → drop)
    session.close();

    Ok(())
}

编译器证明的内容

Bug 类别如何防止模式
未认证访问activate() 需要 &AdminTokenCapability 令牌
错误会话状态下的命令execute() 仅存在于 Session<Active> 上Type-state
错误的响应类型ReadTemp::Response = Celsius,由 trait 固定类型化命令
单位混淆(°C vs RPM)Celsius ≠ Rpm ≠ Volts量纲类型
寄存器宽度不匹配Reg<Width16> 返回 u16Phantom 类型
处理未验证数据必须先调用 ValidFru::parse()验证边界
重复审计条目AuditToken 在 log 时被消费一次性类型
电源时序顺序错误每一步需要前一个令牌Capability 令牌(ch04)

所有这些保证的总运行时开销:零。

每个检查都在编译时完成。生成的汇编代码与没有任何检查的手写 C 代码相同——但C 可能有 bug,这个不可能。

关键要点

  1. 七个模式无缝组合 —— capability 令牌、type-state、类型化命令、一次性类型、量纲类型、验证边界和 phantom 类型都一起工作。
  2. 编译器证明八个 bug 类别不可能 —— 参见上面的“编译器证明的内容“表格。
  3. 零总运行时开销 —— 生成的汇编代码与无检查的 C 代码相同。
  4. 每个模式独立有用 —— 你不需要全部七个;增量采用它们。
  5. 集成章节是设计模板 —— 用它作为你自己类型化诊断工作流的起点。
  6. 从 IPMI 到大规模 Redfish —— ch17 和 ch18 将这相同的七个模式(加上 ch08 的 capability 混合)应用于完整的 Redfish 客户端和服务器。IPMI 工作流这里是基础;Redfish 演练展示了组合如何扩展到具有多个数据源和 schema 版本约束的生产系统。

实战演练 — 类型安全的 Redfish 客户端 🟡

你将学到: 如何组合 type-state 会话、capability 令牌、phantom 类型资源导航、量纲分析、验证边界、构建器 type-state 和一次性类型成一个完整的、零开销的 Redfish 客户端——其中每个协议违规都是编译错误。

交叉引用: ch02(类型化命令)、ch03(一次性类型)、ch04(capability 令牌)、ch05(type-state)、ch06(量纲类型)、ch07(验证边界)、ch09(phantom 类型)、ch10(IPMI 集成)、ch11(技巧 4 — 构建器 type-state)

为什么 Redfish 值得单独一章

第 10 章围绕 IPMI 组合核心模式——一个字节级协议。但大多数 BMC 平台现在除了(或替代)IPMI 外还暴露Redfish REST API,而 Redfish 引入了它自己类别的正确性危险:

危险示例后果
格式错误的 URIGET /redfish/v1/Chassis/1/Processors(错误的父节点)返回 404 或错误数据静默返回
错误电源状态下的操作在已关闭的系统上调用 Reset(ForceOff)BMC 返回错误,或更糟,与另一个操作竞争
缺少特权操作员级代码调用 Manager.ResetToDefaults生产环境中返回 403,安全审计发现
不完整的 PATCH从 PATCH 体中遗漏必需的 BIOS 属性静默无操作或部分配置损坏
未验证的固件应用在检查图像完整性之前调用 SimpleUpdateBMC 变砖
Schema 版本不匹配在 v1.5 BMC 上访问 LastResetTime(v1.13 添加)字段为 null → 运行时 panic
遥测单位混淆比较入口温度 (°C) 与功耗 (W)无意义的阈值决策

在 C、Python 或无类型的 Rust 中,每一个都仅通过纪律和测试来防止。本章使它们成为编译错误。

无类型的 Redfish 客户端

一个典型的 Redfish 客户端看起来像这样:

use std::collections::HashMap;

struct RedfishClient {
    base_url: String,
    token: Option<String>,
}

impl RedfishClient {
    fn get(&self, path: &str) -> Result<serde_json::Value, String> {
        // ... HTTP GET ...
        Ok(serde_json::json!({})) // 存根
    }

    fn patch(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
        // ... HTTP PATCH ...
        Ok(()) // 存根
    }

    fn post_action(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
        // ... HTTP POST ...
        Ok(()) // 存根
    }
}

fn check_thermal(client: &RedfishClient) -> Result<(), String> {
    let resp = client.get("/redfish/v1/Chassis/1/Thermal")?;

    // 🐛 这个字段总是存在吗?如果 BMC 返回 null 怎么办?
    let cpu_temp = resp["Temperatures"][0]["ReadingCelsius"]
        .as_f64().unwrap();

    let fan_rpm = resp["Fans"][0]["Reading"]
        .as_f64().unwrap();

    // 🐛 比较 °C 与 RPM——都是 f64
    if cpu_temp > fan_rpm {
        println!("thermal issue");
    }

    // 🐛 这是正确的路径吗?没有编译期检查。
    client.post_action(
        "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
        &serde_json::json!({"ResetType": "ForceOff"})
    )?;

    Ok(())
}

这“能工作“——直到它不能。每个 unwrap() 都是潜在的 panic,每个字符串路径都是未经检查的假设,单位混淆是看不见的。


第 1 节 — 会话生命周期(Type-State,ch05)

Redfish 会话有严格的生命周期:connect → authenticate → use → close。将每个状态编码为不同的类型。

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connected : connect(host)
    Connected --> Authenticated : login(user, pass)
    Authenticated --> Authenticated : get() / patch() / post_action()
    Authenticated --> Closed : logout()
    Closed --> [*]

    note right of Authenticated : API 调用仅在此存在
    note right of Connected : get() → 编译错误
use std::marker::PhantomData;

// ──── 会话状态 ────

pub struct Disconnected;
pub struct Connected;
pub struct Authenticated;

pub struct RedfishSession<S> {
    base_url: String,
    auth_token: Option<String>,
    _state: PhantomData<S>,
}

impl RedfishSession<Disconnected> {
    pub fn new(host: &str) -> Self {
        RedfishSession {
            base_url: format!("https://{}", host),
            auth_token: None,
            _state: PhantomData,
        }
    }

    /// 转换:Disconnected → Connected。
    /// 验证服务根可达。
    pub fn connect(self) -> Result<RedfishSession<Connected>, RedfishError> {
        // GET /redfish/v1 — 验证服务根
        println!("Connecting to {}/redfish/v1", self.base_url);
        Ok(RedfishSession {
            base_url: self.base_url,
            auth_token: None,
            _state: PhantomData,
        })
    }
}

impl RedfishSession<Connected> {
    /// 转换:Connected → Authenticated。
    /// 通过 POST /redfish/v1/SessionService/Sessions 创建会话。
    pub fn login(
        self,
        user: &str,
        _pass: &str,
    ) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
        // POST /redfish/v1/SessionService/Sessions
        println!("Authenticated as {}", user);
        let token = "X-Auth-Token-abc123".to_string();
        Ok((
            RedfishSession {
                base_url: self.base_url,
                auth_token: Some(token),
                _state: PhantomData,
            },
            LoginToken { _private: () },
        ))
    }
}

impl RedfishSession<Authenticated> {
    /// 仅在 Authenticated 会话上可用。
    fn http_get(&self, path: &str) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        // ... HTTP GET with auth_token header ...
        Ok(serde_json::json!({})) // 存根
    }

    fn http_patch(
        &self,
        path: &str,
        body: &serde_json::Value,
    ) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        let _ = body;
        Ok(serde_json::json!({})) // 存根
    }

    fn http_post(
        &self,
        path: &str,
        body: &serde_json::Value,
    ) -> Result<serde_json::Value, RedfishError> {
        let _url = format!("{}{}", self.base_url, path);
        let _ = body;
        Ok(serde_json::json!({})) // 存根
    }

    /// 转换:Authenticated → Closed(会话被消费)。
    pub fn logout(self) {
        // DELETE /redfish/v1/SessionService/Sessions/{id}
        println!("Session closed");
        // self 被消费——登录后不能使用会话
    }
}

// 尝试在非 Authenticated 会话上调用 http_get:
//
//   let session = RedfishSession::new("bmc01").connect()?;
//   session.http_get("/redfish/v1/Systems");
//   ❌ 错误:method `http_get` not found for `RedfishSession<Connected>`

#[derive(Debug)]
pub enum RedfishError {
    ConnectionFailed(String),
    AuthenticationFailed(String),
    HttpError { status: u16, message: String },
    ValidationError(String),
}

impl std::fmt::Display for RedfishError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ConnectionFailed(msg) => write!(f, "connection failed: {msg}"),
            Self::AuthenticationFailed(msg) => write!(f, "auth failed: {msg}"),
            Self::HttpError { status, message } =>
                write!(f, "HTTP {status}: {message}"),
            Self::ValidationError(msg) => write!(f, "validation: {msg}"),
        }
    }
}

消除的 bug 类别: 在断开连接或未认证的会话上发送请求。该方法根本不存在——没有运行时检查可以忘记。


第 2 节 — 特权令牌(Capability Tokens,ch04)

Redfish 定义了四个特权级别:Login、ConfigureComponents、ConfigureManager、ConfigureSelf。不是在运行时检查权限,而是将它们编码为零大小的证明令牌。

// ──── 特权令牌(零大小)────

/// 证明调用者有 Login 特权。
/// 由成功登录返回——获得的唯一方式。
pub struct LoginToken { _private: () }

/// 证明调用者有 ConfigureComponents 特权。
/// 仅可通过管理员级认证获得。
pub struct ConfigureComponentsToken { _private: () }

/// 证明调用者有 ConfigureManager 特权(固件更新等)。
pub struct ConfigureManagerToken { _private: () }

// 扩展登录以基于角色返回特权令牌:

impl RedfishSession<Connected> {
    /// 管理员登录——返回所有特权令牌。
    pub fn login_admin(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(
        RedfishSession<Authenticated>,
        LoginToken,
        ConfigureComponentsToken,
        ConfigureManagerToken,
    ), RedfishError> {
        let (session, login_tok) = self.login(user, pass)?;
        Ok((
            session,
            login_tok,
            ConfigureComponentsToken { _private: () },
            ConfigureManagerToken { _private: () },
        ))
    }

    /// 操作员登录——仅返回 Login + ConfigureComponents。
    pub fn login_operator(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(
        RedfishSession<Authenticated>,
        LoginToken,
        ConfigureComponentsToken,
    ), RedfishError> {
        let (session, login_tok) = self.login(user, pass)?;
        Ok((
            session,
            login_tok,
            ConfigureComponentsToken { _private: () },
        ))
    }

    /// 只读登录——仅返回 Login 令牌。
    pub fn login_readonly(
        self,
        user: &str,
        pass: &str,
    ) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
        self.login(user, pass)
    }
}

现在特权需求是函数签名的一部分:

use std::marker::PhantomData;
pub struct Authenticated;
pub struct RedfishSession<S> { base_url: String, auth_token: Option<String>, _state: PhantomData<S> }
pub struct LoginToken { _private: () }
pub struct ConfigureComponentsToken { _private: () }
pub struct ConfigureManagerToken { _private: () }
#[derive(Debug)] pub enum RedfishError { HttpError { status: u16, message: String } }

/// 任何有 Login 的人都可以读取热数据。
fn get_thermal(
    session: &RedfishSession<Authenticated>,
    _proof: &LoginToken,
) -> Result<serde_json::Value, RedfishError> {
    // GET /redfish/v1/Chassis/1/Thermal
    Ok(serde_json::json!({})) // 存根
}

/// 更改启动顺序需要 ConfigureComponents。
fn set_boot_order(
    session: &RedfishSession<Authenticated>,
    _proof: &ConfigureComponentsToken,
    order: &[&str],
) -> Result<(), RedfishError> {
    let _ = order;
    // PATCH /redfish/v1/Systems/1
    Ok(())
}

/// 恢复出厂设置需要 ConfigureManager。
fn reset_to_defaults(
    session: &RedfishSession<Authenticated>,
    _proof: &ConfigureManagerToken,
) -> Result<(), RedfishError> {
    // POST .../Actions/Manager.ResetToDefaults
    Ok(())
}

// 调用 reset_to_defaults 的操作员代码:
//
//   let (session, login, configure) = session.login_operator("op", "pass")?;
//   reset_to_defaults(&session, &???);
//   ❌ 错误:没有 ConfigureManagerToken 可用——操作员不能这样做

消除的 bug 类别: 特权升级。操作员级登录在物理上无法生成 ConfigureManagerToken——编译器不会让代码引用它。零运行时成本:对于编译后的二进制文件,这些令牌不存在。


第 3 节 — 类型化资源导航(Phantom Types,ch09)

Redfish 资源形成一棵树。将层次结构编码为类型可以防止构造非法 URI:

graph TD
    SR[ServiceRoot] --> Systems
    SR --> Chassis
    SR --> Managers
    SR --> UpdateService
    Systems --> CS[ComputerSystem]
    CS --> Processors
    CS --> Memory
    CS --> Bios
    Chassis --> Ch1[Chassis Instance]
    Ch1 --> Thermal
    Ch1 --> Power
    Managers --> Mgr[Manager Instance]
use std::marker::PhantomData;

// ──── 资源类型标记 ────

pub struct ServiceRoot;
pub struct SystemsCollection;
pub struct ComputerSystem;
pub struct ChassisCollection;
pub struct ChassisInstance;
pub struct ThermalResource;
pub struct PowerResource;
pub struct BiosResource;
pub struct ManagersCollection;
pub struct ManagerInstance;
pub struct UpdateServiceResource;

// ──── 类型化资源路径 ────

pub struct RedfishPath<R> {
    uri: String,
    _resource: PhantomData<R>,
}

impl RedfishPath<ServiceRoot> {
    pub fn root() -> Self {
        RedfishPath {
            uri: "/redfish/v1".to_string(),
            _resource: PhantomData,
        }
    }

    pub fn systems(&self) -> RedfishPath<SystemsCollection> {
        RedfishPath {
            uri: format!("{}/Systems", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn chassis(&self) -> RedfishPath<ChassisCollection> {
        RedfishPath {
            uri: format!("{}/Chassis", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn managers(&self) -> RedfishPath<ManagersCollection> {
        RedfishPath {
            uri: format!("{}/Managers", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn update_service(&self) -> RedfishPath<UpdateServiceResource> {
        RedfishPath {
            uri: format!("{}/UpdateService", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<SystemsCollection> {
    pub fn system(&self, id: &str) -> RedfishPath<ComputerSystem> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ComputerSystem> {
    pub fn bios(&self) -> RedfishPath<BiosResource> {
        RedfishPath {
            uri: format!("{}/Bios", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ChassisCollection> {
    pub fn instance(&self, id: &str) -> RedfishPath<ChassisInstance> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ChassisInstance> {
    pub fn thermal(&self) -> RedfishPath<ThermalResource> {
        RedfishPath {
            uri: format!("{}/Thermal", self.uri),
            _resource: PhantomData,
        }
    }

    pub fn power(&self) -> RedfishPath<PowerResource> {
        RedfishPath {
            uri: format!("{}/Power", self.uri),
            _resource: PhantomData,
        }
    }
}

impl RedfishPath<ManagersCollection> {
    pub fn manager(&self, id: &str) -> RedfishPath<ManagerInstance> {
        RedfishPath {
            uri: format!("{}/{}", self.uri, id),
            _resource: PhantomData,
        }
    }
}

impl<R> RedfishPath<R> {
    pub fn uri(&self) -> &str {
        &self.uri
    }
}

// ── 使用 ──

fn build_paths() {
    let root = RedfishPath::root();

    // ✅ 有效的导航
    let thermal = root.chassis().instance("1").thermal();
    assert_eq!(thermal.uri(), "/redfish/v1/Chassis/1/Thermal");

    let bios = root.systems().system("1").bios();
    assert_eq!(bios.uri(), "/redfish/v1/Systems/1/Bios");

    // ❌ 编译错误:ServiceRoot 没有 .thermal() 方法
    // root.thermal();

    // ❌ 编译错误:SystemsCollection 没有 .bios() 方法
    // root.systems().bios();

    // ❌ 编译错误:ChassisInstance 没有 .bios() 方法
    // root.chassis().instance("1").bios();
}

消除的 bug 类别: 格式错误的 URI、导航到给定父节点下不存在的子资源。层次结构在结构上强制执行——你只能通过 Chassis → Instance → Thermal 到达 Thermal。


第 4 节 — 类型化遥测读取(类型化命令 + 量纲分析,ch02 + ch06)

将类型化资源路径与量纲返回类型结合,这样编译器知道每个读取携带什么单位:

use std::marker::PhantomData;

// ──── 量纲类型(ch06)────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

// ──── 类型化 Redfish GET(ch02 模式应用于 REST)────

/// Redfish 资源类型决定其解析的响应。
pub trait RedfishResource {
    type Response;
    fn parse(json: &serde_json::Value) -> Result<Self::Response, RedfishError>;
}

// ──── 已验证的热响应(ch07)────

#[derive(Debug)]
pub struct ValidThermalResponse {
    pub temperatures: Vec<TemperatureReading>,
    pub fans: Vec<FanReading>,
}

#[derive(Debug)]
pub struct TemperatureReading {
    pub name: String,
    pub reading: Celsius,           // ← 量纲类型,不是 f64
    pub upper_critical: Celsius,
    pub status: HealthStatus,
}

#[derive(Debug)]
pub struct FanReading {
    pub name: String,
    pub reading: Rpm,               // ← 量纲类型,不是 u32
    pub status: HealthStatus,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HealthStatus { Ok, Warning, Critical }

impl RedfishResource for ThermalResource {
    type Response = ValidThermalResponse;

    fn parse(json: &serde_json::Value) -> Result<ValidThermalResponse, RedfishError> {
        // 一次解析和验证——边界验证(ch07)
        let temps = json["Temperatures"]
            .as_array()
            .ok_or_else(|| RedfishError::ValidationError(
                "missing Temperatures array".into(),
            ))?
            .iter()
            .map(|t| {
                Ok(TemperatureReading {
                    name: t["Name"]
                        .as_str()
                        .ok_or_else(|| RedfishError::ValidationError(
                            "missing Name".into(),
                        ))?
                        .to_string(),
                    reading: Celsius(
                        t["ReadingCelsius"]
                            .as_f64()
                            .ok_or_else(|| RedfishError::ValidationError(
                                "missing ReadingCelsius".into(),
                            ))?,
                    ),
                    upper_critical: Celsius(
                        t["UpperThresholdCritical"]
                            .as_f64()
                            .unwrap_or(105.0), // 安全默认值用于缺失的阈值
                    ),
                    status: parse_health(
                        t["Status"]["Health"]
                            .as_str()
                            .unwrap_or("OK"),
                    ),
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        let fans = json["Fans"]
            .as_array()
            .ok_or_else(|| RedfishError::ValidationError(
                "missing Fans array".into(),
            ))?
            .iter()
            .map(|f| {
                Ok(FanReading {
                    name: f["Name"]
                        .as_str()
                        .ok_or_else(|| RedfishError::ValidationError(
                            "missing Name".into(),
                        ))?
                        .to_string(),
                    reading: Rpm(
                        f["Reading"]
                            .as_u64()
                            .ok_or_else(|| RedfishError::ValidationError(
                                "missing Reading".into(),
                            ))? as u32,
                    ),
                    status: parse_health(
                        f["Status"]["Health"]
                            .as_str()
                            .unwrap_or("OK"),
                    ),
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        Ok(ValidThermalResponse { temperatures: temps, fans })
    }
}

fn parse_health(s: &str) -> HealthStatus {
    match s {
        "OK" => HealthStatus::Ok,
        "Warning" => HealthStatus::Warning,
        _ => HealthStatus::Critical,
    }
}

// ──── Authenticated 会话上的类型化 GET ────

impl RedfishSession<Authenticated> {
    pub fn get_resource<R: RedfishResource>(
        &self,
        path: &RedfishPath<R>,
    ) -> Result<R::Response, RedfishError> {
        let json = self.http_get(path.uri())?;
        R::parse(&json)
    }
}

// ── 使用 ──

fn read_thermal(
    session: &RedfishSession<Authenticated>,
    _proof: &LoginToken,
) -> Result<(), RedfishError> {
    let path = RedfishPath::root().chassis().instance("1").thermal();

    // 响应类型是推断的:ValidThermalResponse
    let thermal = session.get_resource(&path)?;

    for t in &thermal.temperatures {
        // t.reading 是 Celsius——只能与 Celsius 比较
        if t.reading > t.upper_critical {
            println!("CRITICAL: {} at {:?}", t.name, t.reading);
        }

        // ❌ 编译错误:不能比较 Celsius 与 Rpm
        // if t.reading > thermal.fans[0].reading { }

        // ❌ 编译错误:不能比较 Celsius 与 Watts
        // if t.reading > Watts(350.0) { }
    }

    Ok(())
}

消除的 bug 类别:

  • 单位混淆: Celsius ≠ Rpm ≠ Watts——编译器拒绝比较。
  • 缺失字段 panic: parse() 在边界验证;ValidThermalResponse 保证所有字段存在。
  • 错误的响应类型: get_resource(&thermal_path) 返回 ValidThermalResponse,不是原始 JSON。资源类型在编译期决定响应类型。

第 5 节 — 使用构建器 Type-State 的 PATCH(ch11,技巧 4)

Redfish PATCH 负载必须包含特定字段。一个在设置必需字段后提供 .apply() 的构建器可以防止不完整或空的 PATCH:

use std::marker::PhantomData;

// ──── 必需字段的类型级布尔值 ────

pub struct FieldUnset;
pub struct FieldSet;

// ──── BIOS 设置 PATCH 构建器 ────

pub struct BiosPatchBuilder<BootOrder, TpmState> {
    boot_order: Option<Vec<String>>,
    tpm_enabled: Option<bool>,
    _markers: PhantomData<(BootOrder, TpmState)>,
}

impl BiosPatchBuilder<FieldUnset, FieldUnset> {
    pub fn new() -> Self {
        BiosPatchBuilder {
            boot_order: None,
            tpm_enabled: None,
            _markers: PhantomData,
        }
    }
}

impl<T> BiosPatchBuilder<FieldUnset, T> {
    /// 设置启动顺序——将 BootOrder 标记转换为 FieldSet。
    pub fn boot_order(self, order: Vec<String>) -> BiosPatchBuilder<FieldSet, T> {
        BiosPatchBuilder {
            boot_order: Some(order),
            tpm_enabled: self.tpm_enabled,
            _markers: PhantomData,
        }
    }
}

impl<B> BiosPatchBuilder<B, FieldUnset> {
    /// 设置 TPM 状态——将 TpmState 标记转换为 FieldSet。
    pub fn tpm_enabled(self, enabled: bool) -> BiosPatchBuilder<B, FieldSet> {
        BiosPatchBuilder {
            boot_order: self.boot_order,
            tpm_enabled: Some(enabled),
            _markers: PhantomData,
        }
    }
}

impl BiosPatchBuilder<FieldSet, FieldSet> {
    /// .apply() 仅在设置所有必需字段时存在。
    pub fn apply(
        self,
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureComponentsToken,
        system: &RedfishPath<ComputerSystem>,
    ) -> Result<(), RedfishError> {
        let body = serde_json::json!({
            "Boot": {
                "BootOrder": self.boot_order.unwrap(),
            },
            "Oem": {
                "TpmEnabled": self.tpm_enabled.unwrap(),
            }
        });
        session.http_patch(
            &format!("{}/Bios/Settings", system.uri()),
            &body,
        )?;
        Ok(())
    }
}

// ── 使用 ──

fn configure_bios(
    session: &RedfishSession<Authenticated>,
    configure: &ConfigureComponentsToken,
) -> Result<(), RedfishError> {
    let system = RedfishPath::root().systems().system("1");

    // ✅ 设置所有必需字段——.apply() 可用
    BiosPatchBuilder::new()
        .boot_order(vec!["Pxe".into(), "Hdd".into()])
        .tpm_enabled(true)
        .apply(session, configure, &system)?;

    // ❌ 编译错误:.apply() 在 BiosPatchBuilder<FieldSet, FieldUnset> 上未找到
    // BiosPatchBuilder::new()
    //     .boot_order(vec!["Pxe".into()])
    //     .apply(session, configure, &system)?;

    // ❌ 编译错误:.apply() 在 BiosPatchBuilder<FieldUnset, FieldUnset> 上未找到
    // BiosPatchBuilder::new()
    //     .apply(session, configure, &system)?;

    Ok(())
}

消除的 bug 类别:

  • 空 PATCH: 不能在不设置每个必需字段的情况下调用 .apply()。
  • 缺少特权: .apply() 需要 &ConfigureComponentsToken。
  • 错误的资源: 接受 &RedfishPath<ComputerSystem>,不是原始字符串。

第 6 节 — 固件更新生命周期(Single-Use + Type-State,ch03 + ch05)

Redfish UpdateService 有严格的顺序:push image → verify → apply → reboot。每个阶段必须恰好发生一次,按顺序。

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : push_image()
    Uploading --> Uploaded : upload completes
    Uploaded --> Verified : verify() ✓
    Uploaded --> Failed : verify() ✗
    Verified --> Applying : apply() — consumes Verified
    Applying --> NeedsReboot : apply completes
    NeedsReboot --> [*] : reboot()
    Failed --> [*]

    note right of Verified : apply() 消费此状态——
    note right of Verified : 不能应用两次
use std::marker::PhantomData;

// ──── 固件更新状态 ────

pub struct FwIdle;
pub struct FwUploaded;
pub struct FwVerified;
pub struct FwApplying;
pub struct FwNeedsReboot;

pub struct FirmwareUpdate<S> {
    task_uri: String,
    image_hash: String,
    _phase: PhantomData<S>,
}

impl FirmwareUpdate<FwIdle> {
    pub fn push_image(
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureManagerToken,
        image: &[u8],
    ) -> Result<FirmwareUpdate<FwUploaded>, RedfishError> {
        // POST /redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate
        // 或多部分推送到 /redfish/v1/UpdateService/upload
        let _ = image;
        println!("Image uploaded ({} bytes)", image.len());
        Ok(FirmwareUpdate {
            task_uri: "/redfish/v1/TaskService/Tasks/1".to_string(),
            image_hash: "sha256:abc123".to_string(),
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwUploaded> {
    /// 验证图像完整性。成功时返回 FwVerified。
    pub fn verify(self) -> Result<FirmwareUpdate<FwVerified>, RedfishError> {
        // 轮询任务直到验证完成
        println!("Image verified: {}", self.image_hash);
        Ok(FirmwareUpdate {
            task_uri: self.task_uri,
            image_hash: self.image_hash,
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwVerified> {
    /// 应用更新。消费 self——不能应用两次。
    /// 这是来自 ch03 的一次性模式。
    pub fn apply(self) -> Result<FirmwareUpdate<FwNeedsReboot>, RedfishError> {
        // PATCH /redfish/v1/UpdateService — 设置 ApplyTime
        println!("Firmware applied from {}", self.task_uri);
        // self 被移动——再次调用 apply() 是编译错误
        Ok(FirmwareUpdate {
            task_uri: self.task_uri,
            image_hash: self.image_hash,
            _phase: PhantomData,
        })
    }
}

impl FirmwareUpdate<FwNeedsReboot> {
    /// 重启以激活新固件。
    pub fn reboot(
        self,
        session: &RedfishSession<Authenticated>,
        _proof: &ConfigureManagerToken,
    ) -> Result<(), RedfishError> {
        // POST .../Actions/Manager.Reset {"ResetType": "GracefulRestart"}
        let _ = session;
        println!("BMC rebooting to activate firmware");
        Ok(())
    }
}

// ── 使用 ──

fn update_bmc_firmware(
    session: &RedfishSession<Authenticated>,
    manager_proof: &ConfigureManagerToken,
    image: &[u8],
) -> Result<(), RedfishError> {
    // 每个步骤返回下一个状态——旧状态被消费
    let uploaded = FirmwareUpdate::push_image(session, manager_proof, image)?;
    let verified = uploaded.verify()?;
    let needs_reboot = verified.apply()?;
    needs_reboot.reboot(session, manager_proof)?;

    // ❌ 编译错误:使用已移动的值 `verified`
    // verified.apply()?;

    // ❌ 编译错误:FirmwareUpdate<FwUploaded> 没有 .apply() 方法
    // uploaded.apply()?;      // 必须先验证!

    // ❌ 编译错误:push_image 需要 &ConfigureManagerToken
    // FirmwareUpdate::push_image(session, &login_token, image)?;

    Ok(())
}

消除的 bug 类别:

  • 应用未验证的固件: .apply() 仅存在于 FwVerified 上。
  • 重复应用: apply() 消费 self——移动的值不能重用。
  • 跳过重启: FwNeedsReboot 是一个不同的类型;你不能意外地在固件暂存时继续正常操作。
  • 未授权的更新: push_image() 需要 &ConfigureManagerToken。

第 7 节 — 整合在一起

这是组合所有六个部分的完整诊断工作流:

fn full_redfish_diagnostic() -> Result<(), RedfishError> {
    // ── 1. 会话生命周期(第 1 节)────
    let session = RedfishSession::new("bmc01.lab.local");
    let session = session.connect()?;

    // ── 2. 特权令牌(第 2 节)────
    // 管理员登录——获得所有 capability 令牌
    let (session, _login, configure, manager) =
        session.login_admin("admin", "p@ssw0rd")?;

    // ── 3. 类型化导航(第 3 节)────
    let thermal_path = RedfishPath::root()
        .chassis()
        .instance("1")
        .thermal();

    // ── 4. 类型化遥测读取(第 4 节)────
    let thermal: ValidThermalResponse = session.get_resource(&thermal_path)?;

    for t in &thermal.temperatures {
        // Celsius 只能与 Celsius 比较——量纲安全
        if t.reading > t.upper_critical {
            println!("🔥 {} is critical: {:?}", t.name, t.reading);
        }
    }

    for f in &thermal.fans {
        if f.reading < Rpm(1000) {
            println!("⚠ {} below threshold: {:?}", f.name, f.reading);
        }
    }

    // ── 5. 类型安全 PATCH(第 5 节)────
    let system_path = RedfishPath::root().systems().system("1");

    BiosPatchBuilder::new()
        .boot_order(vec!["Pxe".into(), "Hdd".into()])
        .tpm_enabled(true)
        .apply(&session, &configure, &system_path)?;

    // ── 6. 固件更新生命周期(第 6 节)────
    let firmware_image = include_bytes!("bmc_firmware.bin");
    let uploaded = FirmwareUpdate::push_image(&session, &manager, firmware_image)?;
    let verified = uploaded.verify()?;
    let needs_reboot = verified.apply()?;

    // ── 7. 干净关闭 ────
    needs_reboot.reboot(&session, &manager)?;
    session.logout();

    Ok(())
}

编译器证明的内容

#Bug 类别如何防止模式(章节)
1未认证会话上的请求http_get() 仅存在于 Session<Authenticated> 上Type-state(§1)
2特权升级ConfigureManagerToken 不由操作员登录返回Capability tokens(§2)
3格式错误的 Redfish URI导航方法强制执行父→子层次结构Phantom types(§3)
4单位混淆(°C vs RPM vs W)Celsius、Rpm、Watts 是不同的类型量纲分析(§4)
5缺失 JSON 字段 → panicValidThermalResponse 在解析边界验证验证边界(§4)
6错误的响应类型RedfishResource::Response 每个资源固定类型化命令(§4)
7不完整的 PATCH 负载.apply() 仅在所有字段是 FieldSet 时存在构建器 type-state(§5)
8缺少 PATCH 特权.apply() 需要 &ConfigureComponentsTokenCapability tokens(§5)
9应用未验证的固件.apply() 仅存在于 FwVerified 上Type-state(§6)
10重复固件应用apply() 消费 self——值被移动一次性类型(§6)
11没有权威的固件更新push_image() 需要 &ConfigureManagerTokenCapability tokens(§6)
12登录后使用logout() 消费会话所有权(§1)

所有十二个保证的总运行时开销:零。

生成的二进制文件与无类型版本进行相同的 HTTP 调用——但无类型版本可能有 12 类 bug。这个版本不可能。


对比:IPMI 集成(ch10)vs. Redfish 集成

维度ch10 (IPMI)本章 (Redfish)
传输KCS/LAN 上的原始字节HTTPS 上的 JSON
导航扁平命令码(NetFn/Cmd)层次 URI 树
响应绑定IpmiCmd::ResponseRedfishResource::Response
特权模型单个 AdminToken基于角色的多令牌
负载构造字节数组JSON 的构建器 type-state
更新生命周期未覆盖完整 type-state 链
锻炼的模式78(添加构建器 type-state)

这两章是互补的:ch10 展示了这些模式在字节级有效,本章展示了它们在 REST/JSON 级同样有效。类型系统不关心传输——它以两种方式证明正确性。

关键要点

  1. 八个模式组合成一个 Redfish 客户端——会话 type-state、capability 令牌、phantom 类型 URI、类型化命令、量纲分析、验证边界、构建器 type-state 和一次性固件应用。
  2. 十二类 bug 变成编译错误——见上表。
  3. 零运行时开销——每个证明令牌、phantom 类型和 type-state 标记都编译消失。二进制文件与手写的无类型代码相同。
  4. REST API 与字节协议受益相同——来自 ch02–ch09 的模式同样适用于 JSON-over-HTTPS(Redfish)和 bytes-over-KCS(IPMI)。
  5. 特权强制执行是结构性的,不是过程性的——函数签名声明需要什么;编译器强制执行它。
  6. 这是一个设计模板——为你的特定 Redfish schema 和组织角色层次结构调整资源类型标记、capability 令牌和构建器。

实战演练 — 类型安全的 Redfish 服务器 🟡

你将学到: 如何组合响应构建器 type-state、源可用性令牌、量纲序列化、健康汇总、schema 版本化和类型化 action 分组成一个 Redfish 服务器,它不能产生 schema 非合规的响应——这是 ch17 客户端演练的镜像。

交叉引用: ch02(类型化命令——反向用于 action 分发)、ch04(capability 令牌——源可用性)、ch06(量纲类型——序列化端)、ch07(验证边界——反向:“构造,不要序列化”)、ch09(phantom 类型——schema 版本化)、ch11(技巧 3 — #[non_exhaustive],技巧 4 — 构建器 type-state)、ch17(客户端对应)

镜像问题

第 17 章问:“我如何正确地消费 Redfish?” 本章问镜像问题:“我如何正确地生产 Redfish?”

在客户端,危险是信任坏数据。在服务端,危险是发射坏数据——而集群中的每个客户端都信任你发送的内容。

单个 GET /redfish/v1/Systems/1 响应必须融合来自许多来源的数据:

flowchart LR
    subgraph Sources
        SMBIOS["SMBIOS<br/>Type 1, Type 17"]
        SDR["IPMI 传感器<br/>(SDR + readings)"]
        SEL["IPMI SEL<br/>(critical events)"]
        PCIe["PCIe 配置<br/>空间"]
        FW["固件<br/>版本表"]
        PWR["电源状态<br/>寄存器"]
    end

    subgraph Server["Redfish 服务器"]
        Handler["GET 处理程序"]
        Builder["ComputerSystem<br/>构建器"]
    end

    SMBIOS -->|"Name, UUID, Serial"| Handler
    SDR -->|"温度、风扇"| Handler
    SEL -->|"健康升级"| Handler
    PCIe -->|"设备链接"| Handler
    FW -->|"BIOS 版本"| Handler
    PWR -->|"PowerState"| Handler
    Handler --> Builder
    Builder -->|".build()"| JSON["Schema 合规<br/>JSON 响应"]

    style JSON fill:#c8e6c9,color:#000
    style Builder fill:#e1f5fe,color:#000

在 C 语言中,这是一个 500 行的处理程序,调用六个子系统,用 json_object_set() 手动构建 JSON 树,并希望每个必需字段都被填充。忘记一个?响应违反 Redfish schema。单位错了?每个客户端看到损坏的遥测数据。

// C 语言——组装问题
json_t *get_computer_system(const char *id) {
    json_t *obj = json_object();
    json_object_set_new(obj, "@odata.type",
        json_string("#ComputerSystem.v1_13_0.ComputerSystem"));

    // 🐛 忘记设置 "Name"——schema 需要它
    // 🐛 忘记设置 "UUID"——schema 需要它

    smbios_type1_t *t1 = smbios_get_type1();
    if (t1) {
        json_object_set_new(obj, "Manufacturer",
            json_string(t1->manufacturer));
    }

    json_object_set_new(obj, "PowerState",
        json_string(get_power_state()));  // 至少这个总是可用的

    // 🐛 Reading 是原始 ADC 计数,不是 Celsius——没有类型来捕获它
    double cpu_temp = read_sensor(SENSOR_CPU_TEMP);
    // 这个数字在某个地方的 Thermal 响应中结束...
    // 但没有在类型级别将它与"Celsius"联系起来

    // 🐛 Health 是手动计算的——忘记包含 PSU 状态
    json_object_set_new(obj, "Status",
        build_status("Enabled", "OK")); // 应该是"Critical"——PSU 正在失败

    return obj; // 缺失 2 个必需字段,错误的健康状态,原始单位
}

一个处理程序中的四个 bug。在客户端,每个 bug 影响一个客户端。在服务端,每个 bug 影响每个查询这个 BMC 的客户端。


第 1 节 — 响应构建器 Type-State:“构造,不要序列化”(ch07 反向)

第 7 章教授“解析,不要验证“——验证入站数据一次,在类型中携带证明。服务端镜像是**“构造,不要序列化”**——通过一个构建器构建出站响应,该构建器在所有必需字段存在时才提供 .build()。

use std::marker::PhantomData;

// ──── 类型级字段跟踪 ────

pub struct HasField;
pub struct MissingField;

// ──── 响应构建器 ────

/// ComputerSystem Redfish 资源的构建器。
/// 类型参数跟踪哪些 REQUIRED 字段已提供。
/// 可选字段不需要类型级跟踪。
pub struct ComputerSystemBuilder<Name, Uuid, PowerState, Status> {
    // 必需字段——在类型级跟踪
    name: Option<String>,
    uuid: Option<String>,
    power_state: Option<PowerStateValue>,
    status: Option<ResourceStatus>,
    // 可选字段——不跟踪(总是可设置)
    manufacturer: Option<String>,
    model: Option<String>,
    serial_number: Option<String>,
    bios_version: Option<String>,
    processor_summary: Option<ProcessorSummary>,
    memory_summary: Option<MemorySummary>,
    _markers: PhantomData<(Name, Uuid, PowerState, Status)>,
}

#[derive(Debug, Clone, serde::Serialize)]
pub enum PowerStateValue { On, Off, PoweringOn, PoweringOff }

#[derive(Debug, Clone, serde::Serialize)]
pub struct ResourceStatus {
    #[serde(rename = "State")]
    pub state: StatusState,
    #[serde(rename = "Health")]
    pub health: HealthValue,
    #[serde(rename = "HealthRollup", skip_serializing_if = "Option::is_none")]
    pub health_rollup: Option<HealthValue>,
}

#[derive(Debug, Clone, Copy, serde::Serialize)]
pub enum StatusState { Enabled, Disabled, Absent, StandbyOffline, Starting }

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub enum HealthValue { OK, Warning, Critical }

#[derive(Debug, Clone, serde::Serialize)]
pub struct ProcessorSummary {
    #[serde(rename = "Count")]
    pub count: u32,
    #[serde(rename = "Status")]
    pub status: ResourceStatus,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct MemorySummary {
    #[serde(rename = "TotalSystemMemoryGiB")]
    pub total_gib: f64,
    #[serde(rename = "Status")]
    pub status: ResourceStatus,
}

// ──── 构造函数:所有字段从 MissingField 开始 ────

impl ComputerSystemBuilder<MissingField, MissingField, MissingField, MissingField> {
    pub fn new() -> Self {
        ComputerSystemBuilder {
            name: None, uuid: None, power_state: None, status: None,
            manufacturer: None, model: None, serial_number: None,
            bios_version: None, processor_summary: None, memory_summary: None,
            _markers: PhantomData,
        }
    }
}

// ──── 必需字段设置器——每个转换一个类型参数 ────

impl<U, P, S> ComputerSystemBuilder<MissingField, U, P, S> {
    pub fn name(self, name: String) -> ComputerSystemBuilder<HasField, U, P, S> {
        ComputerSystemBuilder {
            name: Some(name), uuid: self.uuid,
            power_state: self.power_state, status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, P, S> ComputerSystemBuilder<N, MissingField, P, S> {
    pub fn uuid(self, uuid: String) -> ComputerSystemBuilder<N, HasField, P, S> {
        ComputerSystemBuilder {
            name: self.name, uuid: Some(uuid),
            power_state: self.power_state, status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, U, S> ComputerSystemBuilder<N, U, MissingField, S> {
    pub fn power_state(self, ps: PowerStateValue)
        -> ComputerSystemBuilder<N, U, HasField, S>
    {
        ComputerSystemBuilder {
            name: self.name, uuid: self.uuid,
            power_state: Some(ps), status: self.status,
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

impl<N, U, P> ComputerSystemBuilder<N, U, P, MissingField> {
    pub fn status(self, status: ResourceStatus)
        -> ComputerSystemBuilder<N, U, P, HasField>
    {
        ComputerSystemBuilder {
            name: self.name, uuid: self.uuid,
            power_state: self.power_state, status: Some(status),
            manufacturer: self.manufacturer, model: self.model,
            serial_number: self.serial_number, bios_version: self.bios_version,
            processor_summary: self.processor_summary,
            memory_summary: self.memory_summary, _markers: PhantomData,
        }
    }
}

// ──── 可选字段设置器——在任何状态下都可用 ────

impl<N, U, P, S> ComputerSystemBuilder<N, U, P, S> {
    pub fn manufacturer(mut self, m: String) -> Self {
        self.manufacturer = Some(m); self
    }
    pub fn model(mut self, m: String) -> Self {
        self.model = Some(m); self
    }
    pub fn serial_number(mut self, s: String) -> Self {
        self.serial_number = Some(s); self
    }
    pub fn bios_version(mut self, v: String) -> Self {
        self.bios_version = Some(v); self
    }
    pub fn processor_summary(mut self, ps: ProcessorSummary) -> Self {
        self.processor_summary = Some(ps); self
    }
    pub fn memory_summary(mut self, ms: MemorySummary) -> Self {
        self.memory_summary = Some(ms); self
    }
}

// ──── .build() 仅在所有必需字段是 HasField 时存在 ────

impl ComputerSystemBuilder<HasField, HasField, HasField, HasField> {
    pub fn build(self, id: &str) -> serde_json::Value {
        let mut obj = serde_json::json!({
            "@odata.id": format!("/redfish/v1/Systems/{id}"),
            "@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
            "Id": id,
            // Type-state 保证这些是 Some——.unwrap() 在这里安全。
            // 在生产中, prefer .expect("guaranteed by type state").
            "Name": self.name.unwrap(),
            "UUID": self.uuid.unwrap(),
            "PowerState": self.power_state.unwrap(),
            "Status": self.status.unwrap(),
        });

        // 可选字段——仅存在时包含
        if let Some(m) = self.manufacturer {
            obj["Manufacturer"] = serde_json::json!(m);
        }
        if let Some(m) = self.model {
            obj["Model"] = serde_json::json!(m);
        }
        if let Some(s) = self.serial_number {
            obj["SerialNumber"] = serde_json::json!(s);
        }
        if let Some(v) = self.bios_version {
            obj["BiosVersion"] = serde_json::json!(v);
        }
        // 注意:.unwrap() 用于 to_value() 简洁。
        // 生产代码应用 `?` 传播序列化错误。
        if let Some(ps) = self.processor_summary {
            obj["ProcessorSummary"] = serde_json::to_value(ps).unwrap();
        }
        if let Some(ms) = self.memory_summary {
            obj["MemorySummary"] = serde_json::to_value(ms).unwrap();
        }

        obj
    }
}

//
// ── 编译器强制执行完整性 ──
//
// ✅ 设置所有必需字段——.build() 可用:
// ComputerSystemBuilder::new()
//     .name("PowerEdge R750".into())
//     .uuid("4c4c4544-...".into())
//     .power_state(PowerStateValue::On)
//     .status(ResourceStatus { ... })
//     .manufacturer("Dell".into())        // 可选——可以包含
//     .build("1")
//
// ❌ 缺失"Name"——编译错误:
// ComputerSystemBuilder::new()
//     .uuid("4c4c4544-...".into())
//     .power_state(PowerStateValue::On)
//     .status(ResourceStatus { ... })
//     .build("1")
//   错误:method `build` not found for
//   `ComputerSystemBuilder<MissingField, HasField, HasField, HasField>`

消除的 bug 类别: schema 非合规响应。处理程序在物理上无法在没有提供每个必需字段的情况下序列化 ComputerSystem。编译器错误消息甚至告诉你哪个字段缺失——它就在类型参数中:Name 位置中的MissingField。


第 2 节 — 源可用性令牌(Capability Tokens,ch04 — 新变化)

在 ch04 和 ch17 中,capability 令牌证明授权——“调用者被允许这样做。“在服务端,相同的模式证明可用性——“这个数据源已成功初始化。”

BMC 查询的每个子系统都可以独立失败。SMBIOS 表可能损坏。传感器子系统可能仍在初始化。PCIe 总线扫描可能超时。将每个编码为证明令牌:

/// 证明 SMBIOS 表成功解析。
/// 仅由 SMBIOS init 函数产生。
pub struct SmbiosReady {
    _private: (),
}

/// 证明 IPMI 传感器子系统响应。
pub struct SensorsReady {
    _private: (),
}

/// 证明 PCIe 总线扫描完成。
pub struct PcieReady {
    _private: (),
}

/// 证明 SEL 成功读取。
pub struct SelReady {
    _private: (),
}

// ──── 数据源初始化 ────

pub struct SmbiosTables {
    pub product_name: String,
    pub manufacturer: String,
    pub serial_number: String,
    pub uuid: String,
}

pub struct SensorCache {
    pub cpu_temp: Celsius,
    pub inlet_temp: Celsius,
    pub fan_readings: Vec<(String, Rpm)>,
    pub psu_power: Vec<(String, Watts)>,
}

/// 丰富的 SEL 摘要——来自类型化事件的每子系统健康。
/// 由 ch07 的 SEL 部分中的消费者流水线构建。
/// 替换有损的 `has_critical_events: bool` 与类型化粒度。
pub struct TypedSelSummary {
    pub total_entries: u32,
    pub processor_health: HealthValue,
    pub memory_health: HealthValue,
    pub power_health: HealthValue,
    pub thermal_health: HealthValue,
    pub fan_health: HealthValue,
    pub storage_health: HealthValue,
    pub security_health: HealthValue,
}

pub fn init_smbios() -> Option<(SmbiosReady, SmbiosTables)> {
    // 读取 SMBIOS 入口点,解析表...
    // 如果表缺失或损坏返回 None
    Some((
        SmbiosReady { _private: () },
        SmbiosTables {
            product_name: "PowerEdge R750".into(),
            manufacturer: "Dell Inc.".into(),
            serial_number: "SVC1234567".into(),
            uuid: "4c4c4544-004d-5610-804c-b2c04f435031".into(),
        },
    ))
}

pub fn init_sensors() -> Option<(SensorsReady, SensorCache)> {
    // 初始化 SDR 存储库,读取所有传感器...
    // 如果 IPMI 子系统不响应返回 None
    Some((
        SensorsReady { _private: () },
        SensorCache {
            cpu_temp: Celsius(68.0),
            inlet_temp: Celsius(24.0),
            fan_readings: vec![
                ("Fan1".into(), Rpm(8400)),
                ("Fan2".into(), Rpm(8200)),
            ],
            psu_power: vec![
                ("PSU1".into(), Watts(285.0)),
                ("PSU2".into(), Watts(290.0)),
            ],
        },
    ))
}

pub fn init_sel() -> Option<(SelReady, TypedSelSummary)> {
    // 在生产中:读取 SEL 条目,通过 ch07 的 TryFrom 解析,
    // 通过 classify_event_health() 分类,通过 summarize_sel() 汇总。
    Some((
        SelReady { _private: () },
        TypedSelSummary {
            total_entries: 42,
            processor_health: HealthValue::OK,
            memory_health: HealthValue::OK,
            power_health: HealthValue::OK,
            thermal_health: HealthValue::OK,
            fan_health: HealthValue::OK,
            storage_health: HealthValue::OK,
            security_health: HealthValue::OK,
        },
    ))
}

现在,从数据源填充构建器字段的函数需要相应的证明令牌:

/// 从 SMBIOS 填充字段。需要证明 SMBIOS 可用。
fn populate_from_smbios<P, S>(
    builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
    _proof: &SmbiosReady,
    tables: &SmbiosTables,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
    builder
        .name(tables.product_name.clone())
        .uuid(tables.uuid.clone())
        .manufacturer(tables.manufacturer.clone())
        .serial_number(tables.serial_number.clone())
}

/// 当 SMBIOS 不可用时的回退——使用安全默认值提供必需字段。
fn populate_smbios_fallback<P, S>(
    builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
    builder
        .name("Unknown System".into())
        .uuid("00000000-0000-0000-0000-000000000000".into())
}

处理程序根据哪些令牌可用来选择路径:

fn build_computer_system(
    smbios: &Option<(SmbiosReady, SmbiosTables)>,
    power_state: PowerStateValue,
    health: ResourceStatus,
) -> serde_json::Value {
    let builder = ComputerSystemBuilder::new()
        .power_state(power_state)
        .status(health);

    let builder = match smbios {
        Some((proof, tables)) => populate_from_smbios(builder, proof, tables),
        None => populate_smbios_fallback(builder),
    };

    // 两条路径都为 Name 和 UUID 产生 HasField。
    // .build() 在任一路径都可用。
    builder.build("1")
}

消除的 bug 类别: 调用初始化失败的子系统。如果 SMBIOS 没有解析,你没有 SmbiosReady 令牌——编译器强制你通过回退路径。没有运行时if (smbios != NULL) 可以忘记。

组合源令牌与 Capability Mixins(ch08)

有多个 Redfish 资源类型要服务(ComputerSystem、Chassis、Manager、Thermal、Power),源填充逻辑在处理程序之间重复。来自 ch08 的mixin模式消除了这种重复。声明处理程序有什么来源, blanket impls 自动提供填充方法:

/// ── 数据源的 Ingredient Traits(ch08)────

pub trait HasSmbios {
    fn smbios(&self) -> &(SmbiosReady, SmbiosTables);
}

pub trait HasSensors {
    fn sensors(&self) -> &(SensorsReady, SensorCache);
}

pub trait HasSel {
    fn sel(&self) -> &(SelReady, TypedSelSummary);
}

/// ── Mixin:任何有 SMBIOS 的处理程序获得身份填充 ──

pub trait IdentityMixin: HasSmbios {
    fn populate_identity<P, S>(
        &self,
        builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
    ) -> ComputerSystemBuilder<HasField, HasField, P, S> {
        let (_, tables) = self.smbios();
        builder
            .name(tables.product_name.clone())
            .uuid(tables.uuid.clone())
            .manufacturer(tables.manufacturer.clone())
            .serial_number(tables.serial_number.clone())
    }
}

/// 自动为任何有 SMBIOS capability 的类型实现。
impl<T: HasSmbios> IdentityMixin for T {}

/// ── Mixin:任何有 Sensors + SEL 的处理程序获得健康汇总 ──

pub trait HealthMixin: HasSensors + HasSel {
    fn compute_health(&self) -> ResourceStatus {
        let (_, cache) = self.sensors();
        let (_, sel_summary) = self.sel();
        compute_system_health(
            Some(&(SensorsReady { _private: () }, cache.clone())).as_ref(),
            Some(&(SelReady { _private: () }, sel_summary.clone())).as_ref(),
        )
    }
}

impl<T: HasSensors + HasSel> HealthMixin for T {}

/// ── 具体处理程序拥有可用的来源 ────

struct FullPlatformHandler {
    smbios: (SmbiosReady, SmbiosTables),
    sensors: (SensorsReady, SensorCache),
    sel: (SelReady, TypedSelSummary),
}

impl HasSmbios  for FullPlatformHandler {
    fn smbios(&self) -> &(SmbiosReady, SmbiosTables) { &self.smbios }
}
impl HasSensors for FullPlatformHandler {
    fn sensors(&self) -> &(SensorsReady, SensorCache) { &self.sensors }
}
impl HasSel     for FullPlatformHandler {
    fn sel(&self) -> &(SelReady, TypedSelSummary) { &self.sel }
}

// FullPlatformHandler 自动获得:
//   IdentityMixin::populate_identity()   (通过 HasSmbios)
//   HealthMixin::compute_health()        (通过 HasSensors + HasSel)
//
// 一个 SensorsOnlyHandler 实现 HasSensors 但不是 HasSel
// 会获得 IdentityMixin(如果它有 SMBIOS)但不是 HealthMixin。
// 对它调用 .compute_health() → 编译错误。

这直接镜像 ch08 的 BaseBoardController 模式:ingredient traits 声明你有什么,mixin traits 通过 blanket impls 提供行为,编译器在每个 mixin 的先决条件上控制。添加新的数据源(如 HasNvme)加上 mixin(如 StorageMixin: HasNvme + HasSel)为每个有两个的处理程序自动获得存储健康汇总。


第 3 节 — 序列化边界的量纲类型(ch06)

在客户端(ch17 §4),量纲类型防止读取 °C 作为 RPM。在服务端,它们防止写入 RPM 到 Celsius JSON 字段。这可能更危险——服务器上的错误值传播到每个客户端。

use serde::Serialize;

// ──── 来自 ch06 的量纲类型,带 Serialize ────

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Rpm(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Watts(pub f64);

// ──── Redfish Thermal 响应成员 ────
// 字段类型强制哪个单位进入哪个 JSON 属性。

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TemperatureMember {
    pub member_id: String,
    pub name: String,
    pub reading_celsius: Celsius,           // ← 必须是 Celsius
    #[serde(skip_serializing_if = "Option::is_none")]
    pub upper_threshold_critical: Option<Celsius>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub upper_threshold_fatal: Option<Celsius>,
    pub status: ResourceStatus,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct FanMember {
    pub member_id: String,
    pub name: String,
    pub reading: Rpm,                       // ← 必须是 Rpm
    pub reading_units: &'static str,        // 总是"RPM"
    pub status: ResourceStatus,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct PowerControlMember {
    pub member_id: String,
    pub name: String,
    pub power_consumed_watts: Watts,        // ← 必须是 Watts
    #[serde(skip_serializing_if = "Option::is_none")]
    pub power_capacity_watts: Option<Watts>,
    pub status: ResourceStatus,
}

// ──── 从传感器缓存构建 Thermal 响应 ────

fn build_thermal_response(
    _proof: &SensorsReady,
    cache: &SensorCache,
) -> serde_json::Value {
    let temps = vec![
        TemperatureMember {
            member_id: "0".into(),
            name: "CPU Temp".into(),
            reading_celsius: cache.cpu_temp,     // Celsius → Celsius ✅
            upper_threshold_critical: Some(Celsius(95.0)),
            upper_threshold_fatal: Some(Celsius(105.0)),
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: if cache.cpu_temp < Celsius(95.0) {
                    HealthValue::OK
                } else {
                    HealthValue::Critical
                },
                health_rollup: None,
            },
        },
        TemperatureMember {
            member_id: "1".into(),
            name: "Inlet Temp".into(),
            reading_celsius: cache.inlet_temp,   // Celsius → Celsius ✅
            upper_threshold_critical: Some(Celsius(42.0)),
            upper_threshold_fatal: None,
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: HealthValue::OK,
                health_rollup: None,
            },
        },

        // ❌ 编译错误——不能把 Rpm 放入 Celsius 字段:
        // TemperatureMember {
        //     reading_celsius: cache.fan_readings[0].1,  // Rpm ≠ Celsius
        //     ...
        // }
    ];

    let fans: Vec<FanMember> = cache.fan_readings.iter().enumerate().map(|(i, (name, rpm))| {
        FanMember {
            member_id: i.to_string(),
            name: name.clone(),
            reading: *rpm,                       // Rpm → Rpm ✅
            reading_units: "RPM",
            status: ResourceStatus {
                state: StatusState::Enabled,
                health: if *rpm > Rpm(1000) { HealthValue::OK } else { HealthValue::Critical },
                health_rollup: None,
            },
        }
    }).collect();

    serde_json::json!({
        "@odata.type": "#Thermal.v1_7_0.Thermal",
        "Temperatures": temps,
        "Fans": fans,
    })
}

消除的 bug 类别: 序列化时的单位混淆。Redfish schema 说 ReadingCelsius 是 °C。Rust 类型系统说 reading_celsius 必须是 Celsius。如果开发人员意外传递 Rpm(8400) 或Watts(285.0),编译器在值到达 JSON 之前捕获它。


第 4 节 — 健康汇总作为类型化 Fold

Redfish Status.Health 是一个汇总——所有子组件的最差健康状态。在 C 语言中,这通常是一系列if 检查,不可避免地遗漏来源。使用类型化枚举和 Ord,汇总是一行 fold——编译器确保每个来源贡献:

/// 从多个来源汇总健康。
/// Ord on HealthValue: OK < Warning < Critical。
/// 返回最差(max)值。
fn rollup(sources: &[HealthValue]) -> HealthValue {
    sources.iter().copied().max().unwrap_or(HealthValue::OK)
}

/// 从所有子组件计算系统级健康。
/// 采用每个来源的显式引用——调用者必须提供**所有**来源。
fn compute_system_health(
    sensors: Option<&(SensorsReady, SensorCache)>,
    sel: Option<&(SelReady, TypedSelSummary)>,
) -> ResourceStatus {
    let mut inputs = Vec::new();

    // ── 实时传感器读取 ────
    if let Some((_proof, cache)) = sensors {
        // 温度健康(量纲:Celsius 比较)
        if cache.cpu_temp > Celsius(95.0) {
            inputs.push(HealthValue::Critical);
        } else if cache.cpu_temp > Celsius(85.0) {
            inputs.push(HealthValue::Warning);
        } else {
            inputs.push(HealthValue::OK);
        }

        // 风扇健康(量纲:Rpm 比较)
        for (_name, rpm) in &cache.fan_readings {
            if *rpm < Rpm(500) {
                inputs.push(HealthValue::Critical);
            } else if *rpm < Rpm(1000) {
                inputs.push(HealthValue::Warning);
            } else {
                inputs.push(HealthValue::OK);
            }
        }

        // PSU 健康(量纲:Watts 比较)
        for (_name, watts) in &cache.psu_power {
            if *watts > Watts(800.0) {
                inputs.push(HealthValue::Critical);
            } else {
                inputs.push(HealthValue::OK);
            }
        }
    }

    // ── SEL 每子系统健康(来自 ch07 的 TypedSelSummary)────
    // 每个子系统的健康通过穷举匹配每个传感器类型和事件变体派生。
    // 没有信息丢失。
    if let Some((_proof, sel_summary)) = sel {
        inputs.push(sel_summary.processor_health);
        inputs.push(sel_summary.memory_health);
        inputs.push(sel_summary.power_health);
        inputs.push(sel_summary.thermal_health);
        inputs.push(sel_summary.fan_health);
        inputs.push(sel_summary.storage_health);
        inputs.push(sel_summary.security_health);
    }

    let health = rollup(&inputs);

    ResourceStatus {
        state: StatusState::Enabled,
        health,
        health_rollup: Some(health),
    }
}

消除的 bug 类别: 不完整的健康汇总。在 C 语言中,忘记在健康计算中包含 PSU 状态是一个静默 bug——系统报告“OK“而 PSU 正在失败。在这里,compute_system_health 采用每个数据源的显式引用。SEL 贡献不再是损失性的bool——它是七个每子系统HealthValue 字段,由 ch07 的消费者流水线通过穷举匹配派生。添加新的 SEL 传感器类型强制分类器处理它;添加新的子系统字段强制汇总包含它。


第 5 节 — Phantom 类型的 Schema 版本化(ch09)

如果 BMC 宣传 ComputerSystem.v1_13_0,响应必须包含在该 schema 版本中引入的属性(LastResetTime、BootProgress)。宣传 v1.13 而没有这些字段是 Redfish Interop Validator 失败。Phantom 版本标记使其成为编译期契约:

use std::marker::PhantomData;

// ──── Schema 版本标记 ────

pub struct V1_5;
pub struct V1_13;

// ──── 版本感知响应 ────

pub struct ComputerSystemResponse<V> {
    pub base: ComputerSystemBase,
    _version: PhantomData<V>,
}

pub struct ComputerSystemBase {
    pub id: String,
    pub name: String,
    pub uuid: String,
    pub power_state: PowerStateValue,
    pub status: ResourceStatus,
    pub manufacturer: Option<String>,
    pub serial_number: Option<String>,
    pub bios_version: Option<String>,
}

// 在所有版本上可用的方法:
impl<V> ComputerSystemResponse<V> {
    pub fn base_json(&self) -> serde_json::Value {
        serde_json::json!({
            "Id": self.base.id,
            "Name": self.base.name,
            "UUID": self.base.uuid,
            "PowerState": self.base.power_state,
            "Status": self.base.status,
        })
    }
}

// ──── v1.13 特定字段 ────

/// 上次系统重置的日期和时间。
pub struct LastResetTime(pub String);

/// 启动进度信息。
pub struct BootProgress {
    pub last_state: String,
    pub last_state_time: String,
}

impl ComputerSystemResponse<V1_13> {
    /// LastResetTime — 在 v1.13+ 中 REQUIRED。
    /// 此方法仅存在于 V1_13。如果 BMC 宣传 v1.13
    /// 而处理程序不调用这个,字段缺失。
    pub fn last_reset_time(&self) -> LastResetTime {
        // 从 RTC 或启动时间戳寄存器读取
        LastResetTime("2026-03-16T08:30:00Z".to_string())
    }

    /// BootProgress — 在 v1.13+ 中 REQUIRED。
    pub fn boot_progress(&self) -> BootProgress {
        BootProgress {
            last_state: "OSRunning".to_string(),
            last_state_time: "2026-03-16T08:32:00Z".to_string(),
        }
    }

    /// 构建完整的 v1.13 JSON 响应,包括版本特定字段。
    pub fn to_json(&self) -> serde_json::Value {
        let mut obj = self.base_json();
        obj["@odata.type"] =
            serde_json::json!("#ComputerSystem.v1_13_0.ComputerSystem");

        let reset_time = self.last_reset_time();
        obj["LastResetTime"] = serde_json::json!(reset_time.0);

        let boot = self.boot_progress();
        obj["BootProgress"] = serde_json::json!({
            "LastState": boot.last_state,
            "LastStateTime": boot.last_state_time,
        });

        obj
    }
}

impl ComputerSystemResponse<V1_5> {
    /// v1.5 JSON — 没有 LastResetTime,没有 BootProgress。
    pub fn to_json(&self) -> serde_json::Value {
        let mut obj = self.base_json();
        obj["@odata.type"] =
            serde_json::json!("#ComputerSystem.v1_5_0.ComputerSystem");
        obj
    }

    // last_reset_time() 不存在这里。
    // 调用它 → 编译错误:
    //   let resp: ComputerSystemResponse<V1_5> = ...;
    //   resp.last_reset_time();
    //   ❌ 错误:method `last_reset_time` not found for
    //            `ComputerSystemResponse<V1_5>`
}

消除的 bug 类别: schema 版本不匹配。如果 BMC 配置为宣传 v1.13,使用 ComputerSystemResponse<V1_13>,编译器确保生成每个 v1.13 必需的字段。降级到 v1.5?更改类型参数——v1.13 方法消失,没有死字段泄漏到响应中。


第 6 节 — 类型化 Action 分发(ch02 反向)

在 ch02 中,类型化命令模式在客户端绑定Request → Response。在服务器端,相同的模式验证入站 action 负载并类型安全地分发它们——反向方向。

use serde::Deserialize;

// ──── Action Trait(ch02 的 IpmiCmd trait 镜像)────

/// Redfish action:框架从 POST 体反序列化 Params,
/// 然后调用 execute()。如果 JSON 不匹配 Params,反序列化
/// 失败——execute() 从不用坏输入调用。
pub trait RedfishAction {
    /// 预期的 JSON 体结构。
    type Params: serde::de::DeserializeOwned;
    /// 执行 action 的结果。
    type Result: serde::Serialize;

    fn execute(&self, params: Self::Params) -> Result<Self::Result, RedfishError>;
}

#[derive(Debug)]
pub enum RedfishError {
    InvalidPayload(String),
    ActionFailed(String),
}

// ──── ComputerSystem.Reset ────

pub struct ComputerSystemReset;

#[derive(Debug, Deserialize)]
pub enum ResetType {
    On,
    ForceOff,
    GracefulShutdown,
    GracefulRestart,
    ForceRestart,
    ForceOn,
    PushPowerButton,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetParams {
    pub reset_type: ResetType,
}

impl RedfishAction for ComputerSystemReset {
    type Params = ResetParams;
    type Result = ();

    fn execute(&self, params: ResetParams) -> Result<(), RedfishError> {
        match params.reset_type {
            ResetType::GracefulShutdown => {
                // 发送 ACPI 关机到主机
                println!("Initiating ACPI shutdown");
                Ok(())
            }
            ResetType::ForceOff => {
                // 断言关机到主机
                println!("Forcing power off");
                Ok(())
            }
            ResetType::On | ResetType::ForceOn => {
                println!("Powering on");
                Ok(())
            }
            ResetType::GracefulRestart => {
                println!("ACPI restart");
                Ok(())
            }
            ResetType::ForceRestart => {
                println!("Forced restart");
                Ok(())
            }
            ResetType::PushPowerButton => {
                println!("Simulating power button press");
                Ok(())
            }
            // 穷举——编译器捕获缺失的变体
        }
    }
}

// ──── Manager.ResetToDefaults ────

pub struct ManagerResetToDefaults;

#[derive(Debug, Deserialize)]
pub enum ResetToDefaultsType {
    ResetAll,
    PreserveNetworkAndUsers,
    PreserveNetwork,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetToDefaultsParams {
    pub reset_to_defaults_type: ResetToDefaultsType,
}

impl RedfishAction for ManagerResetToDefaults {
    type Params = ResetToDefaultsParams;
    type Result = ();

    fn execute(&self, params: ResetToDefaultsParams) -> Result<(), RedfishError> {
        match params.reset_to_defaults_type {
            ResetToDefaultsType::ResetAll => {
                println!("Full factory reset");
                Ok(())
            }
            ResetToDefaultsType::PreserveNetworkAndUsers => {
                println!("Reset preserving network + users");
                Ok(())
            }
            ResetToDefaultsType::PreserveNetwork => {
                println!("Reset preserving network config");
                Ok(())
            }
        }
    }
}

// ──── 通用 Action 分发器 ────

fn dispatch_action<A: RedfishAction>(
    action: &A,
    raw_body: &str,
) -> Result<A::Result, RedfishError> {
    // 反序列化验证负载结构。
    // 如果 JSON 不匹配 A::Params,这失败
    // execute() 从不用坏的输入调用。
    let params: A::Params = serde_json::from_str(raw_body)
        .map_err(|e| RedfishError::InvalidPayload(e.to_string()))?;

    action.execute(params)
}

// ── 使用 ──

fn handle_reset_action(body: &str) -> Result<(), RedfishError> {
    // 类型安全:ResetParams 在 execute() 之前由 serde 验证
    dispatch_action(&ComputerSystemReset, body)?;
    Ok(())

    // 无效 JSON: {"ResetType": "Explode"}
    // → serde 错误:"unknown variant `Explode`"
    // → execute() 从不调用

    // 缺失字段:{}
    // → serde 错误:"missing field `ResetType`"
    // → execute() 从不调用
}

消除的 bug 类别:

  • 无效 action 负载: serde 拒绝未知的枚举变体和缺失字段,在 execute() 调用之前。没有手动if (body["ResetType"] == ...) 链。
  • 缺失变体处理: match params.reset_type 是穷举的——添加新的 ResetType 变体强制每个 action 处理程序更新。
  • 类型混淆: ComputerSystemReset 期望 ResetParams;ManagerResetToDefaults 期望ResetToDefaultsParams。trait 系统防止传递一个 action 的参数到另一个 action 的处理程序。

第 7 节 — 整合在一起:GET 处理程序

这是将所有六个部分组合成单个 schema 合规响应的完整处理程序:

/// 完整的 GET /redfish/v1/Systems/1 处理程序。
///
/// 每个必需字段由构建器 type-state 强制执行。
/// 每个数据源由可用性令牌控制。
/// 每个单位锁定到它的量纲类型。
/// 每个健康输入馈送到类型化汇总。
fn handle_get_computer_system(
    smbios: &Option<(SmbiosReady, SmbiosTables)>,
    sensors: &Option<(SensorsReady, SensorCache)>,
    sel: &Option<(SelReady, TypedSelSummary)>,
    power_state: PowerStateValue,
    bios_version: Option<String>,
) -> serde_json::Value {
    // ── 1. 健康汇总(第 4 节)────
    // 从传感器 + SEL 折叠健康到单个类型化状态
    let health = compute_system_health(
        sensors.as_ref(),
        sel.as_ref(),
    );

    // ── 2. 构建器 type-state(第 1 节)────
    let builder = ComputerSystemBuilder::new()
        .power_state(power_state)
        .status(health);

    // ── 3. 源可用性令牌(第 2 节)────
    let builder = match smbios {
        Some((proof, tables)) => {
            // SMBIOS 可用——从硬件填充
            populate_from_smbios(builder, proof, tables)
        }
        None => {
            // SMBIOS 不可用——安全默认值
            populate_smbios_fallback(builder)
        }
    };

    // ── 4. 来自传感器的可选丰富化(第 3 节)────
    let builder = if let Some((_proof, cache)) = sensors {
        builder
            .processor_summary(ProcessorSummary {
                count: 2,
                status: ResourceStatus {
                    state: StatusState::Enabled,
                    health: if cache.cpu_temp < Celsius(95.0) {
                        HealthValue::OK
                    } else {
                        HealthValue::Critical
                    },
                    health_rollup: None,
                },
            })
    } else {
        builder
    };

    let builder = match bios_version {
        Some(v) => builder.bios_version(v),
        None => builder,
    };

    // ── 5. 构建(第 1 节)────
    // .build() 可用因为两条路径(SMBIOS 存在/不存在)
    // 为 Name 和 UUID 产生 HasField。编译器验证了这个。
    builder.build("1")
}

// ──── 服务器启动 ────

fn main() {
    // 初始化所有数据源——每个返回一个可用性令牌
    let smbios = init_smbios();
    let sensors = init_sensors();
    let sel = init_sel();

    // 模拟处理程序调用
    let response = handle_get_computer_system(
        &smbios,
        &sensors,
        &sel,
        PowerStateValue::On,
        Some("2.10.1".into()),
    );

    // 注意:.unwrap() 用于简洁——在生产中处理错误。
    println!("{}", serde_json::to_string_pretty(&response).unwrap());
}

预期输出:

{
  "@odata.id": "/redfish/v1/Systems/1",
  "@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
  "Id": "1",
  "Name": "PowerEdge R750",
  "UUID": "4c4c4544-004d-5610-804c-b2c04f435031",
  "PowerState": "On",
  "Status": {
    "State": "Enabled",
    "Health": "OK",
    "HealthRollup": "OK"
  },
  "Manufacturer": "Dell Inc.",
  "SerialNumber": "SVC1234567",
  "BiosVersion": "2.10.1",
  "ProcessorSummary": {
    "Count": 2,
    "Status": {
      "State": "Enabled",
      "Health": "OK"
    }
  }
}

编译器证明的内容(服务端)

#Bug 类别如何防止模式(章节)
1响应中缺失必需字段.build() 要求所有 type-state 标记是HasField构建器 type-state(§1)
2调用失败的子系统源可用性令牌控制数据访问Capability tokens(§2)
3没有不可用来源的回退两个 match 臂(存在/不存在)必须产生HasFieldType-state + 穷举匹配(§2)
4JSON 字段中错误的单位reading_celsius: Celsius ≠ Rpm ≠ Watts量纲类型(§3)
5不完整的健康汇总compute_system_health 采用显式来源引用;SEL 通过 ch07 的 TypedSelSummary 提供每子系统HealthValue类型化函数签名 + 穷举匹配(§4)
6Schema 版本不匹配ComputerSystemResponse<V1_13> 有 last_reset_time();V1_5 没有Phantom types(§5)
7无效 action 负载被接受serde 在 execute() 之前拒绝未知/缺失字段类型化 action 分发(§6)
8缺失 action 变体处理match params.reset_type 是穷举的枚举穷举性(§6)
9错误的 action 参数到错误的处理程序RedfishAction::Params 是关联类型类型化命令反向(§6)

总运行时开销:零。 构建器标记、可用性令牌、phantom 版本类型和量纲 newtype 都编译消失。产生的 JSON 与手写的 C 版本相同——减去九类 bug。


镜像:客户端 vs. 服务端模式映射

关注点客户端(ch17)服务端(本章)
边界方向入站:JSON → 类型化值出站:类型化值 → JSON
核心原则“解析,不要验证”“构造,不要序列化”
字段完整性TryFrom 验证必需字段存在构建器 type-state 在必需字段上提供 .build()
单位安全Celsius ≠ Rpm 当读取时Celsius ≠ Rpm 当写入时
特权/可用性Capability 令牌控制请求可用性令牌控制数据源访问
数据源单个来源(BMC)多个来源(SMBIOS、传感器、SEL、PCIe、…)
Schema 版本Phantom 类型防止访问不支持的字段Phantom 类型强制执行提供版本必需字段
Actions客户端发送类型化 action POST服务器通过 RedfishAction trait 验证 + 分发
Health读取并信任 Status.Health通过类型化汇总计算 Status.Health
失败传播一个坏解析 → 一个客户端错误一个坏序列化 → 每个客户端看到错误数据

这两章形成一个完整的故事。第 17 章:“我消费的每个响应都经过类型检查。” 本章:“我生产的每个响应都经过类型检查。” 相同的模式在两个方向流动——类型系统不知道或不关心你在哪一端。

关键要点

  1. “构造,不要序列化” 是服务端的镜像“解析,不要验证“——使用构建器 type-state 所以 .build() 仅在所有必需字段存在时可用。
  2. 源可用性令牌证明初始化——来自 ch04 的相同 capability 令牌模式,重新用于证明数据源就绪。
  3. 量纲类型保护生产者和消费者——在ReadingCelsius 字段中放入Rpm 是编译错误,不是客户报告的 bug。
  4. 健康汇总是类型化 fold——HealthValue 上的 Ord 加上显式来源引用意味着编译器捕获“忘记包含 PSU 状态。“
  5. 类型级的 Schema 版本化——phantom 类型参数使版本特定字段在编译时出现和消失。
  6. Action 分发反向 ch02——serde 将负载反序列化为类型化的Params 结构体,对枚举变体的穷举匹配意味着添加新的ResetType 强制每个处理程序更新。
  7. 服务端 bug 传播到每个客户端——这就是为什么在生产者端的编译期正确性甚至比消费端更关键。

十四个实战技巧 🟡

你将学到: 十四个较小的构造正确性技术——从哨兵消除和密封 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 类别 ——增量采用它们;没有技巧需要重写你的架构。

练习题 🟡

你将学到: 在真实的硬件场景中动手练习正确构造模式——NVMe 管理命令、固件更新状态机、传感器流水线、PCIe phantom 类型、多协议健康检查和会话类型诊断协议。

交叉引用: ch02(练习 1)、ch05(练习 2)、ch06(练习 3)、ch09(练习 4)、ch10(练习 5)

练习题

练习 1:NVMe 管理命令(类型化命令)

为 NVMe 管理命令设计一个类型化命令接口:

  • Identify → IdentifyResponse(型号、序列号、固件版本)
  • GetLogPage → SmartLog(温度、可用备用空间、读取的数据单元数)
  • GetFeature → 特定功能的响应

要求:

  1. 命令类型决定响应类型
  2. 没有运行时调度——仅静态调度
  3. 添加一个 NamespaceId newtype,防止将命名空间 ID 与其他 u32 混淆

提示: 遵循 ch02 的 IpmiCmd trait 模式,但使用 NVMe 特定的常量。

示例解决方案(练习 1)
use std::io;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NamespaceId(pub u32);

#[derive(Debug, Clone, PartialEq)]
pub struct IdentifyResponse {
    pub model: String,
    pub serial: String,
    pub firmware_rev: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct SmartLog {
    pub temperature_kelvin: u16,
    pub available_spare_pct: u8,
    pub data_units_read: u64,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ArbitrationFeature {
    pub high_priority_weight: u8,
    pub medium_priority_weight: u8,
    pub low_priority_weight: u8,
}

/// 核心模式:关联类型为每个命令固定响应类型。
pub trait NvmeAdminCmd {
    type Response;
    fn opcode(&self) -> u8;
    fn nsid(&self) -> Option<NamespaceId>;
    fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}

pub struct Identify { pub nsid: NamespaceId }

impl NvmeAdminCmd for Identify {
    type Response = IdentifyResponse;
    fn opcode(&self) -> u8 { 0x06 }
    fn nsid(&self) -> Option<NamespaceId> { Some(self.nsid) }
    fn parse_response(&self, raw: &[u8]) -> io::Result<IdentifyResponse> {
        if raw.len() < 12 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "太短"));
        }
        Ok(IdentifyResponse {
            model: String::from_utf8_lossy(&raw[0..4]).trim().to_string(),
            serial: String::from_utf8_lossy(&raw[4..8]).trim().to_string(),
            firmware_rev: String::from_utf8_lossy(&raw[8..12]).trim().to_string(),
        })
    }
}

pub struct GetLogPage { pub log_id: u8 }

impl NvmeAdminCmd for GetLogPage {
    type Response = SmartLog;
    fn opcode(&self) -> u8 { 0x02 }
    fn nsid(&self) -> Option<NamespaceId> { None }
    fn parse_response(&self, raw: &[u8]) -> io::Result<SmartLog> {
        if raw.len() < 11 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "太短"));
        }
        Ok(SmartLog {
            temperature_kelvin: u16::from_le_bytes([raw[0], raw[1]]),
            available_spare_pct: raw[2],
            data_units_read: u64::from_le_bytes(raw[3..11].try_into().unwrap()),
        })
    }
}

pub struct GetFeature { pub feature_id: u8 }

impl NvmeAdminCmd for GetFeature {
    type Response = ArbitrationFeature;
    fn opcode(&self) -> u8 { 0x0A }
    fn nsid(&self) -> Option<NamespaceId> { None }
    fn parse_response(&self, raw: &[u8]) -> io::Result<ArbitrationFeature> {
        if raw.len() < 3 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "太短"));
        }
        Ok(ArbitrationFeature {
            high_priority_weight: raw[0],
            medium_priority_weight: raw[1],
            low_priority_weight: raw[2],
        })
    }
}

/// 静态调度——编译器为每个命令类型单态化。
pub struct NvmeController;

impl NvmeController {
    pub fn execute<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
        // 从 cmd.opcode()/cmd.nsid() 构建 SQE,
        // 提交到 SQ,等待 CQ,然后:
        let raw = self.submit_and_read(cmd.opcode())?;
        cmd.parse_response(&raw)
    }

    fn submit_and_read(&self, _opcode: u8) -> io::Result<Vec<u8>> {
        // 真实实现与 /dev/nvme0 通信
        Ok(vec![0; 512])
    }
}

关键点:

  • NamespaceId(u32) 防止将命名空间 ID 与任意 u32 值混淆。
  • NvmeAdminCmd::Response 是“类型索引“——execute() 正好返回 C::Response。
  • 完全静态调度:没有 Box<dyn …>,没有运行时向下转型。

练习 2:固件更新状态机(Type-State)

为 BMC 固件更新生命周期建模:

stateDiagram-v2
    [*] --> Idle
    Idle --> Uploading : begin_upload()
    Uploading --> Uploading : send_chunk(data)
    Uploading --> Verifying : finish_upload()
    Uploading --> Idle : abort()
    Verifying --> Applying : verify() ✅ + VerifiedImage token
    Verifying --> Idle : verify() ❌ or abort()
    Applying --> Rebooting : apply(token)
    Rebooting --> Complete : reboot_complete()
    Complete --> [*]

    note right of Applying : 没有 abort() —— 不可逆
    note right of Verifying : VerifiedImage 是证明令牌

要求:

  1. 每个状态是一个不同的类型
  2. 上传只能从 Idle 开始
  3. 验证需要上传完成
  4. 应用只能在成功验证后发生——接受一个 VerifiedImage 证明令牌
  5. 重启是应用后的唯一选项
  6. 添加一个 abort() 方法,在 Uploading 和 Verifying 状态可用(但在 Applying 不可用——太晚了)

提示: 结合 type-state(ch05)与 capability 令牌(ch04)。

示例解决方案(练习 2)
// --- 状态类型 ---
// 设计选择:这里我们将状态内联存储(`_state: S`),而不是使用
// `PhantomData<S>`(ch05 的方法)。这允许状态携带数据——
// 例如 `Uploading { bytes_sent: usize }` 跟踪进度。使用 `PhantomData`
// 当状态是纯标记(零大小)时;使用内联存储当
// 状态携带有意义的运行时数据时。
pub struct Idle;
pub struct Uploading { bytes_sent: usize }  // 非 ZST——携带进度数据
pub struct Verifying;
pub struct Applying;
pub struct Rebooting;
pub struct Complete;

/// 证明令牌:只能在 verify() 内部构造。
pub struct VerifiedImage { _private: () }

pub struct FwUpdate<S> {
    bmc_addr: String,
    _state: S,
}

impl FwUpdate<Idle> {
    pub fn new(bmc_addr: &str) -> Self {
        FwUpdate { bmc_addr: bmc_addr.to_string(), _state: Idle }
    }
    pub fn begin_upload(self) -> FwUpdate<Uploading> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Uploading { bytes_sent: 0 } }
    }
}

impl FwUpdate<Uploading> {
    pub fn send_chunk(mut self, chunk: &[u8]) -> Self {
        self._state.bytes_sent += chunk.len();
        self
    }
    pub fn finish_upload(self) -> FwUpdate<Verifying> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Verifying }
    }
    /// Abort 在上传期间可用——返回 Idle。
    pub fn abort(self) -> FwUpdate<Idle> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
    }
}

impl FwUpdate<Verifying> {
    /// 成功时,返回下一个状态和 VerifiedImage 证明令牌。
    pub fn verify(self) -> Result<(FwUpdate<Applying>, VerifiedImage), FwUpdate<Idle>> {
        // 真实实现:检查 CRC、签名、兼容性
        let token = VerifiedImage { _private: () };
        Ok((
            FwUpdate { bmc_addr: self.bmc_addr, _state: Applying },
            token,
        ))
    }
    /// Abort 在验证期间可用。
    pub fn abort(self) -> FwUpdate<Idle> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
    }
}

impl FwUpdate<Applying> {
    /// 消费 VerifiedImage 证明——不能在没有验证的情况下应用。
    /// 注意:这里没有 abort() 方法——一旦开始刷写,就太危险了。
    pub fn apply(self, _proof: VerifiedImage) -> FwUpdate<Rebooting> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Rebooting }
    }
}

impl FwUpdate<Rebooting> {
    pub fn wait_for_reboot(self) -> FwUpdate<Complete> {
        FwUpdate { bmc_addr: self.bmc_addr, _state: Complete }
    }
}

impl FwUpdate<Complete> {
    pub fn version(&self) -> &str { "2.1.0" }
}

// 用法:
// let fw = FwUpdate::new("192.168.1.100")
//     .begin_upload()
//     .send_chunk(b"image_data")
//     .finish_upload();
// let (fw, proof) = fw.verify().map_err(|_| "verify failed")?;
// let fw = fw.apply(proof).wait_for_reboot();
// println!("新版本:{}", fw.version());

关键点:

  • abort() 仅存在于 FwUpdate<Uploading> 和 FwUpdate<Verifying> 上——在 FwUpdate<Applying> 上调用它是编译错误,而不是运行时检查。
  • VerifiedImage 有私有字段,所以只有 verify() 能创建它。
  • apply() 消费证明令牌——你不能跳过验证。

练习 3:传感器读数流水线(量纲分析)

构建一个完整的传感器流水线:

  1. 定义 newtype:RawAdc、Celsius、Fahrenheit、Volts、Millivolts、Watts
  2. 实现 From<Celsius> for Fahrenheit 和反向转换
  3. 创建 impl Mul<Volts, Output=Watts> for Amperes(P = V × I)
  4. 构建一个 Threshold<T> 泛型检查器
  5. 编写流水线:ADC → 校准 → 阈值检查 → 结果

编译器应该拒绝:将 Celsius 与 Volts 比较、将 Watts 与 Rpm 相加、在期望 Volts 的地方传递 Millivolts。

示例解决方案(练习 3)
use std::ops::{Add, Sub, Mul};

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RawAdc(pub u16);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);

// --- 安全转换 ---
impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }
}
impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self { Celsius((f.0 - 32.0) * 5.0 / 9.0) }
}
impl From<Millivolts> for Volts {
    fn from(mv: Millivolts) -> Self { Volts(mv.0 / 1000.0) }
}
impl From<Volts> for Millivolts {
    fn from(v: Volts) -> Self { Millivolts(v.0 * 1000.0) }
}

// --- 相同单位类型的算术 ---
// 注意:添加绝对温度(25°C + 30°C)在物理上
// 是有问题的——参见 ch06 关于 ΔT newtype 的讨论以获取更严格的方法。
// 这里为了练习保持简单。
impl Add for Celsius {
    type Output = Celsius;
    fn add(self, rhs: Self) -> Celsius { Celsius(self.0 + rhs.0) }
}
impl Sub for Celsius {
    type Output = Celsius;
    fn sub(self, rhs: Self) -> Celsius { Celsius(self.0 - rhs.0) }
}

// P = V × I  (跨单位乘法)
impl Mul<Amperes> for Volts {
    type Output = Watts;
    fn mul(self, rhs: Amperes) -> Watts { Watts(self.0 * rhs.0) }
}

// --- 泛型阈值检查器 ---
// 练习 3 扩展了 ch06 的 Threshold,带有泛型 ThresholdResult<T>
// 携带触发的读数——这是 ch06 更简单的
// ThresholdResult { Normal, Warning, Critical } 枚举的演进。
pub enum ThresholdResult<T> {
    Normal(T),
    Warning(T),
    Critical(T),
}

pub struct Threshold<T> {
    pub warning: T,
    pub critical: T,
}

// 泛型 impl——适用于任何支持 PartialOrd 的单位类型。
impl<T: PartialOrd + Copy> Threshold<T> {
    pub fn check(&self, reading: T) -> ThresholdResult<T> {
        if reading >= self.critical {
            ThresholdResult::Critical(reading)
        } else if reading >= self.warning {
            ThresholdResult::Warning(reading)
        } else {
            ThresholdResult::Normal(reading)
        }
    }
}
// 现在 `Threshold<Rpm>`、`Threshold<Volts>` 等都自动工作。

// --- 流水线:ADC → 校准 → 阈值 → 结果 ---
pub struct CalibrationParams {
    pub scale: f64,  // ADC 计数/°C
    pub offset: f64, // ADC 0 时的°C
}

pub fn calibrate(raw: RawAdc, params: &CalibrationParams) -> Celsius {
    Celsius(raw.0 as f64 / params.scale + params.offset)
}

pub fn sensor_pipeline(
    raw: RawAdc,
    params: &CalibrationParams,
    threshold: &Threshold<Celsius>,
) -> ThresholdResult<Celsius> {
    let temp = calibrate(raw, params);
    threshold.check(temp)
}

// 编译时安全——这些将**无法编译**:
// let _ = Celsius(25.0) + Volts(12.0);   // 错误:类型不匹配
// let _: Millivolts = Volts(1.0);         // 错误:没有隐式强制转换
// let _ = Watts(100.0) + Rpm(3000);       // 错误:类型不匹配

关键点:

  • 每个物理单位是一个不同的类型——不会意外混淆。
  • Mul<Amperes> for Volts 生成 Watts,将 P = V × I 编码到类型系统中。
  • 对相关单位(mV ↔ V、°C ↔ °F)的显式 From 转换。
  • Threshold<Celsius> 只接受 Celsius——不能意外地阈值检查 RPM。

练习 4:PCIe Capability 遍历(Phantom 类型 + 验证边界)

为 PCIe capability 链表建模:

  1. RawCapability —— 来自配置空间的未验证字节
  2. ValidCapability —— 已解析和验证(通过 TryFrom)
  3. 每种 capability 类型(MSI、MSI-X、PCIe Express、电源管理)有自己的 phantom 类型寄存器布局
  4. 遍历列表返回 ValidCapability 值的迭代器

提示: 结合验证边界(ch07)与 phantom 类型(ch09)。

示例解决方案(练习 4)
use std::marker::PhantomData;

// --- Phantom 标记用于 capability 类型 ---
pub struct Msi;
pub struct MsiX;
pub struct PciExpress;
pub struct PowerMgmt;

// PCI capability ID 来自规范
const CAP_ID_PM:   u8 = 0x01;
const CAP_ID_MSI:  u8 = 0x05;
const CAP_ID_PCIE: u8 = 0x10;
const CAP_ID_MSIX: u8 = 0x11;

/// 未验证的字节——可能是垃圾数据。
#[derive(Debug)]
pub struct RawCapability {
    pub id: u8,
    pub next_ptr: u8,
    pub data: Vec<u8>,
}

/// 已验证和类型标记的 capability。
#[derive(Debug)]
pub struct ValidCapability<Kind> {
    id: u8,
    next_ptr: u8,
    data: Vec<u8>,
    _kind: PhantomData<Kind>,
}

// --- TryFrom:解析而非验证边界 ---
impl TryFrom<RawCapability> for ValidCapability<PowerMgmt> {
    type Error = &'static str;
    fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
        if raw.id != CAP_ID_PM { return Err("不是 PM capability"); }
        if raw.data.len() < 2 { return Err("PM 数据太短"); }
        Ok(ValidCapability {
            id: raw.id, next_ptr: raw.next_ptr,
            data: raw.data, _kind: PhantomData,
        })
    }
}

impl TryFrom<RawCapability> for ValidCapability<Msi> {
    type Error = &'static str;
    fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
        if raw.id != CAP_ID_MSI { return Err("不是 MSI capability"); }
        if raw.data.len() < 6 { return Err("MSI 数据太短"); }
        Ok(ValidCapability {
            id: raw.id, next_ptr: raw.next_ptr,
            data: raw.data, _kind: PhantomData,
        })
    }
}

// (类似的 TryFrom 实现用于 MsiX、PciExpress——为简洁省略)

// --- 类型安全访问器:仅可用于正确的 capability ---
impl ValidCapability<PowerMgmt> {
    pub fn pm_control(&self) -> u16 {
        u16::from_le_bytes([self.data[0], self.data[1]])
    }
}

impl ValidCapability<Msi> {
    pub fn message_control(&self) -> u16 {
        u16::from_le_bytes([self.data[0], self.data[1]])
    }
    pub fn vectors_requested(&self) -> u32 {
        1 << ((self.message_control() >> 1) & 0x07)
    }
}

impl ValidCapability<MsiX> {
    pub fn table_size(&self) -> u16 {
        (u16::from_le_bytes([self.data[0], self.data[1]]) & 0x07FF) + 1
    }
}

// --- Capability 遍历器:遍历链表 ---
pub struct CapabilityWalker<'a> {
    config_space: &'a [u8],
    next_ptr: u8,
}

impl<'a> CapabilityWalker<'a> {
    pub fn new(config_space: &'a [u8]) -> Self {
        // Capability 指针位于 PCI 配置空间偏移 0x34
        let first_ptr = if config_space.len() > 0x34 {
            config_space[0x34]
        } else { 0 };
        CapabilityWalker { config_space, next_ptr: first_ptr }
    }
}

impl<'a> Iterator for CapabilityWalker<'a> {
    type Item = RawCapability;
    fn next(&mut self) -> Option<RawCapability> {
        if self.next_ptr == 0 { return None; }
        let off = self.next_ptr as usize;
        if off + 2 > self.config_space.len() { return None; }
        let id = self.config_space[off];
        let next = self.config_space[off + 1];
        let end = if next > 0 { next as usize } else {
            (off + 16).min(self.config_space.len())
        };
        let data = self.config_space[off + 2..end].to_vec();
        self.next_ptr = next;
        Some(RawCapability { id, next_ptr: next, data })
    }
}

// 用法:
// for raw_cap in CapabilityWalker::new(&config_space) {
//     if let Ok(pm) = ValidCapability::<PowerMgmt>::try_from(raw_cap) {
//         println!("PM control: 0x{:04X}", pm.pm_control());
//     }
// }

关键点:

  • RawCapability → ValidCapability<Kind> 是解析而非验证边界。
  • pm_control() 仅存在于 ValidCapability<PowerMgmt> 上——在 MSI capability 上调用它是编译错误。
  • CapabilityWalker 迭代器生成原始 capability;调用者用 TryFrom 验证他们关心的那些。

练习 5:多协议健康检查(Capability 混合)

创建一个健康检查框架:

  1. 定义原料 trait:HasIpmi、HasRedfish、HasNvmeCli、HasGpio
  2. 创建混合 trait:
    • ThermalHealthMixin(需要 HasIpmi + HasGpio)—— 读取温度,检查警报
    • StorageHealthMixin(需要 HasNvmeCli)—— SMART 数据检查
    • BmcHealthMixin(需要 HasIpmi + HasRedfish)—— 交叉验证 BMC 数据
  3. 构建一个 FullPlatformController 实现所有原料 trait
  4. 构建一个 StorageOnlyController 只实现 HasNvmeCli
  5. 验证 StorageOnlyController 获得 StorageHealthMixin 但不获得其他混合
示例解决方案(练习 5)
// --- 原料 trait ---
pub trait HasIpmi {
    fn ipmi_read_sensor(&self, id: u8) -> f64;
}
pub trait HasRedfish {
    fn redfish_get(&self, path: &str) -> String;
}
pub trait HasNvmeCli {
    fn nvme_smart_log(&self, dev: &str) -> SmartData;
}
pub trait HasGpio {
    fn gpio_read_alert(&self, pin: u8) -> bool;
}

pub struct SmartData {
    pub temperature_kelvin: u16,
    pub spare_pct: u8,
}

// --- 混合 trait 带有 blanket impl ---
pub trait ThermalHealthMixin: HasIpmi + HasGpio {
    fn thermal_check(&self) -> ThermalStatus {
        let temp = self.ipmi_read_sensor(0x01);
        let alert = self.gpio_read_alert(12);
        ThermalStatus { temperature: temp, alert_active: alert }
    }
}
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}

pub trait StorageHealthMixin: HasNvmeCli {
    fn storage_check(&self) -> StorageStatus {
        let smart = self.nvme_smart_log("/dev/nvme0");
        StorageStatus {
            temperature_ok: smart.temperature_kelvin < 343, // 70 °C
            spare_ok: smart.spare_pct > 10,
        }
    }
}
impl<T: HasNvmeCli> StorageHealthMixin for T {}

pub trait BmcHealthMixin: HasIpmi + HasRedfish {
    fn bmc_health(&self) -> BmcStatus {
        let ipmi_temp = self.ipmi_read_sensor(0x01);
        let rf_temp = self.redfish_get("/Thermal/Temperatures/0");
        BmcStatus { ipmi_temp, redfish_temp: rf_temp, consistent: true }
    }
}
impl<T: HasIpmi + HasRedfish> BmcHealthMixin for T {}

pub struct ThermalStatus { pub temperature: f64, pub alert_active: bool }
pub struct StorageStatus { pub temperature_ok: bool, pub spare_ok: bool }
pub struct BmcStatus { pub ipmi_temp: f64, pub redfish_temp: String, pub consistent: bool }

// --- 完整平台:所有原料 → 所有三个混合免费获得 ---
pub struct FullPlatformController;

impl HasIpmi for FullPlatformController {
    fn ipmi_read_sensor(&self, _id: u8) -> f64 { 42.0 }
}
impl HasRedfish for FullPlatformController {
    fn redfish_get(&self, _path: &str) -> String { "42.0".into() }
}
impl HasNvmeCli for FullPlatformController {
    fn nvme_smart_log(&self, _dev: &str) -> SmartData {
        SmartData { temperature_kelvin: 310, spare_pct: 95 }
    }
}
impl HasGpio for FullPlatformController {
    fn gpio_read_alert(&self, _pin: u8) -> bool { false }
}

// --- 仅存储:只有 HasNvmeCli → 只有 StorageHealthMixin ---
pub struct StorageOnlyController;

impl HasNvmeCli for StorageOnlyController {
    fn nvme_smart_log(&self, _dev: &str) -> SmartData {
        SmartData { temperature_kelvin: 315, spare_pct: 80 }
    }
}

// StorageOnlyController 自动获得 storage_check()。
// 在它上面调用 thermal_check() 或 bmc_health() 是**编译错误**。

关键点:

  • Blanket impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {} —— 任何实现两个原料的类型自动获得混合。
  • StorageOnlyController 只实现 HasNvmeCli,所以编译器授予它 StorageHealthMixin 但拒绝 thermal_check() 和 bmc_health() —— 不需要运行时检查。
  • 添加新混合(例如 NetworkHealthMixin: HasRedfish + HasGpio)是一个 trait + 一个 blanket impl——现有控制器如果符合条件就自动获得它。

练习 6:会话类型诊断协议(一次性 + Type-State)

设计一个带有一次性测试执行令牌的诊断会话:

  1. DiagSession 从 Setup 状态开始
  2. 转换到 Running 状态——发放 N 个执行令牌(每个测试用例一个)
  3. 每个 TestToken 在测试运行时被消费——防止运行相同的测试两次
  4. 在所有令牌被消费后,转换到 Complete 状态
  5. 生成报告(仅在 Complete 状态)

高级: 使用 const 泛型 N 在类型级别跟踪剩余多少测试。

示例解决方案(练习 6)
// --- 状态类型 ---
pub struct Setup;
pub struct Running;
pub struct Complete;

/// 一次性测试令牌。不可 Clone,不可 Copy——在使用时被消费。
pub struct TestToken {
    test_name: String,
}

#[derive(Debug)]
pub struct TestResult {
    pub test_name: String,
    pub passed: bool,
}

pub struct DiagSession<S> {
    name: String,
    results: Vec<TestResult>,
    _state: S,
}

impl DiagSession<Setup> {
    pub fn new(name: &str) -> Self {
        DiagSession {
            name: name.to_string(),
            results: Vec::new(),
            _state: Setup,
        }
    }

    /// 转换到 Running——为每个测试用例发放一个令牌。
    pub fn start(self, test_names: &[&str]) -> (DiagSession<Running>, Vec<TestToken>) {
        let tokens = test_names.iter()
            .map(|n| TestToken { test_name: n.to_string() })
            .collect();
        (
            DiagSession {
                name: self.name,
                results: Vec::new(),
                _state: Running,
            },
            tokens,
        )
    }
}

impl DiagSession<Running> {
    /// 消费一个令牌来运行一个测试。move 防止重复运行。
    pub fn run_test(mut self, token: TestToken) -> Self {
        let passed = true; // 真实代码在这里运行实际诊断
        self.results.push(TestResult {
            test_name: token.test_name,
            passed,
        });
        self
    }

    /// 转换到 Complete。
    ///
    /// **注意:** 此解决方案**不**强制所有令牌已被消费——`finish()` 可以在
    /// 仍有令牌剩余时调用。令牌将被简单地丢弃(它们不是 `#[must_use]`)。
    /// 对于完整的编译时强制,使用下面的"高级"变体,
    /// 其中 `finish()` 仅在 `DiagSession<Running, 0>` 上可用。
    pub fn finish(self) -> DiagSession<Complete> {
        DiagSession {
            name: self.name,
            results: self.results,
            _state: Complete,
        }
    }
}

impl DiagSession<Complete> {
    /// 报告**仅**在 Complete 状态可用。
    pub fn report(&self) -> String {
        let total = self.results.len();
        let passed = self.results.iter().filter(|r| r.passed).count();
        format!("{}: {}/{} 通过", self.name, passed, total)
    }
}

// 用法:
// let session = DiagSession::new("GPU 压力测试");
// let (mut session, tokens) = session.start(&["vram", "compute", "thermal"]);
// for token in tokens {
//     session = session.run_test(token);
// }
// let session = session.finish();
// println!("{}", session.report());  // "GPU 压力测试:3/3 通过"
//
// // 这些将**无法编译**:
// // session.run_test(used_token);  →  错误:使用已移动的值
// // running_session.report();      →  错误:没有方法 `report` 在 DiagSession<Running> 上

关键点:

  • TestToken 不是 Clone 或 Copy —— 通过 run_test(token) 消费它时 move 它,所以重新运行相同的测试是编译错误。
  • report() 仅存在于 DiagSession<Complete> 上——在运行中途调用它是不可能的。
  • 高级变体将使用 DiagSession<Running, N> 与 const 泛型,其中 run_test 返回 DiagSession<Running, {N-1}> 且 finish 仅在 DiagSession<Running, 0> 上可用——这确保在结束前所有令牌都被消费。

关键要点

  1. 用真实协议练习 —— NVMe、固件更新、传感器流水线、PCIe 都是这些模式的真实世界目标。
  2. 每个练习映射到核心章节 —— 使用交叉引用来在尝试之前复习模式。
  3. 解决方案使用可展开的详情 —— 在揭示解决方案之前尝试每个练习。
  4. 在练习 5 中组合模式 —— 多协议健康检查结合了类型化命令、量纲类型和验证边界。
  5. 会话类型(练习 6)是前沿 —— 它们强制跨通道的消息排序,将 type-state 扩展到分布式系统。

参考卡片

14+ 种正确构造模式的快速参考,包含选择流程图、模式目录、组合规则、crate 映射和类型即保证速查表。

交叉引用: 每一章——这是整本书的查找表。

快速参考:正确构造模式

模式选择指南

bug 如果被遗漏是否是灾难性的?
├── 是 → 能编码到类型中吗?
│         ├── 是 → 使用正确构造
│         └── 否  → 运行时检查 + 广泛测试
└── 否  → 运行时检查没问题

模式目录

#模式关键 Trait/类型防止运行时成本章节
1类型化命令trait IpmiCmd { type Response; }错误的响应类型零ch02
2一次性类型struct Nonce(非 Clone/Copy)Nonce/密钥重用零ch03
3Capability 令牌struct AdminToken { _private: () }未授权访问零ch04
4Type-StateSession<Active>协议违规零ch05
5量纲类型struct Celsius(f64)单位混淆零ch06
6验证边界struct ValidFru(通过 TryFrom)使用未验证数据解析一次ch07
7Capability 混合trait FanDiagMixin: HasSpi + HasI2c缺少总线访问零ch08
8Phantom 类型Register<Width16>宽度/方向不匹配零ch09
9Sentinel → OptionOption<u8>(而非 0xFF)Sentinel 作为值的 bug零ch11
10Sealed traittrait Cmd: private::Sealed不健全的外部实现零ch11
11非穷举枚举#[non_exhaustive] enum Sku静默的 match 回退零ch11
12Typestate 构建器DerBuilder<Set, Missing>不完整构造零ch11
13FromStr 验证impl FromStr for DiagLevel未验证的字符串输入解析一次ch11
14Const 泛型大小RegisterBank<const N: usize>缓冲区大小不匹配零ch11
15安全 unsafe 包装MmioRegion::read_u32()未检查的 MMIO/FFI零ch11
16异步 Type-StateAsyncSession<Active>异步协议违规零ch11
17Const 断言SdrSensorId<const N: u8>无效的编译时 ID零ch11
18会话类型Chan<SendRequest>无序的通道操作零ch11
19Pin 自引用Pin<Box<StreamParser>>悬垂的结构体内指针零ch11
20RAII / Dropimpl Drop for Session任何退出路径上的资源泄漏零ch11
21错误类型层次#[derive(Error)] enum DiagError静默的错误吞没零ch11
22#[must_use]#[must_use] struct Token静默丢弃的值零ch11

组合规则

Capability 令牌 + Type-State = 授权的状態转换
类型化命令 + 量纲类型 = 物理类型的响应
验证边界 + Phantom 类型 = 已验证配置上的类型化寄存器访问
Capability 混合 + 类型化命令 = 感知总线的类型化操作
一次性类型 + Type-State = 消费即转换的协议
Sealed trait + 类型化命令 = 封闭、健全的命令集
Sentinel → Option + 验证边界 = 干净的解析一次流水线
Typestate 构建器 + Capability 令牌 = 构造完成的证明
FromStr + #[non_exhaustive] = 可演进的、快速失败的枚举解析
Const 泛型大小 + 验证边界 = 定型的、已验证的协议缓冲区
Safe unsafe 包装 + Phantom 类型 = 类型化的、安全的 MMIO 访问
异步 Type-State + Capability 令牌 = 授权的异步转换
会话类型 + 类型化命令 = 完全类型化的请求 - 响应通道
Pin + Type-State = 不能移动的自引用状态机
RAII (Drop) + Type-State = 依赖状态的清理保证
错误层次 + 验证边界 = 类型化的解析错误,带有穷举处理
#[must_use] + 一次性类型 = 难以忽略、难以重用的令牌

要避免的反模式

反模式为什么错误正确的替代方案
fn read_sensor() -> f64无单位——可能是°C、°F 或 RPMfn read_sensor() -> Celsius
fn encrypt(nonce: &[u8; 12])Nonce 可重用(借用)fn encrypt(nonce: Nonce)(move)
fn admin_op(is_admin: bool)调用者可以撒谎(true)fn admin_op(_: &AdminToken)
fn send(session: &Session)没有状态保证fn send(session: &Session<Active>)
fn process(data: &[u8])未验证fn process(data: &ValidFru)
在临时密钥上使用 Clone破坏一次性保证不要派生 Clone
let vendor_id: u16 = 0xFFFFSentinel 在内部携带let vendor_id: Option<u16> = None
fn route(level: &str) 带有回退拼写错误静默默认let level: DiagLevel = s.parse()?
Builder::new().finish() 没有字段构造了不完整的对象Typestate 构建器:finish() 需要 Set
let buf: Vec<u8> 用于固定大小的硬件缓冲区大小仅在运行时检查RegisterBank<4096>(const 泛型)
分散的原始 unsafe { ptr::read(...) }UB 风险,无法审计MmioRegion::read_u32() 安全包装
async fn transition(&mut self)可变借用不强制状态async fn transition(self) -> NextState
fn cleanup() 手动调用在提前返回/panic 时忘记impl Drop —— 编译器插入调用
fn op() -> Result<T, String>不透明的错误,没有变体匹配fn op() -> Result<T, DiagError> 枚举

映射到诊断代码库

模块适用模式
protocol_lib类型化命令、type-state 会话
thermal_diagCapability 混合、量纲类型
accel_diag验证边界、phantom 寄存器
network_diagType-State(链路训练)、capability 令牌
pci_topologyPhantom 类型(寄存器宽度)、已验证配置、sentinel → Option
event_handler一次性审计令牌、capability 令牌、FromStr(Component)
event_log验证边界(SEL 记录解析)
compute_diag量纲类型(温度、频率)
memory_diag验证边界(SPD 数据)、量纲类型
switch_diagType-State(端口枚举)、phantom 类型
config_loaderFromStr(DiagLevel、FaultStatus、DiagAction)
log_analyzer验证边界(CompiledPatterns)
diag_frameworkTypestate 构建器(DerBuilder)、会话类型(orchestrator↔worker)
topology_libConst 泛型寄存器银行、安全 MMIO 包装

类型即保证——快速映射

保证Rust 等价物示例
“此证明存在”一个类型AdminToken
“我有证明”该类型的值let tok = authenticate()?;
“A 意味着 B”函数 fn(A) -> Bfn activate(AdminToken) -> Session<Active>
“A 和 B 都”元组 (A, B) 或多参数fn op(a: &AdminToken, b: &LinkTrained)
“A 或 B”enum { A(A), B(B) } 或 Result<A, B>Result<Session<Active>, Error>
“总是真”()(单元类型)总是可构造
“不可能”!(never 类型)或 enum Void {}永远无法构造

测试类型级保证 🟡

你将学到: 如何测试无效代码无法编译(trybuild)、模糊测试验证边界(proptest)、验证 RAII 不变量,以及通过 cargo-show-asm 证明零成本抽象。

交叉引用: ch03(nonce 的 compile-fail)、ch07(边界的 proptest)、ch05(会话的 RAII)

测试类型级保证

正确构造模式将 bug 从运行时转移到编译时。但你如何测试无效代码实际上无法编译?你如何确保验证边界在模糊测试下保持成立?本章涵盖补充类型级正确性的测试工具。

使用 trybuild 进行 Compile-Fail 测试

trybuild crate 让你断言某些代码不应该编译。这对于在重构期间维护类型级不变量至关重要——如果有人意外给你的一次性 Nonce 添加了 Clone,compile-fail 测试会捕获它。

设置:

# Cargo.toml
[dev-dependencies]
trybuild = "1"

测试文件(tests/compile_fail.rs):

#[test]
fn type_safety_tests() {
    let t = trybuild::TestCases::new();
    t.compile_fail("tests/ui/*.rs");
}

测试用例:Nonce 重用一定不能编译(tests/ui/nonce_reuse.rs):

// tests/ui/nonce_reuse.rs
use my_crate::Nonce;

fn main() {
    let nonce = Nonce::new();
    encrypt(nonce);
    encrypt(nonce); // 应该失败:使用已移动的值
}

fn encrypt(_n: Nonce) {}

预期错误(tests/ui/nonce_reuse.stderr):

error[E0382]: use of moved value: `nonce`
 --> tests/ui/nonce_reuse.rs:6:13
  |
4 |     let nonce = Nonce::new();
  |         ----- move occurs because `nonce` has type `Nonce`, which does not implement the `Copy` trait
5 |     encrypt(nonce);
  |             ----- value moved here
6 |     encrypt(nonce); // should fail: use of moved value
  |             ^^^^^ value used here after move

每章更多的 compile-fail 测试用例:

模式(章节)测试断言文件
一次性 Nonce (ch03)不能使用 nonce 两次nonce_reuse.rs
Capability 令牌 (ch04)没有令牌就不能调用 admin_op()missing_token.rs
Type-State (ch05)不能在 Session<Idle> 上调用 send_command()wrong_state.rs
量纲 (ch06)不能添加 Celsius + Rpmunit_mismatch.rs
Sealed trait (技巧 2)外部 crate 不能实现 sealed traitunseal_attempt.rs
非穷举 (技巧 3)没有通配符的外部 match 失败missing_wildcard.rs

CI 集成:

# .github/workflows/ci.yml
- name: Run compile-fail tests
  run: cargo test --test compile_fail

验证边界的基于属性的测试

验证边界(ch07)解析数据一次并拒绝无效输入。但你如何知道你的验证捕获了所有无效输入?使用 proptest 进行基于属性的测试生成数千个随机输入来对边界进行压力测试:

# Cargo.toml
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;

/// 来自 ch07:ValidFru 包装符合规范的 FRU 负载。
/// 这些测试使用完整的 ch07 ValidFru,带有 board_area()、
/// product_area() 和 format_version() 方法。
/// 注意:ch07 定义了 TryFrom<RawFruData>,所以我们先包装原始字节。

proptest! {
    /// 任何通过验证的字节序列必须能在不 panic 的情况下使用。
    #[test]
    fn valid_fru_never_panics(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
        if let Ok(fru) = ValidFru::try_from(RawFruData(data)) {
            // 这些在已验证的 FRU 上绝不能 panic
            //(来自 ch07 的 ValidFru impl 的方法):
            let _ = fru.format_version();
            let _ = fru.board_area();
            let _ = fru.product_area();
        }
    }

    /// 往返:format_version 在重新解析后保持不变。
    #[test]
    fn fru_round_trip(data in valid_fru_strategy()) {
        let raw = RawFruData(data.clone());
        let fru = ValidFru::try_from(raw).unwrap();
        let version = fru.format_version();
        // 重新解析相同的字节——版本必须相同
        let reparsed = ValidFru::try_from(RawFruData(data)).unwrap();
        prop_assert_eq!(version, reparsed.format_version());
    }
}

/// 自定义策略:生成满足 FRU spec 头部的字节向量。
/// 头部格式匹配 ch07 的 `TryFrom<RawFruData>` 验证:
///   - 字节 0:version = 0x01
///   - 字节 1-6:区域偏移(×8 = 实际字节偏移)
///   - 字节 7:校验和(字节 0-7 的和 = 0 mod 256)
/// 主体是随机的,但足够大以使偏移量在边界内。
fn valid_fru_strategy() -> impl Strategy<Value = Vec<u8>> {
    let header = vec![0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00];
    proptest::collection::vec(any::<u8>(), 64..256)
        .prop_map(move |body| {
            let mut fru = header.clone();
            let sum: u8 = fru.iter().fold(0u8, |a, &b| a.wrapping_add(b));
            fru.push(0u8.wrapping_sub(sum));
            fru.extend_from_slice(&body);
            fru
        })
}

正确构造代码的测试金字塔:

┌───────────────────────────────────┐
│    Compile-Fail 测试 (trybuild)   │ ← "无效代码一定不能编译"
├───────────────────────────────────┤
│  属性测试 (proptest/quickcheck)   │ ← "有效输入绝不能 panic"
├───────────────────────────────────┤
│    单元测试 (#[test])             │ ← "特定输入产生预期输出"
├───────────────────────────────────┤
│    类型系统(模式 ch02–13)        │ ← "整个 bug 类别不能存在"
└───────────────────────────────────┘

RAII 验证

RAII(技巧 12)保证清理。要测试这一点,验证 Drop impl 实际上被触发:

use std::sync::atomic::{AtomicBool, Ordering};

// 注意:这些测试使用全局 AtomicBool,所以它们不能
// 彼此并行运行。使用 `#[serial_test::serial]` 或
// 使用 `cargo test -- --test-threads=1` 运行。
// 或者,使用每测试的 `Arc<AtomicBool>` 通过闭包传递
// 来完全避免全局状态。
static DROPPED: AtomicBool = AtomicBool::new(false);

struct TestSession;
impl Drop for TestSession {
    fn drop(&mut self) {
        DROPPED.store(true, Ordering::SeqCst);
    }
}

#[test]
fn session_drops_on_early_return() {
    DROPPED.store(false, Ordering::SeqCst);
    let result: Result<(), &str> = (|| {
        let _session = TestSession;
        Err("模拟失败")?;
        Ok(())
    })();
    assert!(result.is_err());
    assert!(DROPPED.load(Ordering::SeqCst), "Drop 必须在提前返回时触发");
}

#[test]
fn session_drops_on_panic() {
    DROPPED.store(false, Ordering::SeqCst);
    let result = std::panic::catch_unwind(|| {
        let _session = TestSession;
        panic!("模拟 panic");
    });
    assert!(result.is_err());
    assert!(DROPPED.load(Ordering::SeqCst), "Drop 必须在 panic 时触发");
}

应用到你的代码库

这是一个优先级的计划,用于将类型级测试添加到工作区:

Crate测试类型测试内容
protocol_libCompile-failSession<Idle> 不能调用 send_command()
protocol_lib属性任何字节序列 → TryFrom 要么成功要么返回 Err(不 panic)
thermal_diagCompile-fail没有 HasSpi 混合就不能构造 FanReading
accel_diag属性GPU 传感器解析:随机字节 → 已验证或被拒绝
config_loader属性随机字符串 → DiagLevel 的 FromStr 从不 panic
pci_topologyCompile-failRegister<Width16> 不能在需要 Width32 的地方传递
event_handlerCompile-fail审计令牌不能被 Clone
diag_frameworkCompile-failDerBuilder<Missing, _> 不能调用 finish()

零成本抽象:通过汇编证明

一个常见的担忧:“newtype 和 phantom 类型会增加运行时开销吗?” 答案是不——它们编译为与原始类型完全相同的汇编。以下是如何验证:

设置:

cargo install cargo-show-asm

示例:Newtype vs 原始 u32:

// src/lib.rs
#[derive(Clone, Copy)]
pub struct Rpm(pub u32);

#[derive(Clone, Copy)]
pub struct Celsius(pub f64);

// Newtype 算术
#[inline(never)]
pub fn add_rpm(a: Rpm, b: Rpm) -> Rpm {
    Rpm(a.0 + b.0)
}

// 原始算术(用于比较)
#[inline(never)]
pub fn add_raw(a: u32, b: u32) -> u32 {
    a + b
}

运行:

cargo asm my_crate::add_rpm
cargo asm my_crate::add_raw

结果——相同的汇编:

; add_rpm (newtype)           ; add_raw (原始 u32)
my_crate::add_rpm:            my_crate::add_raw:
  lea eax, [rdi + rsi]         lea eax, [rdi + rsi]
  ret                          ret

Rpm 包装器在编译时被完全擦除。同样的原则适用于 PhantomData<S>(零字节)、ZST 令牌(零字节)以及本指南中使用的所有其他类型级标记。

为你自己的类型验证:

# 显示特定函数的汇编
cargo asm --lib ipmi_lib::session::execute

# 显示 PhantomData 添加零字节
cargo asm --lib --rust ipmi_lib::session::IpmiSession

关键要点: 本指南中的每个模式都有零运行时成本。 类型系统完成所有工作,并在编译期间被完全擦除。 你获得 Haskell 的安全性和 C 的性能。

关键要点

  1. trybuild 测试无效代码不会编译 —— 对于在重构期间维护类型级不变量至关重要。
  2. proptest 模糊测试验证边界 —— 生成数千个随机输入来对 TryFrom 实现进行压力测试。
  3. RAII 验证测试 Drop 运行 —— Arc 计数器或模拟标志证明清理已发生。
  4. cargo-show-asm 证明零成本 —— phantom 类型、ZST 和 newtype 产生与原始 C 相同的汇编。
  5. 为每个“不可能“状态添加 compile-fail 测试 —— 如果有人意外地在一次性类型上派生 Clone,测试会捕获它。

Type-Driven Correctness in Rust 结束