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, ch17 | 2.5 小时 |
| GPU / PCIe 开发者 | ch02, ch06, ch09, ch10, ch15 | 2.5 小时 |
| Redfish 实现者 | ch02, ch05, ch07, ch08, ch17, ch18 | 3 小时 |
| 框架 / 基础设施 | ch04, ch08, ch11, ch14, ch18 | 2.5 小时 |
| 构造正确性新手 | 按顺序 ch01 → ch10,然后 ch12 练习 | 4 小时 |
| 完整深入 | 按顺序阅读所有章节 | 7 小时 |
带注释的目录
| 章 | 标题 | 难度 | 核心思想 |
|---|---|---|---|
| 1 | 理念——为什么类型击败测试 | 🟢 | 三层正确性;类型作为编译器检查的保证 |
| 2 | 类型化命令接口 | 🟡 | 关联类型绑定请求 → 响应 |
| 3 | 一次性类型 | 🟡 | 移动语义作为加密的线性类型 |
| 4 | Capability 令牌 | 🟡 | 零尺寸权限证明令牌 |
| 5 | 协议状态机 | 🔴 | IPMI sessions + PCIe LTSSM 的 type-state |
| 6 | 量纲分析 | 🟢 | Newtype 包装器防止单位混淆 |
| 7 | 有效性边界 | 🟡 | 在边缘解析一次,在类型中携带证明 |
| 8 | Capability 混合 | 🟡 | 成分 traits + 空白实现 |
| 9 | Phantom 类型 | 🟡 | PhantomData 用于寄存器宽度、DMA 方向 |
| 10 | 整合一切 | 🟡 | 一个诊断平台中的全部 7 个模式 |
| 11 | 实战十四个技巧 | 🟡 | Sentinel→Option、密封 traits、builders 等 |
| 12 | 练习 | 🟡 | 六个综合项目问题附带解决方案 |
| 13 | 参考卡 | — | 模式目录 + 决策流程图 |
| 14 | 测试类型级保证 | 🟡 | trybuild, proptest, cargo-show-asm |
| 15 | Const Fn | 🟠 | 内存映射、寄存器、比特域的编译时证明 |
| 16 | Send & Sync | 🟠 | 编译时并发证明 |
| 17 | Redfish 客户端演练 | 🟡 | 八个模式组合成类型安全的 Redfish 客户端 |
| 18 | Redfish 服务器演练 | 🟡 | Builder type-state、源令牌、健康汇总、mixins |
前置知识
| 概念 | 学习资源 |
|---|---|
| 所有权和借用 | Rust Patterns, ch01 |
| Traits 和关联类型 | Rust Patterns, ch02 |
| Newtypes 和 type-state | Rust Patterns, ch03 |
| PhantomData | Rust 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 不存在,因为类型系统无法表达它们。
理念——为什么类型击败测试 🟢
你将学到: 三层编译时正确性(值、状态、协议),泛型函数签名如何充当编译器检查的保证,以及何时构造正确性模式值得(或不值得)投入。
运行时检查的代价
考虑诊断代码库中典型的运行时守卫:
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}"),
}
}
这个函数有四种失败模式编译器无法捕获:
- 拼写错误:
"temperture"→ 运行时 panic raw长度错误:fan_speed只有 1 字节 → 运行时 panic- 调用者将返回的
f64当作 RPM 使用,而实际上是 °C → 逻辑 bug,静默失败 - 添加了新传感器类型但未更新此
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!
关键要点
- 三层正确性 —— 值(newtypes)、状态(type-state)、协议(关联类型)—— 每一层消除更广泛的 bug 类别。
- 类型作为保证 —— 每个泛型函数签名都是编译器在每次构建时检查的契约。
- 代价问题 —— “如果这个 bug 发布出去,有多糟糕?“决定类型还是测试是合适的工具。
- 类型补充测试 —— 它们消除整个类别;测试覆盖特定的值和边界情况。
- 知道何时停止 —— 内部辅助和临时原型很少需要类型级强制执行。
类型化命令接口——请求决定响应 🟡
你将学到: 命令 trait 上的关联类型如何在请求和响应之间创建编译时绑定,消除 IPMI、Redfish 和 NVMe 协议中的不匹配解析、单位混淆和静默类型强制。
无类型泥潭
大多数硬件管理栈——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)vsRpm(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 对有效性边界使用的相同技术):
| 节拍 | 协议 | 复杂度 | 添加内容 |
|---|---|---|---|
| 1 | IPMI | 简单:传感器 ID → 读数 | 核心模式:trait + 关联类型 |
| 2 | Redfish | REST:端点 → 类型化 JSON | Serde 集成,模式类型响应 |
| 3 | NVMe | 二进制:操作码 → 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 传感器读数 | ReadTemp | Celsius |
| Redfish REST | GetThermal | ThermalResponse |
| NVMe Admin | Identify | IdentifyResponse |
| PLDM | GetFwParams | FwParamsResponse |
| MCTP | GetEid | EidResponse |
| PCIe 配置空间 | ReadCapability | CapabilityHeader |
| SMBIOS/DMI | ReadType17 | MemoryDeviceInfo |
请求类型决定响应类型——编译器在每个地方强制执行它。
类型化命令流程
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
}
}
关键要点
- 关联类型 = 编译时契约 —— 命令 trait 上的
type Response将每个请求锁定到精确的一个响应类型。 - 解析被封装 —— 字节布局知识存在于
parse_response中,而不是分散在调用者中。 - 零成本分发 —— 泛型
execute<C: IpmiCmd>单态化为直接调用,没有虚表。 - 一个模式,多个协议 —— IPMI、Redfish、NVMe、PLDM、MCTP 都适合同一个
trait Cmd { type Response; }形状。 - Enum 分发桥接静态和动态 —— 将类型化命令包装在 enum 中用于运行时驱动的脚本,同时不丢失每个分支内的类型安全。
- 渐进复杂度增强直觉 —— 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;
所有权链防止了:
- Nonce 重用——
Nonce不是Clone,每次调用时被消费 - Key 复制——
UnboundKey被移动到SealingKey中,不能同时创建OpeningKey - 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); // 错误:使用已移动的值
关键要点
- 移动 = 线性使用——一个非 Clone、非 Copy 的类型只能被消费一次;编译器强制执行这一点。
- Nonce 重用是灾难性的——Rust 的所有权系统从结构上防止它,而不是依靠纪律。
- 模式适用于加密之外——OTP 熔丝、校准令牌、审计条目——任何最多发生一次的事情。
- 临时密钥免费获得前向保密性——密钥协商值被移动到派生的秘密中并消失。
- 如有疑问,移除
Clone——你以后总是可以添加它;从已发布的 API 中移除它是一个破坏性变更。
Capability 令牌——零成本权限证明 🟡
你将学到: 零大小类型(ZST)如何充当编译时证明令牌,强制执行权限层次结构、电源时序和可撤销权限——全部零运行时成本。
问题:谁被允许做什么?
在硬件诊断中,某些操作是危险的:
- 编程 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 能到达这里
}
关键要点
- ZST 令牌成本为零字节——它们只存在于类型系统中;LLVM 完全优化掉它们。
- 私有构造函数 = 不可伪造——只有你模块的
authenticate()可以铸造令牌。 - Trait 层次结构建模权限级别——
CanFlash: CanDiag: CanView镜像真实 RBAC。 - 生命周期受限令牌自动撤销——
ScopedAdminToken<'session>不能超越会话。 - 与 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(())
}
调用者必须:
- 创建会话(
Idle) - 认证(
Authenticated) - 激活(
Active) - 获取
AdminToken - 然后才能调用
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();
}
三个三部曲共同说明的内容:
| 三部曲 | 协议 | 状态数 | 组合 |
|---|---|---|---|
| 1 | IPMI 会话 | 4 | 纯类型状态 |
| 2 | PCIe LTSSM | 5 | 类型状态 + 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 状态下可用
}
}
关键要点
- 类型状态使错误顺序的调用不可能——方法只存在于它们有效的状态上。
- 每个转换消费
self——转换后不能保留旧状态。 - 与 capability 令牌组合——
firmware_update()需要两者Session<Active>和AdminToken。 - 三个三部曲,复杂性递增——IPMI(纯 FSM)、PCIe LTSSM(recovery 分支)和固件更新(FSM + 令牌 + 一次性证明)展示了模式从简单到丰富组合的扩展。
- 不要过度应用——两状态请求/响应协议不需要类型状态更简单。
- 模式扩展到完整的 Redfish 工作流——ch17 将类型状态应用于 Redfish 会话生命周期,ch18 使用 builder 类型状态进行响应构建。
量纲分析——让编译器检查你的单位 🟢
你将学到: newtype 包装器和
uomcrate 如何将编译器变成单位检查引擎,防止摧毁 3.28 亿美元航天器的那类 bug。
火星气候轨道器
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类型来表示差值。uomcrate(稍后展示)正确处理这一点。对于简单的传感器诊断(只需比较和显示),你可以从温度类型中省略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);
关键要点
- Newtype 以零成本防止单位混淆——
Celsius和Rpm内部都是f64,但编译器将它们视为不同的类型。 - 火星气候轨道器 bug 是不可能的——在期望
Newtons的地方传递Pounds是编译错误。 quantity!宏减少样板代码——为每个单位生成 Display、算术和阈值逻辑。uomcrate 处理导出单位——当你需要Watts = Volts × Amperes自动计算时使用它。- 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 个函数重复相同的检查 ...
}
这种模式(“散弹枪式验证”)有两个问题:
- 冗余——相同的检查出现在数十个地方
- 不完整——在一个函数中忘记一个检查就产生了 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 流水线比较:
| 步骤 | C | Rust |
|---|---|---|
| 解析记录类型 | switch 可能穿透 | match 在 enum 上——穷举 |
| 分类严重性 | 手动 if 链,忘记 PSU | 穷举 match——缺少变体时编译错误 |
| 线性化读数 | double——无单位 | Celsius / Rpm / Watts——不同的类型 |
| 聚合健康 | bool has_critical | 7 个类型化子系统字段 |
| 交接给 Redfish | 无类型 json_object_set("Health", "OK") | TypedSelSummary → 类型化健康汇总 (ch18) |
Rust 流水线不仅防止更多 bug——它产生更丰富的输出。C 流水线在每个阶段丢失信息(多态 → 扁平,量纲 → 无类型,每子系统 → 单个 bool)。Rust 流水线保留所有内容,因为类型系统使保留结构比丢弃更容易。
编译器证明的内容
| C 中的 Bug | Rust 如何防止 |
|---|---|
| 忘记检查记录类型 | match 在 ValidSelRecord 上——必须处理所有三个变体 |
| 触发读数的错误字节索引 | 一次解析到 ThresholdEvent.trigger_reading——消费者从不接触原始字节 |
缺少传感器类型的 case | SensorSpecificEvent 匹配是穷举的——缺少变体时编译错误 |
| 静默丢弃 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)
}
关键要点
- 在边界处解析一次——
TryFrom恰好验证一次原始数据;所有下游代码信任类型。 - 消除散弹枪式验证——如果函数接受
&ValidFru,数据是有效的。句号。 - 模式从扁平扩展到多态——FRU(扁平字节)、Redfish(结构化 JSON)和 SEL(嵌套判别联合)都在递增复杂性下使用相同技术。
- 穷举匹配是验证——对于像 SEL 这样的多态数据,编译器的 enum 穷举检查防止“忘记传感器类型“这类 bug,零运行时成本。
- 消费流水线保留结构——解析 → 分类 → 线性化 → 聚合保持每子系统健康和量纲读数完整,而 C 有损地缩减为单个
bool。类型系统使保留信息比丢弃更容易。 serde是自然的边界——#[derive(Deserialize)]带#[serde(try_from)]在解析时验证 JSON。- 组合有效类型——
ValidServerHealth可以要求ValidFru+ValidThermal+ValidPower。 - 与 proptest 配对(ch14)——模糊测试
TryFrom边界以确保没有有效输入被拒绝且没有无效输入溜过。 - 这些模式组合成完整的 Redfish 工作流——ch17 在客户端应用有效性边界(将 JSON 响应解析为类型化结构体),而 ch18 在服务器端反转模式(builder 类型状态确保序列化前每个必需字段存在)。这里构建的 SEL 消费流水线直接流入 ch18 的
TypedSelSummary健康汇总。
Capability 混合——编译时硬件契约 🟡
你将学到: 原料 trait(总线 capability)如何与混合 trait 和 blanket impl 结合,消除诊断代码重复,同时保证每个硬件依赖在编译时得到满足。
问题:诊断代码重复
服务器平台在子系统间共享诊断模式。风扇诊断、温度监控和电源时序都遵循类似的工作流程,但操作不同的硬件总线。没有抽象会导致复制粘贴:
// 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 完成所有工作。
构造正确性方面
混合模式是构造正确的,因为:
- 没有 SPI 就无法调用
read_fan_speed()——该方法只存在于实现HasSpi + HasI2c的类型上 - 你不能忘记总线 ——如果从
BaseBoardController中移除HasSpi,FanDiagMixin方法在编译时消失 - Mock 测试是自动的 ——用
MockSpi替换LinuxSpi,所有混合逻辑与 mock 一起工作 - 新平台只需声明能力 ——只有 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 自动获得两个混合方法
关键要点
- 原料 trait 声明硬件能力 ——
HasSpi、HasI2c、HasGpio是关联类型 trait。 - 混合 trait 通过 blanket impl 提供行为 ——
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}。 - 添加新平台 = 列出它的能力 ——编译器提供所有匹配的混合方法。
- 移除总线 = 使用它的地方都是编译错误 ——你不会忘记更新下游代码。
- Mock 测试是免费的 ——用
MockSpi替换LinuxSpi;所有混合逻辑不变工作。
Phantom 类型用于资源跟踪 🟡
你将学到:
PhantomData标记如何在类型级别编码寄存器宽度、DMA 方向和文件描述符状态——以零运行时成本防止整个资源不匹配 bug 类别。
问题:混淆资源
硬件资源在代码中看起来相似但不可互换:
- 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) -> u8MemRegion<ReadWrite>具有read和writeMemRegion<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` 方法
关键要点
- PhantomData 以零大小携带类型级信息 ——标记只存在于编译器中。
- 寄存器宽度不匹配成为编译错误 ——
Register<Width16>返回u16,而不是u32。 - DMA 方向在结构上强制执行 ——
DmaBuffer<Read>没有write()方法。 - 与量纲类型结合(ch06) ——
Register<Width16>可以通过解析步骤返回Celsius。 - Phantom 类型仅编译时有效 ——它们不适用于运行时可变属性;对这些使用枚举。
Const Fn —— 编译时正确性证明 🟠
你将学到:
const fn和assert!如何将编译器变成证明引擎——在编译时验证 SRAM 内存映射、寄存器布局、协议帧、位域掩码、时钟树和查找表,零运行时成本。
问题:虚假的内存映射
在嵌入式和系统编程中,内存映射是一切的基础——它们定义了 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 断言证明:
- 每个寄存器自然对齐(offset % width == 0)
- 没有两个寄存器重叠
- 所有寄存器适合 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 分区映射。要求:
- 四个分区:bootloader(64 KB)、application(640 KB)、config(64 KB)、OTA staging(256 KB)
- 每个分区必须4 KB 对齐(flash 擦除粒度):base 和 size 必须是 4096 的倍数
- 没有分区可以重叠另一个
- 所有分区必须适合 flash
- 添加一个
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 > 0<br/>✅ 无溢出"]
MAP["SramMap::verified()<br/>✅ 包含<br/>✅ 无重叠"]
ACC["TypedRegion<RW><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
关键要点
-
const fn+assert!= 编译时证明义务 —— 如果断言在 const 求值期间失败,程序无法编译。不需要测试,不需要代码审查捕获——编译器证明它。 -
内存映射是理想的候选 —— 子区域包含、无重叠、总大小边界和对齐约束都可以表示为 const fn 断言。C 的
#define方法不提供这些保证。 -
Phantom 类型分层在上 —— 结合 const fn(值验证)与 phantom 类型访问标记(权限验证),零运行时成本进行纵深防御。
-
溯源可以在编译时建立 ——
VerifiedAddr在编译时证明地址属于特定区域,消除每次访问的运行时边界检查。 -
模式泛化到内存之外 —— 寄存器映射、位域掩码、协议帧、时钟树、DMA 参数——任何你有编译时已知值和结构不变量的地方。
-
位域和时钟树是理想的候选 —— 寄存器内位的不相交性和派生值约束链(VCO 范围、除法器限制)正是
const fn轻松证明的那种不变量。 -
const fn取代代码生成器和构建脚本用于查找表 —— CRC 表、三角函数、编码映射——在编译时计算,放在.rodata中,零启动成本,没有外部工具。 -
内联
const { }块验证泛型参数 —— 从 Rust 1.79 开始,你可以在调用点强制执行 const 泛型参数的约束,在任何代码运行之前捕获误用。
Send & Sync — 编译期并发证明 🟠
你将学到: Rust 的
Send和Syncauto-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 | 证明 | 非正式含义 |
|---|---|---|
Send | T 类型的值可以安全地移动到另一个线程 | “这可以跨越线程边界” |
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:
| 类型 | Send | Sync | 原因 |
|---|---|---|---|
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 传输进行时持有缓冲区。要求:
DmaTransfer必须是!Send——DMA 控制器使用绑定到这个内核内存总线的物理地址DmaTransfer必须是!Sync——DMA 写入时并发读取会看到撕裂数据- 提供一个
wait()方法消费守卫并返回缓冲区——所有权证明传输完成 - 缓冲区类型
T必须实现DmaSafemarker 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<T><br/>restores Sync"]
ARC["Arc<T><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
关键要点
-
Send和Sync是关于并发安全性的编译期证明——编译器通过检查每个字段在结构上派生它们。无需标注,无运行时成本,无需显式选择。 -
裸指针自动选择退出——任何包含
*const T或*mut T的类型变成!Send + !Sync。这使得外设句柄自然地限制在线程内。 -
PhantomData<*const ()>是显式选择退出——当一个类型没有裸指针但仍应限制在线程内时(C 库句柄、GPU 上下文),phantom 字段可以完成这项工作。 -
Mutex<T>恢复Sync并带证明——编译器在结构上证明所有访问都通过锁。与 C 的pthread_mutex_t不同,你不可能忘记加锁。 -
函数约束是定理——生成器签名中的
F: Send + 'static是编译期证明义务:每个调用点必须证明其闭包是线程安全的。与 C 的void *arg对比,它接受任何内容。 -
该模式补充所有其他正确性技术——typestate 证明协议排序,phantom 类型证明权限,
const fn证明值不变量,Send/Sync证明并发安全性。它们一起覆盖完整的正确性表面。
整合一切——完整诊断平台 🟡
你将学到: 七个核心模式(ch02–ch09)如何组合成单个工作诊断流程——认证、会话、类型化命令、审计令牌、量纲结果、验证数据和 phantom 类型寄存器——零总运行时开销。
交叉引用: 每个核心模式章节(ch02–ch09)、ch14(测试这些保证)
目标
本章将第 2-9 章的七个模式组合成一个单一的、现实的诊断工作流。我们将构建一个服务器健康检查,它:
- 认证(capability 令牌——ch04)
- 打开 IPMI 会话(type-state——ch05)
- 发送类型化命令(类型化命令——ch02)
- 使用一次性令牌进行审计日志(一次性类型——ch03)
- 返回量纲结果(量纲分析——ch06)
- 验证 FRU 数据(验证边界——ch07)
- 读取类型化寄存器(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() 需要 &AdminToken | Capability 令牌 |
| 错误会话状态下的命令 | execute() 仅存在于 Session<Active> 上 | Type-state |
| 错误的响应类型 | ReadTemp::Response = Celsius,由 trait 固定 | 类型化命令 |
| 单位混淆(°C vs RPM) | Celsius ≠ Rpm ≠ Volts | 量纲类型 |
| 寄存器宽度不匹配 | Reg<Width16> 返回 u16 | Phantom 类型 |
| 处理未验证数据 | 必须先调用 ValidFru::parse() | 验证边界 |
| 重复审计条目 | AuditToken 在 log 时被消费 | 一次性类型 |
| 电源时序顺序错误 | 每一步需要前一个令牌 | Capability 令牌(ch04) |
所有这些保证的总运行时开销:零。
每个检查都在编译时完成。生成的汇编代码与没有任何检查的手写 C 代码相同——但C 可能有 bug,这个不可能。
关键要点
- 七个模式无缝组合 —— capability 令牌、type-state、类型化命令、一次性类型、量纲类型、验证边界和 phantom 类型都一起工作。
- 编译器证明八个 bug 类别不可能 —— 参见上面的“编译器证明的内容“表格。
- 零总运行时开销 —— 生成的汇编代码与无检查的 C 代码相同。
- 每个模式独立有用 —— 你不需要全部七个;增量采用它们。
- 集成章节是设计模板 —— 用它作为你自己类型化诊断工作流的起点。
- 从 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 引入了它自己类别的正确性危险:
| 危险 | 示例 | 后果 |
|---|---|---|
| 格式错误的 URI | GET /redfish/v1/Chassis/1/Processors(错误的父节点) | 返回 404 或错误数据静默返回 |
| 错误电源状态下的操作 | 在已关闭的系统上调用 Reset(ForceOff) | BMC 返回错误,或更糟,与另一个操作竞争 |
| 缺少特权 | 操作员级代码调用 Manager.ResetToDefaults | 生产环境中返回 403,安全审计发现 |
| 不完整的 PATCH | 从 PATCH 体中遗漏必需的 BIOS 属性 | 静默无操作或部分配置损坏 |
| 未验证的固件应用 | 在检查图像完整性之前调用 SimpleUpdate | BMC 变砖 |
| 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 字段 → panic | ValidThermalResponse 在解析边界验证 | 验证边界(§4) |
| 6 | 错误的响应类型 | RedfishResource::Response 每个资源固定 | 类型化命令(§4) |
| 7 | 不完整的 PATCH 负载 | .apply() 仅在所有字段是 FieldSet 时存在 | 构建器 type-state(§5) |
| 8 | 缺少 PATCH 特权 | .apply() 需要 &ConfigureComponentsToken | Capability tokens(§5) |
| 9 | 应用未验证的固件 | .apply() 仅存在于 FwVerified 上 | Type-state(§6) |
| 10 | 重复固件应用 | apply() 消费 self——值被移动 | 一次性类型(§6) |
| 11 | 没有权威的固件更新 | push_image() 需要 &ConfigureManagerToken | Capability tokens(§6) |
| 12 | 登录后使用 | logout() 消费会话 | 所有权(§1) |
所有十二个保证的总运行时开销:零。
生成的二进制文件与无类型版本进行相同的 HTTP 调用——但无类型版本可能有 12 类 bug。这个版本不可能。
对比:IPMI 集成(ch10)vs. Redfish 集成
| 维度 | ch10 (IPMI) | 本章 (Redfish) |
|---|---|---|
| 传输 | KCS/LAN 上的原始字节 | HTTPS 上的 JSON |
| 导航 | 扁平命令码(NetFn/Cmd) | 层次 URI 树 |
| 响应绑定 | IpmiCmd::Response | RedfishResource::Response |
| 特权模型 | 单个 AdminToken | 基于角色的多令牌 |
| 负载构造 | 字节数组 | JSON 的构建器 type-state |
| 更新生命周期 | 未覆盖 | 完整 type-state 链 |
| 锻炼的模式 | 7 | 8(添加构建器 type-state) |
这两章是互补的:ch10 展示了这些模式在字节级有效,本章展示了它们在 REST/JSON 级同样有效。类型系统不关心传输——它以两种方式证明正确性。
关键要点
- 八个模式组合成一个 Redfish 客户端——会话 type-state、capability 令牌、phantom 类型 URI、类型化命令、量纲分析、验证边界、构建器 type-state 和一次性固件应用。
- 十二类 bug 变成编译错误——见上表。
- 零运行时开销——每个证明令牌、phantom 类型和 type-state 标记都编译消失。二进制文件与手写的无类型代码相同。
- REST API 与字节协议受益相同——来自 ch02–ch09 的模式同样适用于 JSON-over-HTTPS(Redfish)和 bytes-over-KCS(IPMI)。
- 特权强制执行是结构性的,不是过程性的——函数签名声明需要什么;编译器强制执行它。
- 这是一个设计模板——为你的特定 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 臂(存在/不存在)必须产生HasField | Type-state + 穷举匹配(§2) |
| 4 | JSON 字段中错误的单位 | reading_celsius: Celsius ≠ Rpm ≠ Watts | 量纲类型(§3) |
| 5 | 不完整的健康汇总 | compute_system_health 采用显式来源引用;SEL 通过 ch07 的 TypedSelSummary 提供每子系统HealthValue | 类型化函数签名 + 穷举匹配(§4) |
| 6 | Schema 版本不匹配 | 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 章:“我消费的每个响应都经过类型检查。” 本章:“我生产的每个响应都经过类型检查。” 相同的模式在两个方向流动——类型系统不知道或不关心你在哪一端。
关键要点
- “构造,不要序列化” 是服务端的镜像“解析,不要验证“——使用构建器 type-state 所以
.build()仅在所有必需字段存在时可用。 - 源可用性令牌证明初始化——来自 ch04 的相同 capability 令牌模式,重新用于证明数据源就绪。
- 量纲类型保护生产者和消费者——在
ReadingCelsius字段中放入Rpm是编译错误,不是客户报告的 bug。 - 健康汇总是类型化 fold——
HealthValue上的Ord加上显式来源引用意味着编译器捕获“忘记包含 PSU 状态。“ - 类型级的 Schema 版本化——phantom 类型参数使版本特定字段在编译时出现和消失。
- Action 分发反向 ch02——
serde将负载反序列化为类型化的Params结构体,对枚举变体的穷举匹配意味着添加新的ResetType强制每个处理程序更新。 - 服务端 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 = 0xFFFF | let mut vendor_id: Option<u16> = None |
.unwrap_or(0xFFFF) | .ok()(已经返回 Option) |
if vendor_id != 0xFFFF { ... } | if let Some(vid) = vendor_id { ... } |
序列化:vendor_id | vendor_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 中的消费者。
候选
| 枚举 | 模块 | 为什么 |
|---|---|---|
SkuVariant | inventory, net_inventory | 每代新 SKU |
SensorType | protocol_lib | IPMI 规范保留 0xC0–0xFF 给 OEM |
CompletionCode | protocol_lib | 自定义 BMC 供应商添加代码 |
Component | event_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'")
三个好处
- 快速失败: 错误输入在解析边界被捕获,而不是在诊断逻辑深处三层。
- 别名是显式的:
"MEM"、"DIMM"和"MEMORY"都映射到Component::Memory—— match 臂记录了映射。 .parse()符合人体工程学: 因为FromStr与str::parse()集成,你获得简洁的单行代码:let level: DiagLevel = config["level"].parse()?;
实际代码库使用
项目已经有 8 个 FromStr 实现:
| 类型 | 模块 | 值得注意的别名 |
|---|---|---|
DiagLevel | config_loader | "1" = Quick, "4" = Stress |
Component | event_handler | "MEM" / "DIMM" = Memory, "SSD" / "NVME" = Disk |
SkuVariant | net_inventory | "Accel-X1" = S2001, "Accel-M1" = S2002, "Accel-Z1" = S3001 |
SkuVariant | inventory | 相同的别名(单独的模块,相同的模式) |
FaultStatus | config_loader | Fault 生命周期状态 |
DiagAction | config_loader | 补救动作类型 |
ActionType | config_loader | 动作类别 |
DiagMode | cluster_diag | 多节点测试模式 |
与 TryFrom 的对比:
TryFrom<&[u8]> | FromStr | |
|---|---|---|
| 输入 | 原始字节(二进制协议) | 字符串(配置、CLI、JSON) |
| 典型来源 | IPMI、PCIe 配置空间、FRU | JSON 字段、环境变量、用户输入 |
| 章节 | ch07 | ch11 |
| 两者都使用 | 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 泛型参数 |
| 7 | unsafe 散布在代码库中 | 中——包装器模块 |
技巧 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或rumpsteakcrate。
技巧 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 要求Dropimpl 应用于泛型类型的所有实例。要获得特定状态的清理,使用以下之一:
方法 优点 缺点 单独的包装类型(如上) 干净、零成本 额外的类型名 泛型 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_diag | diag_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())显式确认并消除警告——当丢弃是故意的时这没问题。
关键要点
- 边界处的哨兵 → Option ——在解析时将魔术值转换为
Option;编译器强制调用者处理None。 - 密封 trait 封闭实现漏洞 ——私有超 trait 意味着只有你的 crate 可以实现 trait。
#[non_exhaustive]+#[must_use]是一行、高价值的注释 ——将它们添加到演进的枚举和消费的令牌。- Typestate builder 强制执行必需字段 ——当所有必需的参数是
Set时,finish()才存在。 - 每个技巧针对特定的 bug 类别 ——增量采用它们;没有技巧需要重写你的架构。
练习题 🟡
你将学到: 在真实的硬件场景中动手练习正确构造模式——NVMe 管理命令、固件更新状态机、传感器流水线、PCIe phantom 类型、多协议健康检查和会话类型诊断协议。
交叉引用: ch02(练习 1)、ch05(练习 2)、ch06(练习 3)、ch09(练习 4)、ch10(练习 5)
练习题
练习 1:NVMe 管理命令(类型化命令)
为 NVMe 管理命令设计一个类型化命令接口:
Identify→IdentifyResponse(型号、序列号、固件版本)GetLogPage→SmartLog(温度、可用备用空间、读取的数据单元数)GetFeature→ 特定功能的响应
要求:
- 命令类型决定响应类型
- 没有运行时调度——仅静态调度
- 添加一个
NamespaceIdnewtype,防止将命名空间 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 是证明令牌
要求:
- 每个状态是一个不同的类型
- 上传只能从 Idle 开始
- 验证需要上传完成
- 应用只能在成功验证后发生——接受一个
VerifiedImage证明令牌 - 重启是应用后的唯一选项
- 添加一个
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:传感器读数流水线(量纲分析)
构建一个完整的传感器流水线:
- 定义 newtype:
RawAdc、Celsius、Fahrenheit、Volts、Millivolts、Watts - 实现
From<Celsius> for Fahrenheit和反向转换 - 创建
impl Mul<Volts, Output=Watts> for Amperes(P = V × I) - 构建一个
Threshold<T>泛型检查器 - 编写流水线: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 链表建模:
RawCapability—— 来自配置空间的未验证字节ValidCapability—— 已解析和验证(通过 TryFrom)- 每种 capability 类型(MSI、MSI-X、PCIe Express、电源管理)有自己的 phantom 类型寄存器布局
- 遍历列表返回
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 混合)
创建一个健康检查框架:
- 定义原料 trait:
HasIpmi、HasRedfish、HasNvmeCli、HasGpio - 创建混合 trait:
ThermalHealthMixin(需要 HasIpmi + HasGpio)—— 读取温度,检查警报StorageHealthMixin(需要 HasNvmeCli)—— SMART 数据检查BmcHealthMixin(需要 HasIpmi + HasRedfish)—— 交叉验证 BMC 数据
- 构建一个
FullPlatformController实现所有原料 trait - 构建一个
StorageOnlyController只实现HasNvmeCli - 验证
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)
设计一个带有一次性测试执行令牌的诊断会话:
DiagSession从Setup状态开始- 转换到
Running状态——发放N个执行令牌(每个测试用例一个) - 每个
TestToken在测试运行时被消费——防止运行相同的测试两次 - 在所有令牌被消费后,转换到
Complete状态 - 生成报告(仅在
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>上可用——这确保在结束前所有令牌都被消费。
关键要点
- 用真实协议练习 —— NVMe、固件更新、传感器流水线、PCIe 都是这些模式的真实世界目标。
- 每个练习映射到核心章节 —— 使用交叉引用来在尝试之前复习模式。
- 解决方案使用可展开的详情 —— 在揭示解决方案之前尝试每个练习。
- 在练习 5 中组合模式 —— 多协议健康检查结合了类型化命令、量纲类型和验证边界。
- 会话类型(练习 6)是前沿 —— 它们强制跨通道的消息排序,将 type-state 扩展到分布式系统。
参考卡片
14+ 种正确构造模式的快速参考,包含选择流程图、模式目录、组合规则、crate 映射和类型即保证速查表。
交叉引用: 每一章——这是整本书的查找表。
快速参考:正确构造模式
模式选择指南
bug 如果被遗漏是否是灾难性的?
├── 是 → 能编码到类型中吗?
│ ├── 是 → 使用正确构造
│ └── 否 → 运行时检查 + 广泛测试
└── 否 → 运行时检查没问题
模式目录
| # | 模式 | 关键 Trait/类型 | 防止 | 运行时成本 | 章节 |
|---|---|---|---|---|---|
| 1 | 类型化命令 | trait IpmiCmd { type Response; } | 错误的响应类型 | 零 | ch02 |
| 2 | 一次性类型 | struct Nonce(非 Clone/Copy) | Nonce/密钥重用 | 零 | ch03 |
| 3 | Capability 令牌 | struct AdminToken { _private: () } | 未授权访问 | 零 | ch04 |
| 4 | Type-State | Session<Active> | 协议违规 | 零 | ch05 |
| 5 | 量纲类型 | struct Celsius(f64) | 单位混淆 | 零 | ch06 |
| 6 | 验证边界 | struct ValidFru(通过 TryFrom) | 使用未验证数据 | 解析一次 | ch07 |
| 7 | Capability 混合 | trait FanDiagMixin: HasSpi + HasI2c | 缺少总线访问 | 零 | ch08 |
| 8 | Phantom 类型 | Register<Width16> | 宽度/方向不匹配 | 零 | ch09 |
| 9 | Sentinel → Option | Option<u8>(而非 0xFF) | Sentinel 作为值的 bug | 零 | ch11 |
| 10 | Sealed trait | trait Cmd: private::Sealed | 不健全的外部实现 | 零 | ch11 |
| 11 | 非穷举枚举 | #[non_exhaustive] enum Sku | 静默的 match 回退 | 零 | ch11 |
| 12 | Typestate 构建器 | DerBuilder<Set, Missing> | 不完整构造 | 零 | ch11 |
| 13 | FromStr 验证 | impl FromStr for DiagLevel | 未验证的字符串输入 | 解析一次 | ch11 |
| 14 | Const 泛型大小 | RegisterBank<const N: usize> | 缓冲区大小不匹配 | 零 | ch11 |
| 15 | 安全 unsafe 包装 | MmioRegion::read_u32() | 未检查的 MMIO/FFI | 零 | ch11 |
| 16 | 异步 Type-State | AsyncSession<Active> | 异步协议违规 | 零 | ch11 |
| 17 | Const 断言 | SdrSensorId<const N: u8> | 无效的编译时 ID | 零 | ch11 |
| 18 | 会话类型 | Chan<SendRequest> | 无序的通道操作 | 零 | ch11 |
| 19 | Pin 自引用 | Pin<Box<StreamParser>> | 悬垂的结构体内指针 | 零 | ch11 |
| 20 | RAII / Drop | impl 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 或 RPM | fn 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 = 0xFFFF | Sentinel 在内部携带 | 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_diag | Capability 混合、量纲类型 |
accel_diag | 验证边界、phantom 寄存器 |
network_diag | Type-State(链路训练)、capability 令牌 |
pci_topology | Phantom 类型(寄存器宽度)、已验证配置、sentinel → Option |
event_handler | 一次性审计令牌、capability 令牌、FromStr(Component) |
event_log | 验证边界(SEL 记录解析) |
compute_diag | 量纲类型(温度、频率) |
memory_diag | 验证边界(SPD 数据)、量纲类型 |
switch_diag | Type-State(端口枚举)、phantom 类型 |
config_loader | FromStr(DiagLevel、FaultStatus、DiagAction) |
log_analyzer | 验证边界(CompiledPatterns) |
diag_framework | Typestate 构建器(DerBuilder)、会话类型(orchestrator↔worker) |
topology_lib | Const 泛型寄存器银行、安全 MMIO 包装 |
类型即保证——快速映射
| 保证 | Rust 等价物 | 示例 |
|---|---|---|
| “此证明存在” | 一个类型 | AdminToken |
| “我有证明” | 该类型的值 | let tok = authenticate()?; |
| “A 意味着 B” | 函数 fn(A) -> B | fn 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 + Rpm | unit_mismatch.rs |
| Sealed trait (技巧 2) | 外部 crate 不能实现 sealed trait | unseal_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_lib | Compile-fail | Session<Idle> 不能调用 send_command() |
protocol_lib | 属性 | 任何字节序列 → TryFrom 要么成功要么返回 Err(不 panic) |
thermal_diag | Compile-fail | 没有 HasSpi 混合就不能构造 FanReading |
accel_diag | 属性 | GPU 传感器解析:随机字节 → 已验证或被拒绝 |
config_loader | 属性 | 随机字符串 → DiagLevel 的 FromStr 从不 panic |
pci_topology | Compile-fail | Register<Width16> 不能在需要 Width32 的地方传递 |
event_handler | Compile-fail | 审计令牌不能被 Clone |
diag_framework | Compile-fail | DerBuilder<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 的性能。
关键要点
- trybuild 测试无效代码不会编译 —— 对于在重构期间维护类型级不变量至关重要。
- proptest 模糊测试验证边界 —— 生成数千个随机输入来对
TryFrom实现进行压力测试。 - RAII 验证测试 Drop 运行 —— Arc 计数器或模拟标志证明清理已发生。
- cargo-show-asm 证明零成本 —— phantom 类型、ZST 和 newtype 产生与原始 C 相同的汇编。
- 为每个“不可能“状态添加 compile-fail 测试 —— 如果有人意外地在一次性类型上派生
Clone,测试会捕获它。
Type-Driven Correctness in Rust 结束