Rust 中的类型驱动正确性
讲师简介
- Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering,硅与云硬件基础设施工程)团队首席固件架构师
- 行业资深专家,专长于安全、系统编程(固件、操作系统、虚拟机管理程序)、CPU 与 platform 架构以及 C++ 系统
- 2017 年在 AWS EC2 开始使用 Rust 编程,从此爱上了这门语言
这是一本关于如何利用 Rust 类型系统,让整类 bug 在编译阶段就无法出现 的实用指南。虽然配套书籍 Rust 模式 讲解了相关机制(Traits、关联类型、Type-state),但本指南将展示如何将这些机制 应用 到真实世界领域——如硬件诊断、密码学、协议校验和嵌入式系统。
这里的每一个模式都遵循同一个原则:将运行时检查的不变量推动到类型系统中,从而让编译器强制执行它们。
如何使用本书
难度说明
| 符号 | 级别 | 受众 |
|---|---|---|
| 🟢 | 入门 | 熟悉所有权 + Traits |
| 🟡 | 中级 | 熟悉泛型 + 关联类型 |
| 🔶 | 高级 | 准备学习 Type-state、Phantom Types 和 Session Types |
学习进度指南
| 目标 | 路径 | 时间 |
|---|---|---|
| 快速概览 | 第 1 章、第 13 章(速查卡) | 30 分钟 |
| IPMI / BMC 开发者 | 第 2、5、7、10、17 章 | 2.5 小时 |
| GPU / PCIe 开发者 | 第 2、6、9、10、15 章 | 2.5 小时 |
| Redfish 实现者 | 第 2、5、7、8、17、18 章 | 3 小时 |
| 框架 / 架构师 | 第 4、8、11、14、18 章 | 2.5 小时 |
| “构造即正确”新手 | 按顺序阅读第 1 到 10 章,然后完成第 12 章练习 | 4 小时 |
| 完整深潜 | 按顺序阅读所有章节 | 7 小时 |
目录注释
| 章节 | 标题 | 难度 | 核心思想 |
|---|---|---|---|
| 1 | 核心理念:为什么类型胜过测试 | 🟢 | 正确性的三个层次;作为编译器检查保证的类型 |
| 2 | 类型化命令接口 | 🟡 | 关联类型将请求与响应绑定 |
| 3 | 单次使用类型 | 🟡 | 移动语义作为密码学的线性类型 |
| 4 | 能力令牌 | 🟡 | 零大小的授权证明令牌 |
| 5 | 协议状态机 | 🔶 | 适用于 IPMI 会话 + PCIe LTSSM 的 Type-state |
| 6 | 量纲分析 | 🟢 | Newtype 包装器防止单位混淆 |
| 7 | 已验证边界 | 🟡 | 在边界处一次性解析,在类型中携带证明 |
| 8 | 能力混入 | 🟡 | 组成式 Traits + Blanket 映射 |
| 9 | Phantom Types | 🟡 | 用于寄存器宽度、DMA 方向的 PhantomData |
| 10 | 综合应用 | 🟡 | 在一个诊断平台中组合全部 7 类模式 |
| 11 | 一线实践中的十四个技巧 | 🟡 | 哨兵值转 Option、Sealed Traits、Builders 等 |
| 12 | 练习 | 🟡 | 带解决方案的六个综合问题 |
| 13 | 速查卡 | - | 模式目录 + 决策流程图 |
| 14 | 测试类型层保证 | 🟡 | trybuild, proptest, cargo-show-asm |
| 15 | const fn | 🔶 | 内存映射、寄存器、位字段的编译期证明 |
| 16 | Send 与 Sync | 🔶 | 编译期并发正确性证明 |
| 17 | 实战演练 —— 类型安全的 Redfish 客户端 | 🟡 | 将八种模式组合成类型安全的 Redfish 客户端 |
| 18 | 实战演练 —— 类型安全的 Redfish 服务器 | 🟡 | Builder Type-state、源令牌、健康状况汇总、混入 |
前置要求
| 概念 | 学习位置 |
|---|---|
| 所有权与借用 | Rust 模式,第 1 章 |
| Traits 与关联类型 | Rust 模式,第 2 章 |
| Newtypes 与 Type-state | Rust 模式,第 3 章 |
| PhantomData | Rust 模式,第 4 章 |
| 泛型与 Trait 约束 | Rust 模式,第 1 章 |
“构造即正确”光谱
较低安全性 较高安全性
运行时检查 单元测试 属性测试 构造即正确
---------------- ---------- -------------- -----------------------
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 不存在,因为类型系统 无法表达它们。
核心理念 —— 为什么类型胜过测试 🟢
你将学到:
- 编译期正确性的三个层次(值、状态、协议)。
- 泛型函数签名如何充当编译器检查的保证。
- 何时“构造即正确 (correct-by-construction)”模式值得投入,以及何时不值得。
运行时检查的成本
考虑诊断代码库中一个典型的运行时守卫 (Guard):
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!("未知传感器类型: {sensor_type}"),
}
}
这个函数有 四种失效模式 是编译器无法捕捉到的:
- 拼写错误:
"temperture"→ 在运行时发生 panic。 - 错误的
raw长度:fan_speed仅带有 1 个字节 → 在运行时发生 panic。 - 调用者误用:调用者将返回的
f64当作 RPM 使用,而它实际上是 °C → 逻辑 bug,且无声无息。 - 扩展缺失:添加了新的传感器类型但未更新此
match语句 → 在运行时发生 panic。
每种失效模式都是在 部署之后 才被发现的。测试虽然有所帮助,但它们只能覆盖有人编写过的案例。而类型系统涵盖了 所有 情况,包括那些没人预料到的情况。
正确性的三个层次
层次 1 —— 值正确性 (Value Correctness)
使无效值无法被表达。
// ❌ 任何 u16 都可以是 "port" —— 0 是无效的但可以通过编译
fn connect(port: u16) { /* ... */ }
// ✅ 只有经过校验的端口才能存在
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("端口必须 > 0") }
}
}
fn connect(port: Port) { /* ... */ }
// Port(0) 永远无法被构造出来 —— 不变性在任何地方都成立
硬件示例:SensorId(u8) —— 包装一个原始传感器编号,并校验其在 SDR 范围内。
层次 2 —— 状态正确性 (State Correctness)
使无效的状态转换无法被表达。
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> {
// ... 连接逻辑 ...
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() 方法。
层次 3 —— 协议正确性 (Protocol Correctness)
使无效的交互无法被表达。
use std::io;
trait IpmiCmd {
type Response;
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
// 为便于说明进行了简化 —— 完整特性请参阅第 2 章,
// 包含 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 的类型系统如此强大 —— 它不仅仅是在捕捉错误,它是在 编译期强制执行正确性。
何时 不 使用这些模式
构造即正确 (Correct-by-construction) 并不总是最佳选择:
| 场景 | 建议 |
|---|---|
| 安全批判性边界 (上电序列、加密) | ✅ 始终使用 —— 这里的 bug 会损毁硬件或泄露秘密 |
| 跨模块的公共 API | ✅ 通常建议使用 —— 误用应当导致编译错误 |
| 拥有 3 个以上状态的状态机 | ✅ 通常建议使用 —— 类型状态 (Type-state) 可防止错误的转换 |
| 单个 50 行函数内部的辅助工具 | ❌ 过度设计 —— 简单的 assert! 足矣 |
| 原型设计 / 探索未知硬件 | ❌ 先使用原始类型 —— 在理解行为后再进行细化 |
| 面向用户的 CLI 解析 | ⚠️ 在边界处使用 clap + TryFrom,内部使用原始类型即可 |
关键问题在于:“如果这个 bug 在生产环境中发生,后果有多严重?”
- 风扇停止 → GPU 损毁 → 使用类型
- 错误的 DER 记录 → 客户收到错误数据 → 使用类型
- 调试日志消息稍微出错 → 使用
assert!
关键要点
- 正确性的三个层次 —— 值 (新类型)、状态 (类型状态)、协议 (关联类型) —— 每一层都消除了更广泛的一类 bug。
- 类型作为保证 —— 每个泛型函数签名都是一份合同,编译器在每次构建时都会对其进行检查。
- 成本问题 —— “如果这个 bug 发布了,后果有多严重?”决定了类型还是测试才是正确的工具。
- 类型是对测试的补充 —— 它们消除了整个 类别 的错误;测试则覆盖特定的 数值 和边界情况。
- 知道何时停止 —— 内部辅助工具和临时原型很少需要类型级的强制约束。
类型化命令接口 —— 请求决定响应 🟡
你将学到:
- 如何通过命令特性的关联类型在请求与响应之间建立编译期绑定。
- 消除 IPMI、Redfish 和 NVMe 协议中常见的解析错误、单位混淆和隐式类型强制转换。
非类型化的泥潭
大多数硬件管理协议栈 —— 如 IPMI、Redfish、NVMe Admin、PLDM —— 最初都是以 原始字节输入 → 原始字节输出 的形式存在的。这产生了一类测试只能部分发现的 bug:
use std::io;
struct BmcRaw { /* ipmitool 句柄 */ }
impl BmcRaw {
fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result<Vec<u8>> {
// ... 调用 ipmitool ...
Ok(vec![0x00, 0x19, 0x00]) // 存根示例
}
}
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 字节的小端序
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!("糟糕");
}
log_temp(voltage); // 🐛 将电压作为温度传递
Ok(())
}
fn log_temp(t: f64) { println!("温度: {t}°C"); }
| # | Bug | 发现时间 |
|---|---|---|
| 1 | 将风扇转速解析为 1 字节而非 2 字节 | 生产环境,凌晨 3 点 |
| 2 | 电压未进行缩放 | 所有的电源单元 (PSU) 都被标记为过压 |
| 3 | 将摄氏度与转速进行比较 | 可能永远不会被发现 |
| 4 | 电压被传递给了温度记录器 | 6 个月后在查看历史数据时才发现 |
根本原因: 一切都是 Vec<u8> → f64 → 祈祷。
类型化命令模式
第一步 —— 领域新类型
#[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)与Rpm(f64)的说明: 在本章中,内部类型使用u32,因为 IPMI 传感器读数是整数值。在第 6 章(量纲分析)中,Rpm将使用f64以支持算术运算(求平均值、缩放)。两者都是有效的 —— 无论内部类型如何,新类型都能防止单位混淆。
第二步 —— 命令特性(类型索引分派)
关联类型 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>;
}
第三步 —— 每个命令一个结构体
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, "响应为空"));
}
// 注意:第 1 章的非类型化示例使用了 `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!("风扇转速需要 2 个字节,实际得到 {}", 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!("电压需要 2 个字节,实际得到 {}", raw.len())));
}
Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0))
}
}
第四步 —— 执行器(零 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]) // 存根示例
}
}
第五步 —— 所有四个 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 与 Rpm 比较 → "mismatched types" (类型不匹配) ❌
// Bug #4 —— 编译错误:
// log_temperature(voltage);
// ^^^^^^^ 得到的是 Volts,预期是 Celsius ❌
if cpu_temp > Celsius(85.0) { println!("CPU 过热: {:?}", cpu_temp); }
if fan_rpm < Rpm(4000) { println!("风扇转速过慢: {:?}", fan_rpm); }
Ok(())
}
fn log_temperature(t: Celsius) { println!("温度: {:?}", t); }
fn log_voltage(v: Volts) { println!("电压: {:?}", v); }
# IPMI:不会混淆的传感器读取
添加新传感器只需增加一个结构体和一个实现 —— 无需分散的解析代码:
```rust,ignore
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!("功耗需要 2 个字节,实际得到 {}", 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, "未发现存根数据"))?;
cmd.parse_response(raw)
}
}
#[test]
fn read_temp_parses_raw_byte() {
let bmc = StubBmc {
responses: [(0x20, vec![0x19])].into(), // 25 的十六进制 = 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:模式化 (Schema-Typed) 的 REST 端点
Redfish 更加契合这一模式 —— 每一个端点都返回 DMTF 定义的 JSON 模式:
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![]) // 存根示例 —— 实际实现使用 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 传递给热检查函数:
// check_thermals(&power); → "expected ThermalResponse, found PowerResponse"
for temp in &thermal.temperatures {
if let Some(crit) = temp.critical_hi {
if temp.reading > crit {
println!("由于 {} 处于 {}°C(阈值: {}°C),情况紧急 (CRITICAL)!",
temp.name, temp.reading, crit);
}
}
}
Ok(())
}
NVMe Admin:Identify 不会返回日志页
NVMe admin 命令遵循相同的模式。控制器区分命令操作码 (Opcode),但在 C 语言中,调用者必须清楚哪种结构体对应 4 KB 完成缓冲区。类型化命令模式使得这种错误不可能发生:
use std::io;
/// NVMe Admin 命令特性 —— 形状与 IpmiCmd 相同。
pub trait NvmeAdminCmd {
type Response;
fn opcode(&self) -> u8;
fn parse_completion(&self, data: &[u8]) -> io::Result<Self::Response>;
}
// ── Identify (操作码 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, "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 (操作码 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/健康状态
}
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, "日志页过短"));
}
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 { /* 文件描述符, BAR 等 */ }
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]) // 存根示例 —— 实际实现使用 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!("{} (固件 {}): {}°C, {}% 剩余容量",
id.model_number, id.firmware_rev,
smart.temperature_kelvin.saturating_sub(273),
smart.available_spare_pct);
Ok(())
}
上述三个协议的推进遵循了 阶梯式渐进 的方式(这也是第 7 章用于已验证边界的技术):
| 阶段 | 协议 | 复杂度 | 新增内容 |
|---|---|---|---|
| 1 | IPMI | 简单:传感器 ID → 读数 | 核心模式:特性 + 关联类型 |
| 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(())
}
扩展:用于动态脚本的枚举分派
当命令在运行时源自 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"] -->|"类型 Response = Celsius"| C[Celsius]
RF["ReadFanSpeed"] -->|"类型 Response = Rpm"| R[Rpm]
RV["ReadVoltage"] -->|"类型 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 类型化命令
为两个 PLDM 命令设计一个 PldmCmd 特性(形状与 IpmiCmd 相同):
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 } // 固件更新
fn command_code(&self) -> u8 { 0x02 }
fn parse_response(&self, raw: &[u8]) -> io::Result<FwParamsResponse> {
// 简化版 —— 实际实现需要解码 PLDM 固件更新规范中的字段
if raw.len() < 4 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "数据过短"));
}
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![] }) // 存根示例
}
}
关键要点
- 关联类型 = 编译期合同 —— 命令特性上的
type Response将每个请求锁定到精确的一个响应类型。 - 解析逻辑被封装 —— 字节布局知识存在于
parse_response中,而不是散落在调用方代码中。 - 零成本分派 —— 泛型
execute<C: IpmiCmd>会被单态化为直接调用,无需虚表。 - 一个模式,多种协议 —— IPMI、Redfish、NVMe、PLDM、MCTP 都能套用
trait Cmd { type Response; }这一形状。 - 枚举分派连接静态与动态 —— 将类型化命令包装在枚举中,即可在不丢失类型安全的前提下,处理运行时驱动的脚本。
- 渐进的复杂度增强直觉 —— IPMI (传感器 ID → 读数)、Redfish (端点 → JSON 模式) 和 NVMe (操作码 → 4 KB 结构体覆盖) 均使用相同的特性形状,但每一阶都增加了一层解析复杂度。
单次使用类型 —— 通过所有权提供密码学保证 🟡
你将学到:
- Rust 的移动语义 (Move semantics) 如何充当线性类型系统。
- 在编译期消除 Nonce 重用、双重密钥协商以及意外的熔丝重复编程。
Nonce 重用灾难
在认证加密 (如 AES-GCM、ChaCha20-Poly1305) 中,使用相同的密钥重用 Nonce 是 灾难性 的 —— 这会泄露两个明文的异或 (XOR) 结果,通常还会泄露认证密钥本身。这并非理论上的担忧:
- 2016 年:TLS 中 AES-GCM 的“被禁攻击 (Forbidden Attack)” —— Nonce 重用允许恢复明文。
- 2020 年:由于随机数生成器 (RNG) 质量不佳,多个物联网 (IoT) 固件更新系统被发现重用了 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 的所有权系统实际上是一个 线性类型系统 (Linear type system) —— 除非某个值实现了 Copy,否则它只能被使用一次 (被移动)。ring 库利用了这一点:
// ring::aead::Nonce 是:
// - 不支持 Clone
// - 不支持 Copy
// - 使用时按值消耗 (Consumed by value)
pub struct Nonce(/* 私有字段 */);
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 库通过 NonceSequence 走得更远 —— 这是一个用于 生成 Nonce 且同样不可克隆的特性 (Trait):
/// 一个唯一的 Nonce 序列。
/// 不支持 Clone —— 一旦绑定到密钥,就无法被复制。
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 被移动 —— 无法同时用于加密 (sealing) 和解密 (opening)
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,在每次调用中被消耗。 - 密钥复制 ——
UnboundKey被移动到SealingKey中,无法再用于制作OpeningKey。 - 序列复制 ——
NonceSequence不支持Clone,因此没有两个密钥会共享同一个计数器。
这些都不需要运行时检查。 编译器强制执行了这三点。
案例研究:临时密钥协商 (Ephemeral Key Agreement)
临时 Diffie-Hellman 密钥必须 仅使用一次(这就是“临时”的含义)。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() 后,私钥 不再存在于内存中(它已被丢弃)。C++ 开发人员需要记住执行 memset(key, 0, len) 并希望编译器不会将其优化掉。而在 Rust 中,该密钥直接消失了。
硬件应用:一次性熔丝编程
服务器平台具有用于安全密钥、主板序列号和功能位的 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 硬件 ...
// 负载被消耗 —— 尝试再次使用相同负载进行编程将引发编译错误
Ok(())
}
}
/// 带有验证功能的构造者 —— 创建 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("必需提供地址")?;
let data = self.data.ok_or("必需提供数据")?;
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` 检查是弥补这一空隙的 **安全网**。
/// 有关完整的编译期解决方案,请参阅第 5 章中的类型状态 (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,
// 因此在未消耗的情况下丢弃它会产生警告 (或通过 #[must_use] 产生错误)。
ctrl.calibrate(cal_token)?;
// 现在传感器读数可以工作:
let temp = ctrl.read()?;
println!("温度: {temp}°C");
// 不能再执行校准 —— 令牌已被消耗:
// ctrl.calibrate(cal_token); // ❌ 使用了已移动的值
Ok(())
}
何时使用单次使用类型
| 场景 | 是否使用单次使用 (移动) 语义? |
|---|---|
| 密码学 Nonce | ✅ 始终建议 —— Nonce 重用是灾难性的 |
| 临时密钥 (DH, ECDH) | ✅ 始终建议 —— 重用会削弱前向安全性 |
| OTP 熔丝编写 | ✅ 始终建议 —— 重复编写会导致硬件损坏 |
| 许可证激活码 | ✅ 通常建议 —— 阻止重复激活 |
| 校准令牌 | ✅ 通常建议 —— 强制每个会话仅执行一次 |
| 文件写入句柄 | ⚠️ 视情况而定 —— 取决于具体协议 |
| 数据库事务句柄 | ⚠️ 视情况而定 —— 提交/回滚通常是单次使用的 |
| 通用数据缓冲区 | ❌ 这些需要重用 —— 请使用 &mut [u8] |
单次使用所有权流程
flowchart LR
N["Nonce::new()"] -->|移动| E["encrypt(nonce, msg)"]
E -->|已消耗| X["❌ Nonce 已失效"]
N -.->|"重用尝试"| ERR["编译错误:\n使用了已移动的值"]
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 熔丝、校准令牌、审计条目 —— 任何必须至多执行一次的操作均适用。
- 临时密钥免费获得前向安全性 (Forward secrecy) —— 密钥协商过程产生的值被移动到派生的秘密中并随之消失。
- 若有疑问,请移除
Clone—— 你随时可以在以后加上它;但从已发布的 API 中移除它却是破坏性的变更。
能力令牌 —— 零成本的权限证明 🟡
你将学到:
- 零大小类型 (ZSTs) 如何充当编译期证明令牌。
- 在零运行时开销的情况下,强制执行权限层级、上电序列以及可撤销的授权。
问题:谁被允许做什么?
在硬件诊断中,某些操作是 危险 的:
- 编写 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;
}
每个执行危险操作的函数都必须重复这些检查。一旦漏掉一个,就会产生提权漏洞。
作为证明令牌的零大小类型
能力令牌 (Capability token) 是一种零大小类型 (ZST),它证明了调用者拥有执行某项操作的权限。它在运行时占用 零字节 —— 它仅存在于类型系统中:
use std::marker::PhantomData;
/// 证明调用者拥有管理员权限。
/// 零大小 —— 会被完全编译掉。
/// 不支持 Clone,不支持 Copy —— 必须显式传递。
pub struct AdminToken {
_private: (), // 防止在此模块外被构造
}
/// 证明 PCIe 链路已训练完成并准备就绪。
pub struct LinkTrainedToken {
_private: (),
}
pub struct BmcController { /* ... */ }
impl BmcController {
/// 验证管理员身份 —— 返回一个能力令牌。
/// 这是创建 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> {
// 第一步:验证身份 —— 获取管理员证明
let admin = bmc.authenticate_admin(b"secret")?;
// 第二步:训练链路 —— 获取已训练证明
let trained = bmc.train_link()?;
// 第三步:重置 —— 编译器要求提供两个令牌
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, ...) 是一种 证明义务 (Proof obligation) —— “只有当你能提供 AdminToken 时才可调用此函数” —— 而产生该令牌的唯一途径是通过 authenticate_admin()。
上电序列授权
服务器上电序列有严格的顺序要求:待机 (Standby) → 辅助 (Auxiliary) → 主供电 (Main) → CPU。颠倒顺序可能会损坏硬件。能力令牌可以强制执行该顺序:
/// 状态令牌 —— 每个令牌都证明了前一步已完成。
pub struct StandbyOn { _p: () }
pub struct AuxiliaryOn { _p: () }
pub struct MainOn { _p: () }
pub struct CpuPowered { _p: () }
pub struct PowerController { /* ... */ }
impl PowerController {
/// 第一步:启用待机电源。无前置条件。
pub fn enable_standby(&mut self) -> Result<StandbyOn, &'static str> {
println!("待机电源已开启");
Ok(StandbyOn { _p: () })
}
/// 第二步:启用辅助电源 —— 需要待机证明。
pub fn enable_auxiliary(
&mut self,
_standby: &StandbyOn,
) -> Result<AuxiliaryOn, &'static str> {
println!("辅助电源已开启");
Ok(AuxiliaryOn { _p: () })
}
/// 第三步:启用主供电 —— 需要辅助证明。
pub fn enable_main(
&mut self,
_aux: &AuxiliaryOn,
) -> Result<MainOn, &'static str> {
println!("主供电已开启");
Ok(MainOn { _p: () })
}
/// 第四步:为 CPU 供电 —— 需要主供电证明。
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
// }
# 层级化能力
真实系统是具有 **层级 (Hierarchies)** 的 —— 管理员可以执行用户所能执行的一切,甚至更多。可以通过 Trait 层级来对其进行建模:
```rust,ignore
/// 基础能力 —— 任何经过身份验证的人。
pub trait Authenticated {
fn token_id(&self) -> u64;
}
/// 操作员 (Operator) 可以读取传感器并运行非破坏性的诊断。
pub trait Operator: Authenticated {}
/// 管理员 (Admin) 可以执行操作员所能执行的一切,外加破坏性的操作。
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 // 存根示例
}
/// 仅限操作员及以上级别可以运行诊断。
pub fn run_diag(&mut self, _who: &impl Operator, test: &str) -> bool {
true // 存根示例
}
/// 仅限管理员可以刷写固件。
pub fn flash_firmware(&mut self, _who: &impl Admin, image: &[u8]) -> Result<(), &'static str> {
Ok(()) // 存根示例
}
}
AdminCapToken 可以被传递给任何函数 —— 它满足 Authenticated、Operator 和 Admin 的约束。而 UserToken 只能调用 read_sensor()。编译器在 零运行时成本 的前提下强制执行了完整的权限模型。
受生命周期限制的能力令牌
有时能力应当是 作用域限定的 (Scoped) —— 仅在特定的生命周期内有效。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"凭据")?;
let token = session.token();
// 在此作用域内使用令牌...
// 当会话由于作用域结束而被丢弃时,令牌会被借用检查器立即使其失效。
// 无需在运行时进行到期检查。
// drop(session);
// ❌ 错误:由于 session 被(持有其引用的 `token`)借用,因此无法将其移出
//
// 即使我们跳过销毁语句,仅仅尝试在会话超出作用域后使用 `token` ——
// 结果也是一样的:生命周期不匹配的编译错误。
Ok(())
}
何时使用能力令牌
| 场景 | 模式 |
|---|---|
| 特权硬件操作 | ZST 证明令牌 (AdminToken) |
| 多步顺序操作 | 连续的状态令牌链 (StandbyOn → AuxiliaryOn → …) |
| 基于角色的访问控制 (RBAC) | 特性层级 (Authenticated → Operator → Admin) |
| 时间受限的特权 | 受生命周期限制的令牌 (ScopedAdminToken<'a>) |
| 跨模块授权 | 公有令牌类型,私有构造函数 |
开销总结
| 内容 | 运行时开销 |
|---|---|
| 内存中的 ZST 令牌 | 0 字节 |
| 令牌参数传递 | 被 LLVM 优化掉 |
| 特性层级分派 | 静态分派 (单态化) |
| 生命周期强制执行 | 仅在编译期 |
总运行时开销:零。 权限模型仅存在于类型系统中。
能力令牌层级图
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
练习:分层诊断权限
设计一个三层能力系统:ViewerToken、TechToken、EngineerToken。
- 查看员 (Viewers) 可以调用
read_status() - 技术员 (Techs) 还可以调用
run_quick_diag() - 工程师 (Engineers) 还可以调用
flash_firmware() - 更高层级可以执行较低层级的所有操作(使用特性约束或令牌转换)。
点击查看参考答案
// 令牌 —— 零大小,私有构造函数
pub struct ViewerToken { _private: () }
pub struct TechToken { _private: () }
pub struct EngineerToken { _private: () }
// 能力特性 —— 具有层级关系
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 {
"状态:正常 (OK)".into()
}
pub fn run_quick_diag(_tok: &impl CanDiag) -> String {
"诊断:通过 (PASS)".into()
}
pub fn flash_firmware(_tok: &impl CanFlash, _image: &[u8]) {
// 只有工程师能执行到此处
}
关键要点
- ZST 令牌占用零字节 —— 它们仅存在于类型系统中;LLVM 会将其完全优化掉。
- 私有构造函数 = 不可伪造 —— 只有您模块中的
authenticate()函数可以“铸造”令牌。 - 特性层级模拟权限级别 ——
CanFlash: CanDiag: CanView完美映射了现实中的 RBAC。 - 受生命周期限制的令牌自动撤销 ——
ScopedAdminToken<'session>的存活时间无法超过会话本身。 - 与类型状态模式 (第 5 章) 结合 可用于需要身份验证 且 需按步骤操作的协议。
协议状态机 —— 真实硬件中的类型状态 (Type-State) 🔴
你将学到:
- 类型状态编码 (Type-state encoding) 如何将协议违规(如顺序错误的命令、关闭后使用)变为编译期错误。
- 应用于 IPMI 会话生命周期和 PCIe 链路训练的实例。
参考: 第 1 章(第 2 级 —— 状态正确性)、第 4 章(能力令牌)、第 9 章(虚构类型)、第 11 章(技巧 4 —— 类型状态构造者模式,技巧 8 —— 异步类型状态)。
问题:协议违规
硬件协议具有 严格的状态机。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 : 检测到接收器
Polling --> Configuration : 位锁定 + 符号锁定
Configuration --> L0 : 链路编号 + 通道分配
L0 --> L0 : send_tlp() / receive_tlp()
L0 --> Recovery : 错误超过阈值
Recovery --> L0 : 重新训练成功
Recovery --> Detect : 重新训练失败
note right of L0 : 仅在 L0 状态下允许传输 TLP
在 C/C++ 中,通常使用枚举和运行时检查来跟踪状态:
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;
}
类型状态 (Type-State) 模式
通过类型状态,每个协议状态都是一个 独立的类型。状态转移则是通过消耗一个状态并返回另一个状态的方法来实现。编译器确保了在错误状态下无法调用某些方法,因为 那些方法在对应的类型上根本不存在。
use std::marker::PhantomData;
// 状态 —— 零大小的标记类型 (Marker types)
pub struct Idle;
pub struct Authenticated;
pub struct Active;
pub struct Closed;
案例研究:IPMI 会话生命周期
/// 由其当前状态泛型化的 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!("正在 {} 上验证用户 {} ...", self.transport, user);
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> {
// 在 Active 状态下,session_id 保证为 Some。
println!("在会话 {} 上发送命令 0x{cmd:02X} ...", self.session_id.unwrap());
vec![0x00] // 存根示例:完成码 OK
}
pub fn close(self) -> IpmiSession<Closed> {
// 在 Active 状态下,session_id 保证为 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 状态。
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());
// 恢复与重新训练:
let recovery = link.enter_recovery();
let mut link = recovery.retrain(4)?; // 降级到 Gen4
let _resp = link.send_tlp(&[0x03]);
Ok(())
}
将类型状态与能力令牌结合
类型状态和能力令牌可以自然地组合在一起。例如,一个既需要活动的 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. 管理员特权(能力令牌)
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:固件更新 —— 组合了多阶段状态机的综合应用
固件更新的生命周期比会话状态机更为复杂,它还与能力令牌 (第 4 章) 以及单次使用类型 (第 3 章) 进行了组合。这是本书中最复杂的类型状态示例 —— 如果您能很好地理解它,那么您就已经掌握了这一模式。
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(使用单次令牌 VerifiedImage)
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;
// ── 证明映像通过验证的一次性令牌 (第 3 章) ──
pub struct VerifiedImage {
_private: (),
pub digest: [u8; 32],
}
// ── 能力令牌:仅限管理员发起操作 (第 4 章) ──
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!("验证失败 —— 返回到空闲 (Idle) 状态。");
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 | 类型状态 + 能力令牌 (第 4 章) + 一次性证明 (第 3 章) |
每个阶段都在增加复杂度。到第 3 阶段,编译器已经能够强制执行状态顺序、管理员权限以及一次性应用 —— 仅通过一个状态机就消除了三类错误。
何时使用类型状态
| 协议 | 值得使用类型状态吗? |
|---|---|
| IPMI 会话生命周期 | ✅ 是 —— 认证 → 激活 → 命令 → 关闭 |
| PCIe 链路训练 | ✅ 是 —— 检测 → 轮询 → 配置 → L0 |
| TLS 握手 | ✅ 是 —— ClientHello → ServerHello → Finished |
| USB 枚举 | ✅ 是 —— 已连接 → 已上电 → 默认 → 已寻址 → 已配置 |
| 简单的请求/响应 | ⚠️ 可能不值得 —— 仅有 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—— 转移后无法再持有旧状态。 - 与能力令牌结合使用 ——
firmware_update()需要 同时 满足Session<Active>和AdminToken。 - 三个阶段,复杂度递增 —— IPMI(纯状态机)、PCIe LTSSM(带有恢复分支)和固件更新(状态机 + 令牌 + 一次性证明)展示了该模式如何从简单扩展到复杂的组合应用。
- 不要过度使用 —— 只有两个状态的请求/响应协议在不使用类型状态的情况下会更简单。
- 该模式可扩展到完整的 Redfish 工作流 —— 第 17 章将类型状态应用于 Redfish 会话生命周期,第 18 章则在响应构造中使用构造者类型状态。
量纲分析 —— 让编译器检查单位 🟢
你将学到:
- 新类型 (Newtype) 包装与
uom库如何将编译器转变为单位检查引擎。- 防止曾导致价值 3.28 亿美元航天器坠毁的那类 Bug。
火星气候探测者号 (Mars Climate Orbiter)
1999 年,NASA 的火星气候探测者号坠毁,原因是其中一个团队发送的推力数据单位为 磅力-秒 (pound-force seconds),而导航团队预期的单位是 牛顿-秒 (newton-seconds)。这导致航天器进入大气的实际高度为 57 公里而非 226 公里,最终在大气层中解体。 损失:3.276 亿美元。
根本原因在于:这两个值都是 double 类型。编译器无法区分它们。
这种同样的 Bug 潜伏在每一个涉及物理量的硬件诊断程序中:
// C 语言 —— 全是 double,没有单位检查
double read_temperature(int sensor_id); // 摄氏度?华氏度?开尔文?
double read_voltage(int channel); // 伏特?毫伏?
double read_fan_speed(int fan_id); // RPM?弧度/秒?
// Bug:将摄氏度与华氏度进行逻辑比较
if (read_temperature(0) > read_temperature(1)) { ... } // 单位可能不同!
物理量的新类型
最简单的“正确构建 (Correct-by-construction)”方法是:将每个单位包装在它自己的类型中。
use std::fmt;
/// 以摄氏度 (°C) 为单位的温度。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
/// 以华氏度 (°F) 为单位的温度。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);
/// 以伏特 (V) 为单位的电压。
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
/// 以毫伏 (mV) 为单位的电压。
#[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 与 Volts
// }
运行时零开销 —— 新类型在编译后会还原为原始的 f64 值。包装类纯粹是一个类型层面的概念。
硬件物理量的 Newtype 宏
手动编写新类型会变得很啰嗦。使用宏可以消除这些样板代码:
/// 为物理量生成一个新类型。
macro_rules! quantity {
($Name:ident, $unit:expr) => {
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct $Name(pub f64);
impl $Name {
pub fn new(value: f64) -> Self { $Name(value) }
pub fn value(self) -> f64 { self.0 }
}
impl std::fmt::Display for $Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.2} {}", self.0, $unit)
}
}
impl std::ops::Add for $Name {
type Output = Self;
fn add(self, rhs: Self) -> Self { $Name(self.0 + rhs.0) }
}
impl std::ops::Sub for $Name {
type Output = Self;
fn sub(self, rhs: Self) -> Self { $Name(self.0 - rhs.0) }
}
};
}
// 用法:
quantity!(Celsius, "°C");
quantity!(Fahrenheit, "°F");
quantity!(Volts, "V");
quantity!(Millivolts, "mV");
quantity!(Rpm, "RPM");
quantity!(Watts, "W");
quantity!(Amperes, "A");
quantity!(Pascals, "Pa");
quantity!(Hertz, "Hz");
quantity!(Bytes, "B");
每一行都会生成一个完整的类型,包含 Display、Add、Sub 以及比较运算符。运行时开销全部为零。
物理学警示: 宏为 所有 物理量生成的
Add实现中包含了Celsius。将绝对温度相加 (25°C + 30°C = 55°C) 在物理学上是没有意义的 —— 通常你需要一个独立的TemperatureDelta类型来处理差值。uom库(稍后展示)正确处理了这一点。对于仅进行比较和显示的简单传感器诊断,你可以不在温度类型中实现Add/Sub,而仅在加法有意义的地方(瓦特、伏特、字节)保留它们。如果确实需要做差值运算,可以定义一个CelsiusDelta(f64)新类型并实现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 步进代表的 ℃ 数
}
pub struct VoltageCalibration {
pub reference_mv: f64,
pub divider_ratio: f64,
}
impl TemperatureCalibration {
/// 将 raw ADC 转换为摄氏度 (°C)。返回类型保证了输出结果为 Celsius。
pub fn convert(&self, adc: AdcReading) -> Celsius {
Celsius::new(adc.raw as f64 * self.scale + self.offset)
}
}
impl VoltageCalibration {
/// 将 raw ADC 转换为伏特 (V)。返回类型保证了输出结果为 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!("温度: {temp}, 状态: {result:?}");
// 编译错误 —— 不能将摄氏度读数与伏特阈值进行比较:
// 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 库
对于生产环境,uom 库提供了包含数百种单位、自动转换且零运行时开销的全面量纲分析体系:
// 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。如果你只需要简单物理量,而不需要派生单位算术运算,则使用手写的新类型。
何时使用量纲类型
| 场景 | 建议 |
|---|---|
| 传感器读数 (温度, 电压, 风扇) | ✅ 始终如此 —— 防止单位混淆 |
| 阈值比较 | ✅ 始终如此 —— 使用泛型 Threshold<T> |
| 跨子系统数据交换 | ✅ 始终如此 —— 在 API 边界强制执行契约 |
| 内部计算 (全程使用相同单位) | ⚠️ 可选 —— 这种情况下出错概率较低 |
| 字符串/显示格式化 | ❌ 为该物理量类型实现 Display 特性即可 |
传感器流水线类型流转图
flowchart LR
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) 新类型。实现:
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 → "mismatched types"
// let bad = Watts(100.0) + Celsius(50.0);
关键要点
- 新类型在零成本下防止单位混淆 ——
Celsius与Rpm内部都是f64,但编译器将其视为不同的类型。 - 火星气候探测者号的 Bug 不再可能发生 —— 将磅力单位传递给预期为牛顿单位的函数将引发编译错误。
quantity!宏能减少样板代码 —— 为每种单位快速生成 Display、算术运算和阈值逻辑。uom库处理派生单位 —— 当你需要自动计算瓦特 = 伏特 × 安培时,请使用它。- 阈值对物理量是泛型的 ——
Threshold<Celsius>无法意外地与Threshold<Rpm>进行比较。
已验证边界 —— 解析,而非验证 🟡
你将学到:
- 如何在系统边界仅验证一次数据,将验证后的证明携带在专门的类型中,并永不重复检查。
- 应用于 IPMI FRU 记录(扁平字节)、Redfish JSON(结构化文档)以及 IPMI SEL 记录(带有嵌套分派的多态二进制)的实例。
- 包含一个完整的端到端演练。
参考: 第 2 章(类型化命令)、第 6 章(量纲类型)、第 11 章(技巧 2 —— 密封特性,技巧 3 ——
#[non_exhaustive],技巧 5 —— FromStr)、第 14 章(proptest)。
问题:散弹枪式验证 (Shotgun Validation)
在典型的代码中,验证逻辑往往分散在各处。每个接收数据的函数都会为了“以防万一”而重新检查一遍:
// 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。
解析,而非验证 (Parse, Don’t Validate)
“正确构建 (Correct-by-construction)”的方法是:在边界处仅验证一次,然后将验证后的证明携带在类型中。
/// 来自线路的原始字节 —— 尚未经过验证。
#[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
}
}
/// 此函数不需要验证 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 响应。解析一次,将有效性携带在类型中:
```rust,ignore
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)。现实世界的数据通常是 多态的 (Polymorphic):后续字节的解释取决于之前的字节。IPMI 系统事件日志 (SEL) 记录就是最典型的示例。
问题的形态
每条 SEL 记录都恰好是 16 字节。但这些字节的 含义 取决于一套分派链 (Dispatch chain):
字节 2:记录类型 (Record Type)
├─ 0x02 → 系统事件 (System Event)
│ 字节 10[6:4]:事件类型 (Event Type)
│ ├─ 0x01 → 阈值事件 (读数 + 阈值位于数据字节 2-3)
│ ├─ 0x02-0x0C → 离散事件 (位于偏移量字段中的位)
│ └─ 0x6F → 传感器特定 (含义取决于字节 7 的传感器类型)
│ 字节 7:传感器类型 (Sensor Type)
│ ├─ 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) { // 阈值 (threshold)
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) { // 内存 (memory)
// 🐛 忘记检查事件数据 1 的偏移量位
printf("Memory ECC error\n");
}
// 🐛 缺少 else —— 默默地丢弃了 30 多种其他传感器类型
}
}
// 🐛 OEM 记录类型被默默忽略
}
第一步 —— 解析外层框架 (Outer Frame)
第一个 TryFrom 在记录类型上进行分派 —— 这是联合体 (Union) 的最外层:
/// 来自 `Get SEL Entry` (IPMI 命令 0x43) 的原始 16 字节 SEL 记录。
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)),
}
}
}
在这个边界之后,所有消费者都在枚举上进行匹配。编译器强制要求处理全部三种记录类型 —— 你绝不会“忘记”处理 OEM 记录。
第二步 —— 解析系统事件:传感器类型 → 类型化事件
内部分派将事件数据字节转换为一个由传感器类型索引的和类型 (Sum type)。在这里,C 语言中的嵌套 switch 变成了一个嵌套枚举:
#[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, // ← 关键点:事件数据是类型化的 (TYPED)
}
#[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 的传感器类型。标记为 non-exhaustive,
/// 因为未来的 IPMI 修订版及 OEM 范围会增加更多变体(参见第 11 章技巧 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 线性化之后,它们将变为量纲类型 (第 6 章)。
#[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,
}
/// 传感器特定事件 —— 每种传感器类型对应一个变体,
/// 并且带有一个包含该传感器定义的事件的穷尽枚举。
#[derive(Debug)]
pub enum SensorSpecificEvent {
Temperature(TempEvent),
Voltage(VoltageEvent),
Fan(FanEvent),
Processor(ProcessorEvent),
PowerSupply(PowerSupplyEvent),
Memory(MemoryEvent),
PhysicalSecurity(PhysicalSecurityEvent),
Watchdog(WatchdogEvent),
}
// ──── 每种传感器类型的事件枚举(来自 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,
}
## 第三步 —— 解析器组装 (The Parser Wiring)
```rust,ignore
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 => {
// 阈值 (Threshold) —— 事件数据第 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 => {
// 传感器特定 (Sensor-specific) —— 根据传感器类型进行分派
let offset = event_data[0] & 0x0F;
let specific = parse_sensor_specific(&sensor_type, offset)?;
TypedEvent::SensorSpecific(specific)
}
0x02..=0x0C => {
// 通用离散 (Generic discrete)
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))
}
// 对于温度、电压、风扇等,也是同样的模式。
// 每种传感器类型都将其偏移量映射到专用枚举。
_ => Err(SelParseError::InvalidEventData {
reason: "尚未为此传感器类型实现特定的分派逻辑",
}),
}
}
第四步 —— 使用类型化的 SEL 记录
一旦解析完成,下游代码将在嵌套枚举上进行模式匹配。编译器会强制执行穷尽性检查 —— 不会有默默的失败,也不会遗忘任何传感器类型:
/// 确定一个 SEL 事件是否应当触发硬件警报。
/// 编译器核心保证了每个变体都得到了处理。
fn should_alert(record: &ValidSelRecord) -> bool {
match record {
ValidSelRecord::SystemEvent(sys) => match &sys.event {
TypedEvent::Threshold(t) => {
// 任何严重 (Critical) 或不可恢复 (Non-recoverable) 的阈值越界 → 警报
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
),
// 如果未来版本增加了新的传感器类型变体?
// ❌ 编译错误:未覆盖所有模式 (non-exhaustive patterns)
_ => 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 => "已产生 (asserted)",
EventDirection::Deassertion => "已消除 (deasserted)",
};
match &sys.event {
TypedEvent::Threshold(t) => {
format!("{sensor}: {:?} {} (读数: 0x{:02X}, 阈值: 0x{:02X})",
t.crossing, dir, 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),
}
}
演练:端到端 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 线性化 (第 6 章) 后,这会变为:
// 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,产生 (assertion)
[
0x01, 0x00, // 记录 ID: 1
0x02, // 记录类型:系统事件
0x00, 0x00, 0x00, 0x00, // 时间戳(存根)
0x20, // 生成者:IPMB 从节点地址 0x20
0x00, // 通道/LUN
0x04, // 事件消息版本
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, // 事件消息版本
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]: Memory 传感器 #3: Memory(CorrectableEcc) 已产生 (asserted)
SEL[1]: Temperature 传感器 #1: UpperCriticalHigh 已产生 (asserted) (读数: 0x5F, 阈值: 0x5A)
→ 原始读数: 0x5F, 原始阈值: 0x5A
=== 警报清单 (1) ===
🚨 Temperature 传感器 #1: UpperCriticalHigh 已产生 (asserted) (读数: 0x5F, 阈值: 0x5A)
条目 0 (可纠正 ECC 错误) 被记录但未报警。条目 1 (达到上临界温度点) 触发了警报。这两个决定都是通过穷尽模式匹配强制执行的 —— 编译器证明了每种传感器类型以及每种阈值越界情况都得到了妥善处理。
从解析后的事件到 Redfish 健康评估:消费者流水线
上面的演练以报警结束 —— 但在真实的 BMC 中,解析后的 SEL 记录会流入 Redfish 健康汇总评估 (第 18 章)。目前普遍的做法往往只是交接一个弱类型的 bool 标志:
// ❌ 有损转换 —— 丢弃了每个子系统的细节
pub struct SelSummary {
pub has_critical_events: bool,
pub total_entries: u32,
}
这会导致类型系统刚才赋予我们的一切优势尽失:受影响的是哪个子系统、严重级别是多少、读数是否携带量纲数据。让我们来构建完整的流水线。
第 1 步 —— SDR 线性化:原始字节 → 量纲类型 (第 6 章)
阈值 SEL 事件在事件数据字节 2-3 中携带原始传感器读数。IPMI SDR (传感器数据记录) 提供了线性化公式。由于线性化,原始字节将变为特定的量纲类型:
/// 单个传感器的 SDR 线性化系数。
/// 参见 IPMI 规范 36.3 节的完整公式。
pub struct SdrLinearization {
pub sensor_type: SensorType,
pub m: i16, // 乘数 (multiplier)
pub b: i16, // 偏移量 (offset)
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);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32);
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 演练中,原始字节 0x5F (十进制 95) 就变成了 Celsius(95.0) —— 和编译器会阻止它与 Rpm 或 Watts 进行误比较。
第 2 步 —— 各子系统健康分类
不要把所有信息都压缩成一个 has_critical_events: bool,而是将解析后的每个 SEL 条目分入各子系统的健康桶中:
/// “最差情况”健康值 —— Ord 特性赋予了我们免费的 `.max()` 使用权。
/// (具体定义见第 18 章;此处为 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 {
// 非临界 (Non-critical) → Warning
ThresholdCrossing::UpperNonCriticalLow
| ThresholdCrossing::UpperNonCriticalHigh
| ThresholdCrossing::LowerNonCriticalLow
| ThresholdCrossing::LowerNonCriticalHigh => HealthValue::Warning,
// 临界 (Critical) 或不可恢复 (Non-recoverable) → Critical
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 { .. } => {
// 通用离散事件 —— 按传感器类型分类,默认为 Warning
match record.sensor_type {
SensorType::Processor => SubsystemHealth::Processor(HealthValue::Warning),
SensorType::Memory => SubsystemHealth::Memory(HealthValue::Warning),
_ => SubsystemHealth::Thermal(HealthValue::OK),
}
}
}
}
第 3 步 —— 聚合为类型化的 SEL 摘要
用一个结构化摘要来替换那个有损的 bool 值,从而保留每个子系统的健康信息:
use std::collections::HashMap;
/// 丰富的 SEL 摘要 —— 基于类型化事件得出的分子系统健康状态。
/// 此摘要将被交接给 Redfish 服务器 (第 18 章) 进行健康汇总评估。
#[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]\nSEL 条目"]
PARSE["TryFrom:\nValidSelRecord\n(枚举树)"]
CLASSIFY["classify_event_health\n(穷尽匹配)"]
LINEARIZE["SDR 线性化\n原始 → Celsius/Rpm/Watts"]
SUMMARY["TypedSelSummary\n(分子系统健康\n+ 量纲读数)"]
REDFISH["第 18 章: 健康汇总\n→ 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,00,00],
// 温度上临界点 (传感器 #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
}
// ── 此摘要直接供第 18 章的健康汇总逻辑使用 ──
// compute_system_health() 现在可以使用分子系统的值,
// 而不仅仅是一个简单的 `has_critical_events: bool`
}
消费者流水线证明了什么
| 阶段 | 模式 | 强制执行了什么 |
|---|---|---|
| 解析 (Parse) | 已验证边界 (ch07) | 每个消费者都使用类型化枚举,绝不接触原始字节 |
| 分类 (Classify) | 穷尽匹配 | 每种传感器类型和事件变体都映射到一个健康值 —— 不会遗漏 |
| 线性化 (Linearize) | 量纲分析 (ch06) | 原始字节 0x5F 变为 Celsius(95.0),而非 f64 —— 不会与 RPM 混淆 |
| 聚合 (Aggregate) | 类型化折叠 | 分子系统健康状态使用 HealthValue::max() —— Ord 特性保证了正确性 |
| 交接 (Handoff) | 结构化摘要 | 第 18 章接收包含 7 个子系统健康值的 TypedSelSummary,而非一个 bool |
与无类型的 C 语言流水线对比:
| 步骤 | C 语言 | Rust 语言 |
|---|---|---|
| 解析记录类型 | 带有潜在“跌落 (fallthrough)”风险的 switch | 在枚举上进行 match —— 穷尽匹配 |
| 分类严重性 | 手动的 if 链,遗漏了电源子系统 | 穷尽的 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 如何防止 |
|---|---|
| 忘记检查记录类型 | 在 ValidSelRecord 上 match —— 必须处理全部三个变体 |
| 触发读数的字节索引出错 | 仅在边界处解析一次,存入 ThresholdEvent.trigger_reading —— 消费者永不接触原始字节 |
漏掉了某种传感器类型的 case | SensorSpecificEvent 的匹配是穷尽的 —— 漏掉变体无法通过编译 |
| 默默丢弃了 OEM 记录 | 枚举变体存在 —— 必须被处理或显式使用 _ => 忽略 |
| 将阈值读数 (°C) 与风扇偏移量进行比较 | 在 SDR 线性化之后,Celsius ≠ Rpm (第 6 章) |
| 增加了新传感器类型,却漏掉了报警逻辑 | #[non_exhaustive] + 穷尽匹配 → 在下游 crate 中会引发编译错误 |
| 事件数据在两个代码路径中解析方式不一致 | 唯一的 parse_system_event() 边界 —— 单一事实来源 |
“三段式”模式 (The Three-Beat Pattern)
回顾本章的三个案例研究,你会发现一个 层层递进的弧度:
| 案例研究 | 输入形态 | 解析复杂度 | 核心技术 |
|---|---|---|---|
| FRU (字节) | 扁平、固定布局 | 一个 TryFrom,检查各字段 | 已验证边界类型 |
| Redfish (JSON) | 结构化、已知模式 | 一个 TryFrom,检查字段 + 嵌套 | 同样的技术,不同的传输层 |
| SEL (多态字节) | 嵌套的受限联合体 | 分派链:记录类型 → 事件类型 → 传感器类型 | 枚举树 + 穷尽匹配 |
这三者的原理是完全一致的:在边界处仅验证一次,将证明携带在类型中,永不重复检查。 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 配置空间 (configuration space) | ✅ 总是 —— 寄存器布局严格 |
| 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 (Memory Device) 记录设计一个 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 这样的多态数据,编译器的枚举穷尽性检查防止了“忘记某种传感器类型”这类 Bug,且具有零运行时开销。
- 消费者流水线保留了结构 —— 解析 → 分类 → 线性化 → 聚合,使得子系统健康和量纲读数保持完整,而 C 语言的有损处理则将其削减为单一的
bool。类型系统使得保留信息比丢弃信息更容易。 serde是天然的边界 —— 带有#[serde(try_from)]的#[derive(Deserialize)]可以在解析时验证 JSON。- 组合已验证类型 —— 一个
ValidServerHealth可以要求ValidFru+ValidThermal+ValidPower。 - 配合 proptest (第 14 章) —— 对
TryFrom边界进行模糊测试,确保不拒绝任何有效的输入,且没有无效的输入能混入。 - 这些模式组合成了完整的 Redfish 工作流 —— 第 17 章在客户端应用已验证边界(将 JSON 响应解析为类型化结构体),而第 18 章在服务器端反向应用此模式(构建器类型状态确保在序列化之前每个必填字段都已准备就绪)。此处构建的 SEL 消费者流水线直接服务于第 18 章的
TypedSelSummary健康汇总。
能力混入 —— 编译时硬件契约 🟡
你将学到:
- 如何将成分特性 (Ingredient Traits,即总线能力) 与混入特性 (Mixin Traits) 以及一揽子实现 (Blanket Impls) 结合使用。
- 这种模式如何消除诊断代码的重复,同时在编译时保证满足每一项硬件依赖。
问题:诊断代码重复
服务器平台通常会在各个子系统之间共享诊断模式。风扇诊断、温度监控和上电时序控制都遵循类似的工作流,但它们运行在不同的硬件总线上。如果没有抽象,你只能靠复制粘贴:
// 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 Mixins),每种硬件总线都被视为一种 成分特性 (Ingredient Trait),而只要具备了正确的“成分”,诊断行为就会被自动提供。
成分特性 (硬件能力)
每种总线或外设都是特性 (Trait) 上的一个关联类型。诊断控制器会声明它拥有哪些总线:
/// SPI 总线能力。
pub trait HasSpi {
type Spi: SpiBus;
fn spi(&self) -> &Self::Spi;
}
/// I2C 总线能力。
pub trait HasI2c {
type I2c: I2cBus;
fn i2c(&self) -> &Self::I2c;
}
/// GPIO 引脚访问能力。
pub trait HasGpio {
type Gpio: GpioController;
fn gpio(&self) -> &Self::Gpio;
}
/// IPMI 访问能力。
pub trait HasIpmi {
type Ipmi: IpmiClient;
fn ipmi(&self) -> &Self::Ipmi;
}
// 总线特性的定义:
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);
}
}
# 混入特性 (诊断行为)
混入特性会为任何实现其所需能力的类型 **自动提供** 行为:
```rust,ignore
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}: 失败 ({speed} RPM)");
return false;
}
}
true
}
}
// 一揽子实现 —— 任何拥有 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}: 危急温度 ({temp}°C)");
return false;
}
if self.check_thermal_alert(addr as u32) {
println!("传感器 0x{addr:02X}: 告警引脚已响应 (Alert pin asserted)");
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
}
}
}
# 具体控制器 —— 自由混合与匹配
具体的诊断控制器只需声明其拥有的能力,就能 **自动继承** 所有匹配的混入行为:
```rust,ignore
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)
// 无需手动实现这些特性 —— 一揽子实现已经完成了一切。
“正确构建”的体现
混入模式之所以体现了“正确构建”,是因为:
- 没有 SPI 就无法调用
read_fan_speed()—— 该方法仅存在于实现了HasSpi + HasI2c的类型上。 - 总线不可被遗漏 —— 如果你从
BaseBoardController中移除了HasSpi,FanDiagMixin的方法会在编译时立即消失。 - Mock 测试是全自动的 —— 将
LinuxSpi替换为MockSpi,所有的混入逻辑都能直接与 Mock 对象配合工作。 - 新平台只需声明能力 —— 一个只有 I2C 和 GPIO 的 GPU 扩展卡会自动获得
TempMonitorMixin,但由于没有 SPI,它无法获得FanDiagMixin。
何时使用能力混入
| 场景 | 是否使用混入? |
|---|---|
| 横向切分的诊断行为 | ✅ 是 —— 防止复制粘贴 |
| 多总线硬件控制器 | ✅ 是 —— 声明能力,获取行为 |
| 平台特定的测试框架 | ✅ 是 —— 模拟各种能力进行测试 |
| 单总线的简单外设 | ⚠️ 开销可能不划算 |
| 纯业务逻辑 (无硬件相关) | ❌ 有更简单的模式可用 |
## 混入特性架构
```mermaid
flowchart TD
subgraph "成分特性 (Ingredient Traits)"
SPI["HasSpi"]
I2C["HasI2c"]
GPIO["HasGpio"]
end
subgraph "混入特性 (Mixin Traits, 一揽子实现)"
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
练习:网络诊断混入
为网络诊断设计一套混入系统:
- 成分特性:
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() { "链路: 正常 (UP)" } else { "链路: 断开 (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() {
"远程诊断: 健康"
} else {
"远程诊断: 降级"
}
}
}
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 会自动获得上述两个混入特性提供的方法
关键要点
- 成分特性用于声明硬件能力 ——
HasSpi,HasI2c,HasGpio是基于关联类型的特性。 - 混入特性通过一揽子实现提供行为 ——
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}。 - 适配新平台只需列出其能力 —— 编译器会自动为你匹配并提供所有符合条件的混入方法。
- 移除总线会导致编译器在所有使用处报错 —— 你不会在下游代码中遗忘更新。
- Mock 测试是免费的 —— 只要把
LinuxSpi换成MockSpi,所有的混入逻辑都能在不改动一行代码的情况下继续运行。
用于资源追踪的幽灵类型 🟡
你将学到:
- 如何使用
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 Type Parameters)
幽灵类型 是指在结构体定义中出现,但在任何字段中都不使用的类型参数。它的存在纯粹是为了携带类型层面的信息:
use std::marker::PhantomData;
// 寄存器宽度标记 —— 零大小 (zero-sized)
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 缓冲区具有方向性:有些是用于“设备到主机” (FromDevice, 读),有些则是用于“主机到设备” (ToDevice, 写)。使用错误的方向会破坏数据或导致总线错误:
```rust,ignore
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]); // ❌ DmaBuffer<FromDevice> 上没有 `write_data` 方法
// }
// 无法读取 ToDevice 缓冲区的数据:
// fn oops2(buf: &DmaBuffer<ToDevice>) {
// let data = buf.read_data(); // ❌ DmaBuffer<ToDevice> 上没有 `read_data` 方法
// }
文件描述符的所有权
一个常见的 Bug 是:在文件描述符被关闭之后仍然使用它。幽灵类型可以追踪文件的“打开/关闭”状态:
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 句柄被消耗(通过 self),防止了 closure-after-close。
pub fn close(self) -> Fd<Closed> {
// ... 关闭 fd ...
Fd { raw: self.raw, _state: PhantomData }
}
}
impl Fd<Closed> {
// Fd<Closed> 上不存在 read() 或 write() 方法。
// 这使得“关闭后使用”变成了一个编译错误。
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)?; // ❌ Fd<Closed> 上没有 `read` 方法
// closed.write(&[1])?; // ❌ Fd<Closed> 上没有 `write` 方法
Ok(())
}
# 将幽灵类型与之前的模式相结合
幽灵类型可以与我们之前见过的所有模式组合使用:
```rust,ignore
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);
/// 将幽灵类型 (寄存器宽度) 与量纲类型 (Celsius) 相结合。
fn read_temp_sensor(reg: &Register<Width16>) -> Celsius {
let raw = reg.read(); // 由幽灵类型保证返回 u16
Celsius(raw as f64 * 0.0625) // 由返回类型保证封装为 Celsius
}
// 编译器会强制执行:
// 1. 寄存器必须是 16 位的 (幽灵类型)
// 2. 结果必须是 Celsius (新类型)
// 且两者的运行时开销均为零。
何时使用幽灵类型
| 场景 | 是否使用幽灵参数? |
|---|---|
| 寄存器宽度编码 | ✅ 总是 —— 防止宽度不匹配 |
| DMA 缓冲区方向 | ✅ 总是 —— 防止数据损坏 |
| 文件描述符状态 | ✅ 总是 —— 防止关闭后使用 |
| 内存区域权限 (R/W/X) | ✅ 总是 —— 强制执行访问控制 |
| 通用容器 (Vec, HashMap) | ❌ 否 —— 直接使用具体的类型参数即可 |
| 运行时变量属性 | ❌ 否 —— 幽灵类型仅在编译时有效 |
幽灵类型资源矩阵
flowchart TD
subgraph "宽度标记 (Width Markers)"
W8["Width8"]
W16["Width16"]
W32["Width32"]
end
subgraph "方向标记 (Direction Markers)"
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
练习:内存区域权限
为具有读、写和执行权限的内存区域设计幽灵类型:
MemRegion<ReadOnly>拥有fn read(&self, offset: usize) -> u8。MemRegion<ReadWrite>同时拥有read和write。MemRegion<Executable>拥有read和fn execute(&self)。- 对
ReadOnly进行写入,或对ReadWrite执行execute,都应当无法通过编译。
点击查看参考答案
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()方法。 - 与量纲类型相结合 (第 6 章) ——
Register<Width16>可以通过解析步骤直接返回Celsius类型。 - 幽灵类型仅限编译时 —— 它们无法处理运行时的变量属性;对于这类需求请使用枚举。
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!,这能将编译器变成一个验证你所设定的不变式的定理证明器 (Theorem Prover):
pub const fn checked_add(a: u32, b: u32) -> u32 {
let sum = a as u64 + b as u64;
assert!(sum <= u32::MAX as u64, "overflow");
sum as u32
}
// ✅ 编译通过 —— 100 + 200 符合 u32 范围
const X: u32 = checked_add(100, 200);
// ❌ 编译错误:"overflow"
// const Y: u32 = checked_add(u32::MAX, 1);
fn main() {
println!("{X}");
}
核心洞察:
const fn+assert!= 一个证明责任 (Proof Obligation)。每一个断言都是一个编译器必须验证的定理。如果证明失败,程序就无法通过编译。无需测试套件,无需人工代码审查来捕捉这类错误 —— 编译器本身就是审计员。
构建一个已验证的 SRAM 内存映射
Region 类型
Region 代表一个连续的内存块。其构造函数是一个强制执行基本合法性的 const fn:
#[derive(Debug, Clone, Copy)]
pub struct Region {
pub base: u32,
pub size: u32,
}
impl Region {
/// 创建一个 Region。如果不变式验证失败,则在编译时发生 panic。
pub const fn new(base: u32, size: u32) -> Self {
assert!(size > 0, "region 长度必须非零");
assert!(
base as u64 + size as u64 <= u32::MAX as u64,
"region 超出了 32 位地址空间"
);
Self { base, size }
}
pub const fn end(&self) -> u32 {
self.base + self.size
}
/// 如果 `inner` 完全位于 `self` 内部,则返回 true。
pub const fn contains(&self, inner: &Region) -> bool {
inner.base >= self.base && inner.end() <= self.end()
}
/// 如果两个 region 存在重叠地址,则返回 true。
pub const fn overlaps(&self, other: &Region) -> bool {
self.base < other.end() && other.base < self.end()
}
/// 如果 `addr` 落在该 region 内部,则返回 true。
pub const fn contains_addr(&self, addr: u32) -> bool {
addr >= self.base && addr < self.end()
}
}
// 每一个 Region 自诞生起就是合法的 —— 你无法构造一个无效的 Region
const R: Region = Region::new(0x2000_0000, 1024);
fn main() {
println!("Region: {:#010X}..{:#010X}", R.base, R.end());
}
已验证的内存映射
现在我们将多个 region 组合成一个完整的 SRAM 映射。构造函数在编译阶段证明了六个无重叠不变式 (Overlap-freedom Invariants) 和四个包含关系不变式 (Containment Invariants):
#[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, "region 长度必须非零");
assert!(base as u64 + size as u64 <= u32::MAX as u64, "overflow");
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 {
// ── 包含关系:每一个子 region 必须位于 SRAM 总范围内 ──
assert!(total.contains(&bootloader), "引导加载程序超出了 SRAM 范围");
assert!(total.contains(&firmware), "固件超出了 SRAM 范围");
assert!(total.contains(&data), "数据段超出了 SRAM 范围");
assert!(total.contains(&stack), "栈超出了 SRAM 范围");
// ── 无重叠:任何一对子 region 之间都不能共享地址 ──
assert!(!bootloader.overlaps(&firmware), "引导加载程序与固件重叠");
assert!(!bootloader.overlaps(&data), "引导加载程序与数据段重叠");
assert!(!bootloader.overlaps(&stack), "引导加载程序与栈重叠");
assert!(!firmware.overlaps(&data), "固件与数据段重叠");
assert!(!firmware.overlaps(&stack), "固件与栈重叠");
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), // 引导加载程序:16 KB
Region::new(0x2000_4000, 128 * 1024), // 固件:128 KB
Region::new(0x2002_4000, 64 * 1024), // 数据段:64 KB
Region::new(0x2003_4000, 48 * 1024), // 栈: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);
}
进行了 10 次编译时检查,产生的运行时指令为零。二进制文件中仅包含已验证过的常数。
破坏内存映射
假如有人在没有调整其他任何内容的情况下,将数据段从 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,现在变成了一个编译错误。 无需单元测试,无需人工代码审查捕捉 —— 编译器证明了该 Bug 的“不可能存在”。与 C 语言相比,同样的 Bug 会被静默部署,并在数月后的实际运行中表现为一次栈破坏。
使用幽灵类型分层访问控制
结合 const fn 验证与由幽灵类型 (第 9 章) 处理的访问权限,能够在类型层面上强制执行读取 / 写入约束:
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, "region 长度必须非零");
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); // ✅ 从只读 region 读取
read_word(&DATA, 0); // ✅ 从读写 region 读取
write_word(&DATA, 0, 42); // ✅ 向读写 region 写入
// write_word(&BOOTLOADER, 0, 42); // ❌ 编译错误:需要 ReadWrite,但传入的是 ReadOnly
}
引导加载程序所在的 Region 在物理上是可写的 (它是 SRAM),但类型系统防止了意外的写入。这种 硬件能力 与 软件权限 之间的清晰界定,正是“正确构建”的真谛。
指针来源:证明地址确实属于特定 Region
更进一步,我们可以创建已验证的地址 —— 即在静态环境中已证明位于特定 Region 内部的数值:
#[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()
}
}
/// 该地址在编译时已证明位于某个 Region 内部。
pub struct VerifiedAddr {
addr: u32, // 私有字段 —— 只能通过受检构造函数创建
}
impl VerifiedAddr {
/// 如果 `addr` 位于 `region` 外部,则在编译时发生 panic。
pub const fn new(region: &Region, addr: u32) -> Self {
assert!(region.contains_addr(addr), "地址位于 Region 外部");
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);
// ❌ 无法通过编译:地址位于引导加载程序 Region 而非数据段
// const BAD_ADDR: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2000_0000);
fn main() {
println!("状态寄存器地址:{:#010X}", STATUS_WORD.raw());
println!("配置寄存器地址:{:#010X}", CONFIG_WORD.raw());
}
指针来源 (Provenance) 在编译时确立 —— 在访问这些地址时,不再需要任何运行时的越界检查。由于构造函数是私有的,只有在编译器证明地址合法时,VerifiedAddr 实例才可能存在。
内存映射之外
const fn 证明模式适用于任何具有 编译时已知数值且具备结构化不变式 的场景。上面的 SRAM 映射证明了 Region 之间 (Inter-region) 的属性(包含关系、非重叠)。同样的技巧可以在日益精细的领域中进行扩展:
flowchart TD
subgraph coarse["粗粒度"]
MEM["内存映射<br/>Region 互不重叠"]
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 寄存器:偏移量={:#04X}, 位宽={}", DATA.offset, DATA.width);
println!("UART STATUS 寄存器:偏移量={:#04X}, 位宽={}", 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: {}/{} bytes", CRC.end(), MAX_FRAME);
}
通过这种构建方式,字段之间是连续的 —— 每个字段都紧跟在上一个字段的结尾。最后的断言证明了帧的大小符合协议的最大限制。
内联 const 代码块:用于泛型验证
自 Rust 1.79 起,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 字节对齐
这些断言在函数被单态化 (Monomorphized) 时求值 —— 每一个具有不同 N 的调用点都会获得其专属的编译时检查。
寄存器内的位域布局
寄存器映射证明了寄存器之间 互不重叠 —— 但 单个寄存器内部的位域 呢?控制寄存器会将多个字段打包进一个字 (Word) 中。如果两个字段共享了同一个位位置,读取和写入操作就会发生静默的数据篡改。在 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);
}
这与上文的寄存器映射模式形成了互补 —— 寄存器映射证明了 寄存器之间 (Inter-register) 的互斥性,而位域布局证明了 寄存器内部 (Intra-register) 的互斥性。二者结合,提供了从寄存器块一直到具体比特位的全方位覆盖。
时钟树 / PLL 配置
微控制器通过倍频器 / 分频器链条来派生外设时钟。PLL 会产生 f_vco = f_in × N / M,且 VCO 频率必须保持在硬件规定的范围内。对于特定单板,一旦参数设置错误,芯片产出的时钟就是垃圾数据,或者拒绝锁定 (Lock)。这些约束非常适合用 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
}
}
// 带有 8 MHz HSE 晶振的 STM32F4 → 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 频率必须介于 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!("'123456789' 的 CRC-32 结果 = {:#010X} ✓", check);
println!("查找表大小:{} 个表项 × 4 字节 = {} 字节(存放在 .rodata 中)",
CRC32_TABLE.len(), CRC32_TABLE.len() * 4);
}
crc32_table() 函数完全在编译阶段运行。产生的 1 KB 查找表被直接嵌入到了二进制文件的只读数据段中 —— 无需分配器,无需初始化代码,更没有启动开销。相比之下,C 语言的做法通常是使用代码生成器,或者在程序启动时计算该表。Rust 的版本可证明是正确的(通过 const _ 断言验证已知值),且可证明是完备的(如果函数无法产生合法的表,编译器将拒绝编译)。
何时使用 Const Fn 证明
| 场景 | 建议 |
|---|---|
| 内存映射、寄存器偏移、分区表 | ✅ 总是使用 |
| 具有固定字段的协议帧布局 | ✅ 总是使用 |
| 寄存器内部的位域掩码 | ✅ 总是使用 |
| 时钟树 / PLL 参数链 | ✅ 总是使用 |
| 查找表 (CRC、三角函数、编码等) | ✅ 总是使用 —— 零启动开销 |
| 具有跨字段不变式的常数(非重叠、和值 ≤ 边界等) | ✅ 总是使用 |
| 具有定义域约束的配置值 | ✅ 当数值在编译时已知时 |
| 源自用户输入或文件的数值 | ❌ 使用运行时验证 |
| 高度动态的结构(树、图等) | ❌ 使用基于属性的测试 |
| 单个数值的范围检查 | ⚠️ 考虑改用新类型 + From 模式 (第 7 章) |
开销总结
| 内容 | 运行时开销 |
|---|---|
const fn 断言 (assert!, panic!) | 仅编译时 —— 0 条指令 |
const _: () = { ... } 验证块 | 仅编译时 —— 不在二进制文件中 |
Region、Register、Field 结构体 | 平铺数据 —— 布局与原始整数一致 |
内联 const { } 泛型验证 | 在编译时单态化 —— 零开销 |
查找表 (crc32_table()) | 在编译时计算 —— 存放在 .rodata 中 |
幽灵类型访问标记 (TypedRegion<ReadOnly>) | 零大小类型 —— 被优化掉 |
上表中的每一项都是 零运行时开销 —— 所有的证明都仅存在于编译阶段。产生的二进制文件仅包含已验证过的常数和查找表,完全不包含任何断言检查代码。
练习:Flash 分区映射
为起始地址为 0x0800_0000 的 1 MB NOR Flash 设计一个已验证的分区映射。要求:
- 具有四个分区:引导加载程序 (bootloader) (64 KB)、应用程序 (application) (640 KB)、配置 (config) (64 KB)、OTA 暂存区 (OTA staging) (256 KB)
- 每一个分区必须 4 KB 对齐(Flash 擦除粒度):基地址和长度都必须是 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, "分区基地址必须 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), "引导加载程序超出了 Flash 范围");
assert!(total.contains(&app), "应用程序超出了 Flash 范围");
assert!(total.contains(&config), "配置段超出了 Flash 范围");
assert!(total.contains(&ota), "OTA 暂存区超出了 Flash 范围");
assert!(!boot.overlaps(&app), "引导加载程序与应用段重叠");
assert!(!boot.overlaps(&config), "引导加载程序与配置段重叠");
assert!(!boot.overlaps(&ota), "引导加载程序与 OTA 段重叠");
assert!(!app.overlaps(&config), "应用段与配置段重叠");
assert!(!app.overlaps(&ota), "应用段与 OTA 段重叠");
assert!(!config.overlaps(&ota), "配置段与 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), // 引导加载程序:64 KB
FlashRegion::new(0x0801_0000, 640 * 1024), // 应用程序:640 KB
FlashRegion::new(0x080B_0000, 64 * 1024), // 配置:64 KB
FlashRegion::new(0x080C_0000, 256 * 1024), // OTA 暂存区:256 KB
);
// 确保 Flash 的每一字节都被核算在内
const _: () = assert!(
FLASH.total_used() == 1024 * 1024,
"分区必须正好填满整个 Flash"
);
fn main() {
println!("Flash 映射:已使用 {} KB / 总计 {} KB",
FLASH.total_used() / 1024,
FLASH.total.size / 1024);
}
flowchart LR
subgraph compile["编译时 —— 零运行时开销"]
direction TB
RGN["Region::new()<br/>✅ 长度 > 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!= 编译时证明责任 —— 如果断言在常量求值期间失败,程序将无法通过编译。无需测试,无需人工代码审查捕捉 —— 编译器证明了其正确性。 -
内存映射是理想的运用场景 —— 子 Region 的包含关系、无重叠、总长度边界和对齐约束都可以表达为
const fn断言。C 语言的#define方法无法提供任何这类保证。 -
可以在其上叠加幽灵类型 —— 将
const fn(数值验证)与幽灵类型的访问标记(权限验证)结合起来,以零运行时开销实现纵深防御。 -
指针来源 (Provenance) 可以在编译时确立 ——
VerifiedAddr在编译阶段就证明了某个地址属于特定 Region,从而消除了每次访问时的运行时越界检查。 -
该模式可推广至内存之外的领域 —— 寄存器映射、位域掩码、协议帧、时钟树、DMA 参数 —— 适用于任何具有编译时已知数值且具备结构化不变式的场景。
-
位域和时钟树是绝佳的目标 —— 寄存器内的位互斥性以及派生值约束链(VCO 范围、分频器限制)正是
const fn能够轻松证明的那类不变式。 -
const fn可以在查找表方面取代代码生成器和构建脚本 —— CRC 表、三角函数、编码映射等 —— 在编译时计算,存放在.rodata中,具有零启动开销且无需外部工具。 -
内联
const { }代码块可以验证泛型参数 —— 自 Rust 1.79 起,你可以在调用点对常量泛型强制执行约束,在任何代码运行之前就捕获滥用行为。
Send & Sync —— 编译时并发证明 🟠
你将学到:
- Rust 的
Send和Sync自动特性 (Auto-traits) 如何将编译器转变为并发审计员 —— 在编译阶段以零运行时开销证明哪些类型可以跨越线程边界,以及哪些类型可以被安全共享。
问题所在:无安全网的并发访问
在系统编程中,外设、共享缓冲区和全局状态会从多个上下文进行访问 —— 包括主循环、中断处理程序 (ISR)、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 定义了两个由编译器自动派生 (derive) 的标记特性 (Marker Traits):
| 特性 | 证明内容 | 通俗含义 |
|---|---|---|
Send | 类型为 T 的数值可以安全地 移动 (Move) 到另一个线程 | “它可以跨越线程边界” |
Sync | 共享引用 &T 可以安全地被多个线程使用 | “它可以从多个线程同时读取” |
这些是 自动特性 (Auto-traits) —— 编译器通过检查结构体中的每一个字段来自动派生它们。如果结构体的所有字段都是 Send,那么该结构体就是 Send;如果所有字段都是 Sync,那么结构体就是 Sync。只要有一个字段选择退出 (Opt out),整个结构体就会失去相应的特性。这种证明是基于结构的,无需人工标注,也没有运行时开销。
flowchart TD
STRUCT["你的结构体"]
INSPECT["编译器检查<br/>每一个字段"]
ALL_SEND{"所有字段<br/>均为 Send?"}
ALL_SYNC{"所有字段<br/>均为 Sync?"}
SEND_YES["Send ✅<br/><i>可以跨越线程边界</i>"]
SEND_NO["!Send ❌<br/><i>仅限单个线程访问</i>"]
SYNC_YES["Sync ✅<br/><i>可以跨多线程共享</i>"]
SYNC_NO["!Sync ❌<br/><i>禁止并发引用</i>"]
STRUCT --> INSPECT
INSPECT --> ALL_SEND
INSPECT --> ALL_SYNC
ALL_SEND -->|是| SEND_YES
ALL_SEND -->|"存在任何 !Send 字段<br/>(如 Rc, *const T)"| SEND_NO
ALL_SYNC -->|是| SYNC_YES
ALL_SYNC -->|"存在任何 !Sync 字段<br/>(如 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。无需程序员介入,也没有遗忘的可能性。
这两个特性通过一个核心恒等式相联系:
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 的,因此封装一个原始指针会自动使包含它的类型退出这两个特性:
/// 内存映射 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 库的不透明句柄。
/// 即使内部的 fd 只是个普通的整数,PhantomData<*const ()>
/// 也能让它变为 !Send + !Sync。
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!("上一次读取值:{}", guard.last_reading.get());
println!("总读取次数:{}", guard.reading_count.get());
}
与 C 语言版本相比:pthread_mutex_lock 只是一个运行时的调用,程序员很容易遗漏。而在 Rust 中,类型系统使得你不通过 Mutex 就无法访问 SensorCache。这一证明是结构化的 —— 唯一的运行时开销就是锁本身。
Mutex不仅仅是同步 —— 它还证明了同步。Mutex::lock()返回的是一个能Deref为&T的MutexGuard。没有锁,就没法拿到内部数据的引用。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("后台任务发生 panic")
}
fn main() {
// ✅ u32 是 Send 的,且闭包没有捕获任何非 Send 的内容
let result = run_on_background(|| 6 * 7);
println!("结果:{result}");
// ✅ String 是 Send 的
let greeting = run_on_background(|| String::from("来自后台的问候"));
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
编译器能将违规行为溯源到具体的约束 —— 并且告诉程序员 原因。对比一下 C 语言的 pthread_create:void *arg 接受任何东西 —— 无论是不是线程安全的。C 编译器无法区分非原子引用计数和普通整数。而 Rust 则是在类型层面确立了这一界限。
何时使用 Send/Sync 证明
| 场景 | 做法 |
|---|---|
| 封装了原始指针的外设句柄 | 自动获得 !Send + !Sync —— 无需额外操作 |
| 来自 C 库的句柄(整数 fd / 句柄) | 添加 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()方法来 消费 (Consume) 该卫兵并返回缓冲区 —— 所有权证明了传输已完成。 - 缓冲区类型
T必须实现一个名为DmaSafe的标记特性。
参考答案
use std::marker::PhantomData;
/// 标记特性:表示该类型可被用作 DMA 缓冲区。
/// 在真实的固件中:该类型必须是 repr(C) 的,且不含填充字节 (Padding)。
trait DmaSafe {}
impl DmaSafe for [u8; 64] {}
impl DmaSafe for [u8; 256] {}
/// 代表一个正在进行的 DMA 传输的卫兵 (Guard)。
/// !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);
Self {
buffer,
channel,
_no_send_sync: PhantomData,
}
}
/// 等待传输完成并返回缓冲区。
/// 消费 self —— 此后该卫兵将不复存在。
pub fn wait(self) -> T {
// 在真实的固件中:轮询 DMA 状态寄存器直到任务完成
println!("DMA 通道 {} 传输完成", self.channel);
self.buffer
}
}
fn main() {
let buf = [0u8; 64];
// 开启传输 —— buf 被移动到了卫兵内部
let transfer = DmaTransfer::start(buf, 2);
// ❌ buf 此时已无法访问 —— 借由所有权防止了“DMA 传输期间仍被使用”的 Bug
// println!("{:?}", buf);
// ❌ 无法通过编译:DmaTransfer 是 !Send 的
// std::thread::spawn(move || { transfer.wait(); });
// ✅ 在原线程等待,并取回缓冲区
let buf = transfer.wait();
println!("缓冲区已回收:共 {} 字节", buf.len());
}
flowchart TB
subgraph compiler["编译时 —— 自动派生证明"]
direction TB
SEND["Send<br/>✅ 可安全跨线程移动"]
SYNC["Sync<br/>✅ 可安全跨线程共享引用"]
NOTSEND["!Send<br/>❌ 仅限单个线程访问"]
NOTSYNC["!Sync<br/>❌ 禁止并发共享"]
end
subgraph types["类型体系分类"]
direction TB
PLAIN["基元类型、String、Vec<br/>Send + Sync"]
CELL["Cell, RefCell<br/>Send + !Sync"]
RC["Rc, 原始指针<br/>!Send + !Sync"]
MUTEX["Mutex<T><br/>恢复了 Sync"]
ARC["Arc<T><br/>共享所有权 + Send"]
end
subgraph runtime["运行时"]
SAFE["线程安全访问<br/>无数据竞争<br/>无遗漏加锁"]
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 上下文)时,一个幽灵字段便能完成该任务。 -
Mutex<T>能够有证明地恢复Sync—— 编译器在结构上证明了所有的访问都必然经过锁。与 C 语言的pthread_mutex_t不同,你不可能忘记加锁。 -
函数约束即定理 —— 任务调度器签名中的
F: Send + 'static属于一种证明责任:每一个调用点都必须证明其闭包是线程安全的。而 C 语言的void *arg则可以接受任何内容。 -
该模式与所有其他正确性技巧互补 —— 类型状态 (Typestate) 证明了协议顺序,幽灵类型证明了权限,
const fn证明了数值不变式,而Send/Sync则证明了并发安全性。多管齐下,共同构成了完整的正确性保障。
综合运用 —— 一个完整的诊断平台 🟡
你将学到:
- 如何将所有七种核心模式(第 2 章至第 9 章)组合成一个单一的诊断工作流。
- 涵盖身份验证、会话管理、类型化命令、审计令牌、量纲结果、验证过的数据以及幽灵类型寄存器。
- 所有这些保证的运行时总开销均为零。
参考: 所有核心模式章节(第 2 章至第 9 章)、第 14 章(测试这些保证)。
目标
本章将第 2 章至第 9 章中的 七种模式 结合到一个真实、完整的诊断工作流中。我们将构建一个服务器健康检查程序,它能够:
- 进行身份验证(能力令牌 —— 第 4 章)
- 开启 IPMI 会话(类型状态 —— 第 5 章)
- 发送类型化命令(类型化命令 —— 第 2 章)
- 使用一次性令牌 进行审计日志记录(一次性类型 —— 第 3 章)
- 返回量纲结果(量纲分析 —— 第 6 章)
- 验证 FRU 数据(已验证边界 —— 第 7 章)
- 读取类型化寄存器(幽灵类型 —— 第 9 章)
use std::marker::PhantomData;
use std::io;
// ──── 模式 1:量纲类型 (第 6 章) ────
#[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:类型化命令 (第 2 章) ────
/// 与第 2 章相同的 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:能力令牌 (第 4 章) ────
}
// ──── 模式 4:类型状态会话 (第 5 章) ────
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, // ← 需要能力令牌
) -> Result<Session<Active>, String> {
println!("会话已在 {} 上激活", self.host);
Ok(Session { host: self.host, _state: PhantomData })
}
}
impl Session<Active> {
/// 执行类型化命令 —— 仅在 Active 会话上可用。
/// 返回 io::Result 以传播传输错误(与第 2 章一致)。
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:一次性审计令牌 (第 3 章) ────
/// 每一个诊断运行都会获得一个独一无二的审计令牌。
/// 不可 Clone,也不可 Copy —— 保证每一条审计条目都是唯一的。
pub struct AuditToken {
run_id: u64,
}
}
}
// ──── 模式 6:已验证边界 (第 7 章) ────
// 这里简化了第 7 章中完整的 ValidFru —— 仅保留本复合示例所需的字段。
// 完整的 TryFrom<RawFruData> 版本请参见第 7 章。
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:幽灵类型寄存器 (第 9 章) ────
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>,
}
}
}
// ──── 复合工作流 (Composite Workflow) ────
fn full_diagnostic() -> Result<(), String> {
// 1. 验证身份 → 获取能力令牌
let admin = authenticate("admin", "secret")
.map_err(|e| e.to_string())?;
// 2. 连接并开启会话 (类型状态: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. 读取幽灵类型化的 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"); // ❌ 错误:尝试使用已被移动的值 (use of moved value)
// 9. 关闭会话 (类型状态:Active → 被消费/丢弃)
session.close();
Ok(())
}
编译器证明了什么
| Bug 类型 | 它是如何被防止的 | 模式 |
|---|---|---|
| 未经授权的访问 | activate() 需要 &AdminToken | 能力令牌 |
| 在错误的会话状态下发送命令 | execute() 仅存在于 Session<Active> 上 | 类型状态 |
| 错误的响应类型 | ReadTemp::Response = Celsius,通过 trait 固化 | 类型化命令 |
| 单位混淆 (°C vs RPM) | Celsius ≠ Rpm ≠ Volts | 量纲类型 |
| 寄存器宽度不匹配 | Reg<Width16> 返回 u16 | 幽灵类型 |
| 处理未经验证的数据 | 必须首先调用 ValidFru::parse() | 已验证边界 |
| 重复的审计条目 | AuditToken 在记录日志时被消耗 | 一次性类型 |
| 上电时序错误 (乱序) | 每个步骤都需要前一步生成的令牌 | 能力令牌 (第 4 章) |
实现所有这些保证所带来的运行时总开销:零。
每一项检查都发生在编译时。生成的汇编代码与那些没有任何检查的手写 C 代码完全相同 —— 区别在于 C 语言可能存在 Bug,而这里不会。
关键要点
- 七种模式可以无缝组合 —— 能力令牌、类型状态、类型化命令、一次性类型、量纲类型、已验证边界以及幽灵类型都能完美配合工作。
- 编译器证明了八类 Bug 是不可能存在的 —— 参见上面的“编译器证明了什么”表格。
- 零运行时总开销 —— 生成的汇编代码与未受检的 C 代码完全一致。
- 每种模式都可以独立发挥作用 —— 你不需要一次性使用全部七种;可以根据需要逐步采用。
- 集成章是一个设计模板 —— 可以将其作为你构建自己的类型化诊断工作流的起点。
- 从 IPMI 扩展到大规模 Redfish —— 第 17 章和第 18 章将这些相同的七种模式(加上第 8 章的能力混入)应用于完整的 Redfish 客户端和服务器。这里的 IPMI 工作流是基础;Redfish 演练则展示了这些组合如何扩展到具有多个数据源和模式版本约束的生产系统。
实战演练 —— 类型安全的 Redfish 客户端 🟡
你将学到:
- 如何将类型状态会话 (Type-state Sessions)、能力令牌 (Capability Tokens)、幽灵类型化的资源导航、维度分析 (Dimensional Analysis)、验证边界 (Validated Boundaries)、构建器类型状态 (Builder Type-state) 以及一次性类型 (Single-use Types) 组合成一个完整的、零开销的 Redfish 客户端 —— 在这里,任何违反协议的行为都将导致编译错误。
参考: 第 2 章(类型化命令)、第 3 章(一次性类型)、第 4 章(能力令牌)、第 5 章(类型状态)、第 6 章(维度分析)、第 7 章(验证边界)、第 9 章(幽灵类型)、第 10 章(IPMI 集成)、第 11 章(技巧 4 —— 构建器类型状态)。
为什么 Redfish 值得单独拿出一章来讨论
第 10 章围绕 IPMI 这一字节级协议组合了核心模式。然而,目前大多数 BMC 平台都会同时提供(或仅提供)Redfish REST API。Redfish 引入了其特有的一系列正确性风险:
| 风险 / 隐患 | 示例 | 后果 |
|---|---|---|
| 格式错误的 URI | GET /redfish/v1/Chassis/1/Processors (父节点错误) | 404 错误或静默返回错误数据 |
| 在错误的电源状态下执行操作 | 对已下电系统执行 Reset(ForceOff) | BMC 返回错误,甚至与其他操作发生竞争 |
| 缺少权限 | 操作员级别的代码调用了 Manager.ResetToDefaults | 生产环境中的 403 错误,安全审计发现项 |
| 不完整的 PATCH 操作 | 在 PATCH 请求体中遗漏了必需的 BIOS 属性 | 静默的空操作或部分配置损坏 |
| 未经验证的固件应用 | 在执行镜像完整性检查前调用了 SimpleUpdate | 导致 BMC 变砖 (损坏) |
| 架构版本不匹配 | 在 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!("热设计问题");
}
// 🐛 路径是否正确?并没有编译时检查。
client.post_action(
"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
&serde_json::json!({"ResetType": "ForceOff"})
)?;
Ok(())
}
这段代码在“理想情况下”能正常工作,但隐患重重。每一个 unwrap() 都是潜在的 panic 风险,每一个字符串路径都是未经校验的假设,而单位混淆则完全不可见。
第 1 节 —— 会话生命周期 (类型状态,第 5 章)
Redfish 会话具有严格的生命周期:连接 → 身份验证 → 使用 → 关闭。我们将每个状态编码为不同的类型。
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。
/// 验证服务根节点 (Service Root) 是否可达。
pub fn connect(self) -> Result<RedfishSession<Connected>, RedfishError> {
// GET /redfish/v1 —— 验证服务根节点
println!("正在连接至 {}/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: &get_str,
_pass: &str,
) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
// POST /redfish/v1/SessionService/Sessions
println!("已通过身份验证,用户为:{}", 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);
// ... 带上 auth_token 头部执行 HTTP GET ...
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!("会话已关闭");
// self 已被消费 —— 登出后无法再使用该会话
}
}
// 尝试在非 Authenticated 会话上调用 http_get:
//
// let session = RedfishSession::new("bmc01").connect()?;
// session.http_get("/redfish/v1/Systems");
// ❌ 错误:在 `RedfishSession<Connected>` 上找不到方法 `http_get`
#[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, "连接失败: {msg}"),
Self::AuthenticationFailed(msg) => write!(f, "身份验证失败: {msg}"),
Self::HttpError { status, message } =>
write!(f, "HTTP {status}: {message}"),
Self::ValidationError(msg) => write!(f, "验证错误: {msg}"),
}
}
}
被彻底消除的 Bug: 在未连接或未认证的会话上发送请求。这类方法根本不存在 —— 因而没有程序员会遗漏运行时检查的风险。
第 2 节 —— 权限令牌 (能力令牌,第 4 章)
Redfish 定义了四个权限级别:Login、ConfigureComponents、ConfigureManager、ConfigureSelf。我们不选择在运行时检查权限,而是通过零大小的证明令牌来编码它们。
// ──── 权限令牌 (零大小) ────
/// 调用者拥有 Login 权限的证明。
/// 由成功的登录操作返回 —— 获取该令牌的唯一途径。
pub struct LoginToken { _private: () }
/// 调用者拥有 ConfigureComponents 权限的证明。
/// 仅能通过管理员级别的身份验证获取。
pub struct ConfigureComponentsToken { _private: () }
/// 调用者拥有 ConfigureManager 权限 (固件更新等) 的证明。
pub struct ConfigureManagerToken { _private: () }
// 扩展 login 方法,根据角色返回权限令牌:
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 节 —— 类型化的资源导航 (幽灵类型,第 9 章)
Redfish 资源构成了一棵树。将这一层级结构编码为类型,可以防止构造出非法的 URI:
graph TD
SR[服务根节点] --> Systems[系统集合]
SR --> Chassis[机箱集合]
SR --> Managers[管理器集合]
SR --> UpdateService[更新服务]
Systems --> CS[计算机系统实例]
CS --> Processors[处理器]
CS --> Memory[内存]
CS --> Bios[Bios]
Chassis --> Ch1[机箱实例]
Ch1 --> Thermal[热资源]
Ch1 --> Power[电源资源]
Managers --> Mgr[管理器实例]
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 节 —— 类型化的遥测读取 (类型化命令 + 维度分析,第 2 章 + 第 6 章)
将类型化的资源路径与具备维度的返回类型结合起来,使编译器能够知晓每一个读数所携带的单位:
use std::marker::PhantomData;
// ──── 维度类型 (第 6 章) ────
#[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 (将第 2 章的模式应用于 REST) ────
/// Redfish 资源类型决定了其解析后的响应结构。
pub trait RedfishResource {
type Response;
fn parse(json: &serde_json::Value) -> Result<Self::Response, RedfishError>;
}
// ──── 已验证的热设计响应 (第 7 章) ────
#[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> {
// 在单次读取中完成解析与验证 —— 边界验证 (第 7 章)
let temps = json["Temperatures"]
.as_array()
.ok_or_else(|| RedfishError::ValidationError(
"缺少 Temperatures 数组".into(),
))?
.iter()
.map(|t| {
Ok(TemperatureReading {
name: t["Name"]
.as_str()
.ok_or_else(|| RedfishError::ValidationError(
"缺少 Name 字段".into(),
))?
.to_string(),
reading: Celsius(
t["ReadingCelsius"]
.as_f64()
.ok_or_else(|| RedfishError::ValidationError(
"缺少 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(
"缺少 Fans 数组".into(),
))?
.iter()
.map(|f| {
Ok(FanReading {
name: f["Name"]
.as_str()
.ok_or_else(|| RedfishError::ValidationError(
"缺少 Name 字段".into(),
))?
.to_string(),
reading: Rpm(
f["Reading"]
.as_u64()
.ok_or_else(|| RedfishError::ValidationError(
"缺少 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,
}
}
// ──── 在经过认证的会话上执行类型化的 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):{} 当前值为 {:?}", 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 节 —— 使用构建器类型状态执行 PATCH (第 11 章,技巧 4)
Redfish 的 PATCH 负载必须包含特定字段。如果构建器在必需字段未设置时禁止调用 .apply(),就能防止不完整或空的补丁操作:
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,
system: &RedfishPath<ComputerSystem>,
) -> Result<(), RedfishError> {
// ✅ 两个必需字段均已设置 —— .apply() 可用
BiosPatchBuilder::new()
.boot_order(vec!["Pxe".into(), "Hdd".into()])
.tpm_enabled(true)
.apply(session, configure, system)?;
// ❌ 编译错误:在 `BiosPatchBuilder<FieldSet, FieldUnset>` 上找不到 .apply() 方法
// BiosPatchBuilder::new()
// .boot_order(vec!["Pxe".into()])
// .apply(session, configure, system)?;
// ❌ 编译错误:在 `BiosPatchBuilder<FieldUnset, FieldUnset>` 上找不到 .apply() 方法
// BiosPatchBuilder::new()
// .apply(session, configure, system)?;
Ok(())
}
被彻底消除的 Bug:
- 空的 PATCH: 在未设置所有必需字段前无法调用
.apply()。 - 缺少权限:
.apply()需要传入&ConfigureComponentsToken。 - 错误的资源: 接受的是
&RedfishPath<ComputerSystem>而非原始字符串。
第 6 节 —— 固件更新生命周期 (一次性类型 + 类型状态,第 3 章 + 第 5 章)
Redfish 的 UpdateService 具有严格的执行序列:推送镜像 → 验证 → 应用 → 重启。每个阶段必须恰好发生一次,且顺序固定。
stateDiagram-v2
[*] --> Idle
Idle --> Uploading : push_image()
Uploading --> Uploaded : 上传完成
Uploaded --> Verified : verify() ✓
Uploaded --> Failed : verify() ✗
Verified --> Applying : apply() —— 消费掉 Verified
Applying --> NeedsReboot : 应用完成
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.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!("镜像已验证,哈希值为:{}", self.image_hash);
Ok(FirmwareUpdate {
task_uri: self.task_uri,
image_hash: self.image_hash,
_phase: PhantomData,
})
}
}
impl FirmwareUpdate<FwVerified> {
/// 应用更新。会消费掉 self —— 无法应用两次。
/// 这即是第 3 章对应的一次性模式。
pub fn apply(self) -> Result<FirmwareUpdate<FwNeedsReboot>, RedfishError> {
// 调用 PATCH /redfish/v1/UpdateService —— 设置 ApplyTime
println!("固件已应用,任务 URI 为:{}", 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 正在重启以激活新固件");
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 节) ──
// 管理员登录 —— 获取所有的能力令牌
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 {
// 摄氏度只能与摄氏度对比 —— 维度安全性
if t.reading > t.upper_critical {
println!("🔥 {} 处于临界状态: {:?}", t.name, t.reading);
}
}
for f in &thermal.fans {
if f.reading < Rpm(1000) {
println!("⚠ {} 低于阈值: {:?}", 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. 安全关闭 (Clean shutdown) ──
needs_reboot.reboot(&session, &manager)?;
session.logout();
Ok(())
}
编译器证明了什么
| # | Bug 类别 | 如何防范 | 模式 (对应章节) |
|---|---|---|---|
| 1 | 在未认证的会话上发送请求 | http_get() 仅在 Session<Authenticated> 上存在 | 类型状态 (第 1 节) |
| 2 | 权限提升 | 操作员登录不会返回 ConfigureManagerToken | 能力令牌 (第 2 节) |
| 3 | 格式错误的 Redfish URI | 导航方法强制遵循父节点 → 子节点的级联关系 | 幽灵类型 (第 3 节) |
| 4 | 单位混淆 (°C vs RPM vs W) | Celsius, Rpm, Watts 是互不兼容的独立类型 | 维度分析 (第 4 节) |
| 5 | JSON 字段缺失导致 panic | ValidThermalResponse 在解析边界处完成验证 | 验证边界 (第 4 节) |
| 6 | 响应类型错误 | 每一个资源都拥有固定的 RedfishResource::Response | 类型化命令 (第 4 节) |
| 7 | 不完整的 PATCH 负载 | .apply() 仅当所有字段均为 FieldSet 时才存在 | 构建器类型状态 (第 5 节) |
| 8 | PATCH 操作缺少权限 | .apply() 需要传入 &ConfigureComponentsToken | 能力令牌 (第 5 节) |
| 9 | 应用未经验证的固件 | .apply() 仅在 FwVerified 状态上存在 | 类型状态 (第 6 节) |
| 10 | 固件重复应用 | apply() 会消费掉 self —— 数值已被移动 | 一次性类型 (第 6 节) |
| 11 | 越权执行固件更新 | push_image() 需要传入 &ConfigureManagerToken | 能力令牌 (第 6 节) |
| 12 | 登出后使用会话 | logout() 消费了整个会话对象 | 所有权 (第 1 节) |
这 12 项保证的总运行时开销为:零。
生成的二进制文件所发出的 HTTP 调用与未类型化版本完全一致 —— 但未类型化版本可能包含上述 12 类 Bug,而本版本则从根本上杜绝了它们。
对比:IPMI 集成 (第 10 章) 与 Redfish 集成
| 维度 | 第 10 章 (IPMI) | 本章 (Redfish) |
|---|---|---|
| 传输层 / 协议 | 基于 KCS/LAN 的原始字节 | 基于 HTTPS 的 JSON |
| 导航方式 | 扁平的命令码 (NetFn/Cmd) | 树状层级结构的 URI |
| 响应绑定 | IpmiCmd::Response | RedfishResource::Response |
| 权限模型 | 单一的 AdminToken | 基于角色的多令牌机制 (Role-based) |
| 负载构建 | 字节数组 | 针对 JSON 的构建器类型状态 |
| 更新生命周期 | 未涉及 | 完整的类型状态链 |
| 所运用的模式数量 | 7 | 8 (新增了构建器类型状态) |
这两章是互补的:第 10 章展示了这些模式在字节层面上的效果,而本章展示了它们在 REST/JSON 层面上的应用。类型系统并不关心具体的传输方式 —— 无论哪种方式,它都能证明正确性。
关键要点
- 八种模式组合成一个 Redfish 客户端 —— 包括会话类型状态、能力令牌、幽灵类型化的 URI、类型化命令、维度分析、验证边界、构建器类型状态以及一次性固件应用。
- 12 类 Bug 变成了编译错误 —— 详见上表。
- 零运行时开销 —— 所有的证明令牌、幽灵类型和类型状态标记在编译后都会消失。生成的二进制文件与手写的未类型化代码完全一致。
- REST API 与字节级协议同样受益 —— 第 2 章到第 9 章的模式同样适用于基于 HTTPS 的 JSON (Redfish) 以及基于 KCS 的字节流 (IPMI)。
- 权限强制在结构上实现,而非在流程中实现 —— 函数签名声明了需求;编译器负责强制执行。
- 这是一个设计模板 —— 根据你特定的 Redfish Schema 和组织内部的角色等级制度,灵活调整资源类型标记、能力令牌及构建器。
实战演练 —— 类型安全的 Redfish 服务器 🟡
你将学到: 如何将响应构建器类型状态 (Response Builder Type-state)、来源可用性令牌 (Source-availability Tokens)、维度化序列化 (Dimensional Serialization)、健康状况汇总 (Health Rollup)、架构版本化 (Schema Versioning) 以及类型化操作分发 (Typed Action Dispatch) 组合成一个 无法产生不符合架构规范响应 的 Redfish 服务器 —— 这是 第 17 章 客户端演练的镜像实现。
参考: 第 2 章(类型化命令 —— 在操作分发中反向应用)、第 4 章(能力令牌 —— 来源可用性)、第 6 章(维度类型 —— 序列化侧)、第 7 章(验证边界 —— 反向应用:“构造,而非序列化”)、第 9 章(幽灵类型 —— 架构版本化)、第 11 章(技巧 3 ——
#[non_exhaustive],技巧 4 —— 构建器类型状态)、第 17 章(客户端对应章节)。
镜像问题
第 17 章探讨的是:“我该如何正确地使用 Redfish?”而本章则探讨其镜像问题:“我该如何正确地生成 Redfish?”
在客户端,主要的风险在于信任了错误的数据。而在服务器端,风险则在于发出了错误的数据 —— 且集群中的每一个客户端都会无条件信任你发送的内容。
一个典型的 GET /redfish/v1/Systems/1 响应必须集成来自多个来源的数据:
flowchart LR
subgraph Sources["数据来源"]
SMBIOS["SMBIOS\n类型 1, 类型 17"]
SDR["IPMI 传感器\n(SDR + 读数)"]
SEL["IPMI SEL\n(关键事件)"]
PCIe["PCIe 配置\n空间"]
FW["固件\n版本表"]
PWR["电源状态\n寄存器"]
end
subgraph Server["Redfish 服务器"]
Handler["GET 处理程序"]
Builder["ComputerSystem\n构建器"]
end
SMBIOS -->|"名称, UUID, 序列号"| Handler
SDR -->|"温度, 风扇"| Handler
SEL -->|"健康状况升级"| Handler
PCIe -->|"设备链接"| Handler
FW -->|"BIOS 版本"| Handler
PWR -->|"电源状态"| Handler
Handler --> Builder
Builder -->|".build()"| JSON["符合架构规范的\nJSON 响应"]
style JSON fill:#c8e6c9,color:#000
style Builder fill:#e1f5fe,color:#000
在 C 语言中,这通常是一个长达 500 行的处理器函数,它调用六个子系统,通过 json_object_set() 手动构建 JSON 树,并祈祷每一个必需字段都被填上了。忘了一个?响应就违反了 Redfish 架构。单位写错了?每一个客户端看到的都是损坏的遥测数据。
// 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" —— 架构要求该字段
// 🐛 忘了设置 "UUID" —— 架构要求该字段
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())); // 至少这一项总是可用的
// 🐛 读数是原始 ADC 数值,而非摄氏度 —— 没有类型能捕捉到这一点
double cpu_temp = read_sensor(SENSOR_CPU_TEMP);
// 这个数值最终会进入另一个 Thermal 响应中...
// 但在类型层面上,没有任何东西能将其与 "Celsius" (摄氏度) 联系起来
// 🐛 健康状况是手动计算的 —— 忘了包含 PSU (电源) 状态
json_object_set_new(obj, "Status",
build_status("Enabled", "OK")); // 实际上应该是 "Critical" —— 因为 PSU 正在发生故障
return obj; // 缺少了 2 个必需字段,错误的健康状况,原始单位
}
一个处理器函数里就有四个 Bug。在客户端,每个 Bug 只会影响一个客户端。而在服务器端,每个 Bug 都会影响每一个查询该 BMC 的客户端。
第 1 节 —— 响应构建器类型状态:“构造,而非序列化” (第 7 章的反向应用)
第 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,
}
}
}
// ──── 必需字段的 setter —— 每一个都会转换一个类型参数 ────
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,
}
}
}
// ──── 可选字段的 setter —— 在任何状态下均可用 ────
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,
// 类型状态保证了这些字段均为 Some —— 在此处调用 .unwrap() 是安全的。
// 在生产环境中,建议使用 .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);
}
// 注意:为简洁起见,对 to_value() 调用了 .unwrap()。
// 生产代码应使用 `?` 来传播序列化错误。
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")
// 错误:在 `ComputerSystemBuilder<MissingField, HasField, HasField, HasField>` 上
// 找不到方法 `build`
被彻底消除的 Bug: 不符合架构规范的响应。处理器函数在物理逻辑上无法序列化一个未提供所有必需字段的 ComputerSystem。编译错误信息甚至会明确告诉你缺少了哪一个字段 —— 它就体现在类型参数中:处于 Name 位置的 MissingField。
第 2 节 —— 来源可用性令牌 (能力令牌,第 4 章 —— 新用途)
在第 4 章和第 17 章中,能力令牌证明的是授权 (Authorization) —— “调用者被允许执行某些操作”。而在服务器端,同一模式证明的是可用性 (Availability) —— “数据源已成功初始化”。
BMC 查询的每个子系统都可能独立发生故障。SMBIOS 表可能损坏,传感器子系统可能仍在初始化,PCIe 总线扫描可能超时。我们将每个子系统编码为一个证明令牌:
/// SMBIOS 表已成功解析的证明。
/// 仅由 SMBIOS 初始化函数生成。
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 摘要 —— 从类型化事件导出的各子系统健康状况。
/// 由第 7 章 SEL 章节中的消费者管道 (Consumer Pipeline) 构建。
/// 以具备类型详细程度的结构取代了信息丢失的 `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 条目,通过第 7 章的 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,第 8 章) 的结合
随着需要处理的 Redfish 资源类型不断增加 (ComputerSystem、Chassis、Manager、Thermal、Power),数据填充逻辑会在不同的处理器函数中重复。第 8 章中的混入 (Mixin) 模式可以消除这种重复。声明处理器具备哪些来源,全局实现 (Blanket Impl) 即可自动提供填充方法:
/// ── 针对数据源的基础组件 Trait (Ingredient Traits,参考第 8 章) ──
pub trait HasSmbios {
fn smbios(&self) -> &(SmbiosReady, SmbiosTables);
}
pub trait HasSensors {
fn sensors(&self) -> &(SensorsReady, SensorCache);
}
pub trait HasSel {
fn sel(&self) -> &(SelReady, TypedSelSummary);
}
/// ── 混入效果:任何具备 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 能力的类型自动实现 IdentityMixin。
impl<T: HasSmbios> IdentityMixin for T {}
/// ── 混入效果:任何具备传感器 + 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() 会导致编译错误。
这直接镜像了第 8 章中的 BaseBoardController 模式:基础组件 trait 声明你拥有些什么,混入 trait 通过全局实现提供行为,且编译器会根据前置条件对每个混入进行验证。增加一个新的数据源 (例如 HasNvme) 以及对应的混入 (例如 StorageMixin: HasNvme + HasSel),就能自动地为每一个同时拥有这两项能力的处理器提供存储方面的健康汇总。
第 3 节 —— 序列化边界上的维度类型 (第 6 章)
在客户端侧 (第 17 章第 4 节),维度类型防止将摄氏度 (°C) 读取为 RPM。在服务器端,维度类型防止将 RPM 写入到 JSON 的摄氏度字段中。这可以说更加危险 —— 因为服务器端的错误数值会传播到每一个客户端。
use serde::Serialize;
// ──── 源自第 6 章的维度类型,且带有 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 架构规定 ReadingCelsius 的单位是摄氏度 (°C)。Rust 类型系统规定 reading_celsius 的类型必须是 Celsius。如果开发者不小心传入了 Rpm(8400) 或 Watts(285.0),编译器会在该数值进入 JSON 之前就将其拦截。
第 4 节 —— 作为类型化折叠的健康状况汇总 (Health Rollup)
Redfish 的 Status.Health 是一种汇总 (Rollup) —— 即所有子组件中最差的健康状态。在 C 语言中,这通常是一系列的 if 检查,而这不可避免地会遗漏某个来源。配合类型化枚举和 Ord trait,这种汇总就变成了一行代码的折叠 (Fold) 操作 —— 且编译器保证了每一个来源都能参与汇总:
/// 汇总多个来源的健康状况。
/// HealthValue 上的 Ord 实现: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 各子系统健康状况 (来源于第 7 章的 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 —— 系统即便在 PSU 故障时仍会报告 “OK”。而在 Rust 中,compute_system_health 需要接收指向每一个数据源的显式引用。SEL 的贡献不再是一个有损的 bool 值 —— 而是七个针对各子系统的 HealthValue 字段,它们是通过第 7 章消费者管道中的穷举匹配得出的。增加一种新的 SEL 传感器类型会强制分类器去处理它;增加一个新的子系统字段会强制汇总逻辑将其包含在内。
第 5 节 —— 使用幽灵类型实现架构版本化 (第 9 章)
如果 BMC 宣称支持 ComputerSystem.v1_13_0,那么响应中必须包含该架构版本中引入的属性 (如 LastResetTime、BootProgress)。宣称支持 v1.13 但缺失这些字段会导致 Redfish 互操作性验证器 (Redfish Interop Validator) 报错。通过幽灵版本标记,我们可以将此约束转化为一种编译时契约:
use std::marker::PhantomData;
// ──── 架构版本标记 ────
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+ 版本中是必需的。
/// 该方法仅在 V1_13 上存在。如果 BMC 宣称支持 v1.13
/// 而处理器没有调用此方法,字段就会缺失。
pub fn last_reset_time(&self) -> LastResetTime {
// 从 RTC 或启动时间戳寄存器读取
LastResetTime("2026-03-16T08:30:00Z".to_string())
}
/// BootProgress —— 在 v1.13+ 版本中是必需的。
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();
// ❌ 错误:在 `ComputerSystemResponse<V1_5>` 上找不到方法 `last_reset_time`
}
被彻底消除的 Bug: 架构版本不匹配。如果 BMC 配置为宣称支持 v1.13,则使用 ComputerSystemResponse<V1_13>,编译器将确保生成 v1.13 所需的每一个字段。降级到 v1.5?只需更改类型参数 —— v1.13 的方法便会消失,且不会有任何冗余字段泄露到响应中。
第 6 节 —— 类型化操作分发 (第 2 章的反向应用)
在第 2 章中,类型化命令模式在客户端侧绑定了 Request → Response。在服务器端,同一模式可以反向用于验证入站的操作负载并进行类型安全的分发。
use serde::Deserialize;
// ──── 操作 Trait (第 2 章 IpmiCmd trait 的镜像) ────
/// 一个 Redfish 操作:框架从 POST 请求体中反序列化参数 (Params),
/// 然后调用 execute()。如果 JSON 与 Params 不匹配,反序列化便会失败
/// —— 此时 execute() 绝不会带着错误输入被调用。
pub trait RedfishAction {
/// 预期的 JSON 请求体结构。
type Params: serde::de::DeserializeOwned;
/// 执行操作的结果。
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!("正在发起 ACPI 关机");
Ok(())
}
ResetType::ForceOff => {
// 向主机发送强制断电信号
println!("强制断电");
Ok(())
}
ResetType::On | ResetType::ForceOn => {
println!("正在上电");
Ok(())
}
ResetType::GracefulRestart => {
println!("ACPI 重启");
Ok(())
}
ResetType::ForceRestart => {
println!("强制重启");
Ok(())
}
ResetType::PushPowerButton => {
println!("模拟按下电源按钮");
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!("完全恢复出厂设置");
Ok(())
}
ResetToDefaultsType::PreserveNetworkAndUsers => {
println!("恢复设置,保留网络和用户数据");
Ok(())
}
ResetToDefaultsType::PreserveNetwork => {
println!("恢复设置,仅保留网络配置");
Ok(())
}
}
}
}
// ──── 泛型操作分发器 ────
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:
- 非法的操作负载: serde 会在
execute()被调用前拒绝未知的枚举变体或缺失的字段。无需手动编写if (body["ResetType"] == ...)检查链。 - 缺失变体处理: 对
params.reset_type的match操作是穷举性的 —— 增加一个新的ResetType变体将强制更新每一个操作处理器。 - 类型混淆:
ComputerSystemReset预期接收ResetParams;而ManagerResetToDefaults预期接收ResetToDefaultsParams。Trait 系统防止了将某个操作的参数传递给另一个操作的处理器。
第 7 节 —— 总结与集成:GET 处理器
下面是一个集成了上述六个小节内容的完整处理器,它能生成一个符合架构规范的单次响应:
/// 完整的 GET /redfish/v1/Systems/1 处理器。
///
/// 每一个必需字段都由构建器类型状态强制执行。
/// 每一个数据源都受可用性令牌管控。
/// 每一个单位都与其维度类型绑定。
/// 每一种健康输入都会反馈到类型化的汇总逻辑中。
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. 构建器类型状态 (第 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() 要求所有类型状态标记均为 HasField | 构建器类型状态 (第 1 节) |
| 2 | 调用了初始化失败的子系统 | 来源可用性令牌管控数据访问 | 能力令牌 (第 2 节) |
| 3 | 数据源不可用时缺失备选方案 | match 的两种分支 (存在/缺失) 都必须生成 HasField | 类型状态 + 穷举匹配 (第 2 节) |
| 4 | JSON 字段中的单位错误 | reading_celsius: Celsius ≠ Rpm ≠ Watts | 维度类型 (第 3 节) |
| 5 | 不完整的健康状况汇总 | compute_system_health 接收显式来源引用;SEL 通过第 7 章的 TypedSelSummary 提供各子系统的 HealthValue | 类型化函数签名 + 穷举匹配 (第 4 节) |
| 6 | 架构版本不匹配 | ComputerSystemResponse<V1_13> 具备 last_reset_time();而 V1_5 则没有 | 幽灵类型 (第 5 节) |
| 7 | 接受了非法的操作负载 | serde 会在 execute() 被调用前拒绝未知/缺失字段 | 类型化操作分发 (第 6 节) |
| 8 | 缺失操作变体处理逻辑 | 对 params.reset_type 的 match 操作是穷举性的 | 枚举穷举性 (第 6 节) |
| 9 | 错误的参数传递给处理器 | RedfishAction::Params 是一种关联类型 | 类型化命令的反向应用 (第 6 节) |
总运行时开销:零。 构建器标记、可用性令牌、幽灵版本类型以及维度新型类型在编译后都会消失。生成的 JSON 与手写的 C 语言版本完全一致 —— 但它杜绝了九类 Bug。
镜像对照:客户端与服务器端模式映射图
| 关注项 | 客户端 (第 17 章) | 服务器端 (本章) |
|---|---|---|
| 边界方向 | 入站:JSON → 类型化数值 | 出站:类型化数值 → JSON |
| 核心原则 | “解析,而非验证” | “构造,而非序列化” |
| 字段完整性 | TryFrom 验证必需字段是否存在 | 构建器类型状态管控必需字段的 .build() 调用 |
| 单位安全性 | 读取时:Celsius ≠ Rpm | 写入时:Celsius ≠ Rpm |
| 权限 / 可用性 | 能力令牌管控请求发送 | 可用性令牌管控数据源访问 |
| 数据来源 | 单一来源 (BMC) | 多个来源 (SMBIOS, 传感器, SEL, PCIe, …) |
| 架构版本 | 幽灵类型防止访问不支持的字段 | 幽灵类型强制提供该版本要求的字段 |
| 操作 (Actions) | 客户端发送类型化的操作 POST 请求 | 服务器通过 RedfishAction trait 验证并分发操作 |
| 健康状况 | 读取并信任 Status.Health | 通过类型化的汇总逻辑计算 Status.Health |
| 错误传播 | 一次解析错误 → 导致一个客户端出错 | 一次序列化错误 → 导致每一个客户端看到的数据都出错 |
这两章构成了一个完整的故事。第 17 章:“我消费的每一个响应都经过了类型检查。”本章:“我生成的每一个响应都经过了类型检查。”同样的模式在两个方向上流动 —— 类型系统并不知道(也不关心)你处于线路在哪一端。
关键要点
- “构造,而非序列化” 是服务器端对“解析,而非验证”的镜像应用 —— 使用构建器类型状态,使得
.build()仅在所有必需字段齐备时才可用。 - 来源可用性令牌证明初始化状态 —— 这是对第 4 章能力令牌模式的重用,旨在证明数据源已准备就绪。
- 维度类型同时保护生产者和消费者 —— 将
Rpm填入ReadingCelsius字段是一个编译错误,而不是一个直到客户反馈时才发现的 Bug。 - 健康状况汇总是类型化的折叠操作 ——
HealthValue上的Ord实现配合显式的来源引用,意味着编译器能捕捉到“忘了包含 PSU 状态”这种错误。 - 在类型层面上实现架构版本化 —— 幽灵类型参数使得特定版本的字段在编译时动态地显现或消失。
- 操作分发是第 2 章的反向应用 ——
serde将负载反序列化为类型化的Params结构体,且对枚举变体的穷举匹配意味着增加一个新的ResetType会强制每个处理器都进行更新。 - 服务器端 Bug 会传播到每一个客户端 —— 这就是为什么生产者侧的编译时正确性比消费者侧更为关键。
来自实战的 14 个小技巧 🟡
你将学到:
- 14 种具体的“正确构建”小技巧 —— 从消除哨兵值、密封特性到会话类型、
Pin、RAII 以及#[must_use]。- 每一项技巧都能以近乎零成本的方式消除一类特定的 Bug。
参考: 第 2 章(密封特性扩展了第 2 章的内容)、第 5 章(类型状态构建器扩展了第 5 章的内容)、第 7 章(FromStr 扩展了第 7 章的内容)。
来自实战的 14 个小技巧
第 2 章至第 9 章介绍的八种核心模式涵盖了主要的“正确构建”技术。本章则收集了 14 个在生产级 Rust 代码中反复出现的 虽然较小但价值很高的小技巧 —— 它们每一个都能以零或近乎零的成本消除一类特定的 Bug。
技巧 1 —— 在边界处将“哨兵值”映射为 Option
硬件协议中充斥着“哨兵值 (Sentinel Values)”:IPMI 使用 0xFF 表示“传感器不存在”,PCI 使用 0xFFFF 表示“没有设备”,SMBIOS 使用 0x00 表示“未知”。如果你在代码中一直将这些哨兵值作为普通整数传递,那么每个消费者都必须记住去检查那个魔数。即使只有一次比较忘记了检查,你也会得到一个 255°C 的幻影读数,或者一个伪造的厂商 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 是特殊的。如果有人在没先检查 0xFFFF 的情况下写了 if vendor_id == target_id,那么当目标 ID 碰巧也因为错误的输入被解析为 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 —— 密封特性 (Sealed Traits)
第 2 章介绍了 IpmiCmd,它带有一个关联类型,将每个命令与其响应绑定。但这里有一个漏洞:如果 任何 代码都能实现 IpmiCmd,那么有人就可能写出一个 MaliciousCmd,其 parse_response 返回错误的类型或者直接发生 panic。整个系统的类型安全性都建立在每一个实现都是正确的基础之上。
密封特性 (Sealed Trait) 关闭了这个漏洞。其背后的思想很简单:让特性要求一个 私有 的父特性 (Supertrait),而该父特性只有在你自己的 crate 中才能被实现。
// — 私有模块:不从 crate 中导出 —
mod private {
pub trait Sealed {}
}
// — 公有特性:要求实现 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) | 用户应当能扩展系统 (自定义报告格式化器) |
| 关联类型必须满足某些不变式 | 特性只是一个简单的能力标记 (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 进行匹配且没有通配符分支 (wildcard arm),都会 由于无法编译而静默失败 —— 这正是重点所在。但内部代码呢?如果没有 #[non_exhaustive],你在 同一个 crate 中的 match 可以在没写通配符的情况下通过编译,而增加新变体就会导致你自己的构建失败。
将枚举标记为 #[non_exhaustive] 会强制 外部 crate 在对其进行匹配时必须包含通配符分支。而在定义该枚举的 crate 内部,#[non_exhaustive] 不起作用 —— 你依然可以编写穷尽匹配 (exhaustive matches)。
为什么这很有用: 当你从一个库 crate(或工作区中的共享子 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", // 对于外部 crate,#[non_exhaustive] 要求必须有此分支
}
}
工作区 (Workspace) 提示: 如果你所有的代码都在同一个 crate 中,
#[non_exhaustive]就起不到作用 —— 它只影响跨 crate 边界的情况。对于本项目的大型工作区,请将不断演进的枚举放在共享 crate(如core_lib或inventory)中,这样该属性就能保护其他工作区 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 章展示了针对 协议 的类型状态(会话生命周期、链路训练)。同样的想法也适用于 构建器 (Builders) —— 这些结构体的 build() / finish() 方法只有在所有必填字段都已设置时才能被调用。
链式构建器的问题
diag_framework/src/der.rs 中的 DerBuilder 目前的代码如下(简化版):
// 当前的链式构建器 —— 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
类型状态构建器: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 模块") // 顺序不影响
.mnemonic("ACCEL_CARD_ER691")
.description("热过载触发降频")
.finish();
// ❌ 编译错误 —— DerBuilder<Set<String>, Missing> 上没有 `finish()` 方法
let bad = DerBuilder::new("CSI_ERR", 62691)
.mnemonic("ACCEL_CARD_ER691")
.finish(); // 错误:未找到 `finish` 方法
何时使用类型状态构建器
| 在以下情况使用…… | 在以下情况不用费事…… |
|---|---|
| 遗漏一个字段会导致静默 Bug (如 DER 缺失助记符) | 所有的字段都有合理的默认值 |
| 该构建器是公有 API 的一部分 | 该构建器仅仅是测试用的临时脚手架 |
| 有 2 到 3 个以上的必填字段 | 只有一个必填字段 (直接在 new() 中传入即可) |
技巧 5 —— 将 FromStr 作为验证边界
第 7 章介绍了针对二进制数据(FRU 记录、SEL 条目)的 TryFrom<&[u8]>。对于 字符串 输入 —— 配置文件、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!("未知的诊断级别:'{other}'")),
}
}
}
现在,笔误会立即被发现:
let level: DiagLevel = "extendedd".parse()?;
// 错误:("未知的诊断级别:'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 | 故障生命周期状态 |
DiagAction | config_loader | 补救动作类型 |
ActionType | config_loader | 动作类别 |
DiagMode | cluster_diag | 多节点测试模式 |
与 TryFrom 的对比:
TryFrom<&[u8]> | FromStr | |
|---|---|---|
| 输入 | 原始字节 (二进制协议) | 字符串 (配置、CLI、JSON) |
| 典型来源 | IPMI, PCIe 配置空间, FRU | JSON 字段, 环境变量, 用户输入 |
| 对应章节 | 第 7 章 | 第 11 章 |
| 共同点 | 使用 Result —— 迫使调用者处理无效输入 |
技巧 6 —— 使用常量泛型进行编译时大小验证
当硬件缓冲区、寄存器组或协议帧具有固定大小时,常量泛型 (Const Generics) 能让编译器强制执行这些约束:
/// 固定大小的寄存器组。其大小是该类型的一部分。
/// `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> ❌
使用常量泛型实现编译时断言:
/// 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 (常量断言)。
何时使用: 固定大小的协议缓冲区 (NVMe, PCIe 配置空间)、DMA 描述符、硬件 FIFO 深度。任何在硬件层面上被定义为常量、且在运行时永远不应当变动的大小。
技巧 7 —— 对 unsafe 进行安全封装
本项目目前几乎没有 unsafe 语句块。但当你需要增加 MMIO 寄存器访问、DMA 或 FFI 时,你就会用到 unsafe。正确构建的方法是:将每一个 unsafe 语句块都封装在一个安全的抽象中,从而使非安全性受到限制且可审计。
/// MMIO 映射后的寄存。只要映射有效,该指针就是有效的。
/// 所有的 unsafe 都限制在这个模块内 —— 调用者使用的是安全的方法。
pub struct MmioRegion {
base: *mut u8,
len: usize,
}
impl MmioRegion {
/// # 安全性 (Safety)
/// - `base` 必须是一个指向 MMIO 映射区域的有效指针。
/// - 该区域在整个结构体的生命周期内必须保持映射状态。
/// - 其他任何代码都不能为由于该区域设置别名。
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
}
}
结合幽灵类型 (第 9 章) 实现类型化的 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()调用者只需在入口处负责一次 其他所有方法均为安全的 (safe) 调用者无法触发未定义行为 (UB) 每一个 unsafe块上方都有# 安全性 (# SAFETY:)注释审计者可以进行局部验证 封装在一个模块内并加上 #[deny(unsafe_op_in_unsafe_fn)]即使在 unsafe fn内部,单独的操作也要加上unsafe标记在封装模块上运行 cargo +nightly miri test验证其是否符合内存模型
✅ 检查点:技巧 1–7
你已经掌握了 7 个日常开发中的小技巧。下面是一个快速记分卡:
| 技巧 | 被消除的 Bug 类型 | 采用成本 |
|---|---|---|
| 1 | 哨兵值混淆 (0xFF) | 低 —— 仅需在边界处进行一次 match |
| 2 | 未经授权的特性实现 | 低 —— 增加 Sealed 父特性约束 |
| 3 | 枚举扩充导致的消费者崩溃 | 低 —— 只需要增加一行属性标记 |
| 4 | 遗漏了构建器字段 | 中 —— 增加了额外的类型参数 |
| 5 | 字符串类型的配置项写错 | 低 —— 实现 FromStr 特性 |
| 6 | 错误的缓冲区大小 | 低 —— 增加一个常量泛型参数 |
| 7 | unsafe 代码散落在各处 | 中 —— 编写封装模块 |
技巧 8-14 属于 进阶 内容 —— 它们涉及 async、常量求值、会话类型、Pin 以及 Drop。如果你觉得累了,可以在这里稍作休息;上面这几项技巧对于明天的开发工作来说已经是非常高价值、且低投入的即战力了。
技巧 8 —— 异步类型状态机 (Async Type-State Machines)
当硬件驱动使用 async 时(例如:异步 BMC 通信、异步 NVMe I/O),类型状态依然有效 —— 但需要注意所有权在 .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 在跨越 .await 时被消耗(移动到了 future 中)。
pub async fn authenticate(self, user: &str, pass: &str)
-> Result<AsyncSession<Active>, String>
{
// 第一阶段:发送凭据 (消耗掉 Idle session)
let pending: AsyncSession<Authenticating> = AsyncSession {
host: self.host,
_state: PhantomData,
};
// 模拟异步 BMC 身份验证
// tokio::time::sleep(Duration::from_secs(1)).await;
// 第二阶段:返回 Active session
Ok(AsyncSession {
host: pending.host,
_state: PhantomData,
})
}
}
impl AsyncSession<Active> {
pub async fn send_command(&mut self, cmd: &[u8]) -> Vec<u8> {
// 在此处进行异步 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;
异步类型状态的关键规则:
| 规则 | 原因 |
|---|---|
转换方法采用 self (按值传递),而非 &mut self | 这样所有权转移在跨越 .await 时才能生效 |
对于可恢复的错误,返回 Result<NextState, (Error, PrevState)> | 这样调用者可以从之前的状态重试 |
| 不要将状态拆分到多个不同的 Future 中 | 单个 Future 应当拥有对应的单个 Session |
配合 tokio::spawn 时使用 Send + 'static 约束 | 该 Session 必须可跨线程移动 |
注意: 如果你需要在出错时找回 之前 的状态(以便重试),请返回
Result<AsyncSession<Active>, (Error, AsyncSession<Idle>)>,这样调用者才能拿回所有权。否则,一个失败的.await会永久性地“丢弃”该 session。
技巧 9 —— 通过常量断言实现细化类型 (Refinement Types)
当数值约束是编译时不变式(而非运行时数据)时,使用 const 求值来强制执行它。这与技巧 6 不同(技巧 6 提供的是类型层面的大小区分) —— 这里我们是在编译时 拒绝无效的数值:
/// 必须处于 IPMI SDR 范围 (0x01..=0xFE) 内的传感器 ID。
/// 当 `N` 为常量时,约束会在编译时被检查。
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(第 7 章,技巧 5)。
技巧 10 —— 用于信道通信的会话类型 (Session Types)
当两个组件通过信道 (Channel) 通信时(例如:诊断编排器 ↔ 工作线程),会话类型 (Session Types) 可以在类型系统中对协议进行编码:
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> {
pub fn send(self, req: String) -> Chan<RecvResponse> {
println!("发送请求中:{req}");
Chan { _state: PhantomData }
}
}
impl Chan<RecvResponse> {
pub fn recv(self) -> (String, Chan<Done>) {
( "响应数据".to_string(), Chan { _state: PhantomData } )
}
}
// 用法示例:
fn protocol_demo(c: Chan<SendRequest>) {
let c = c.send("开始诊断".to_string());
let (resp, c) = c.recv();
println!("收到响应:{resp}");
// c 现在处于 Done 状态 —— 无法再发送或接收
}
为什么这很有用:
- 防止违反协议: 你无法在
Done状态下发送请求。 - 强制实现全对等: 编译器确保你不仅发送了请求,还 不得不 接收响应(否则你无法获得
Chan<Done>)。 - 零成本: 所有状态转换在跨越边界时都会被内联消除。
何时使用: 线程间的诊断协议、BMC 命令序列、任何对顺序有要求的“请求-响应”模式。对于复杂的多消息协议,可以考虑使用
session-types或rumpsteak等 crate。
技巧 11 —— 用于自引用状态机的 Pin
某些类型状态机需要持有对其自身数据的引用(例如:一个追踪其自有缓冲区内部位置的解析器)。Rust 通常禁止这样做,因为移动结构体(move)会导致内部指针失效。Pin<T> 通过保证该值 不会被移动 解决了这一问题:
use std::pin::Pin;
use std::marker::PhantomPinned;
/// 一个持有对其自身缓冲区引用的流式解析器。
/// 一旦被固定 (pinned),它就不能再被移动 —— 从而保证内部引用始终有效。
pub struct StreamParser {
buffer: Vec<u8>,
/// 指向 `buffer` 内部。仅在被固定时有效。
cursor: *const u8,
_pin: PhantomPinned, // 选择退出 Unpin —— 防止意外的取消固定 (unpinning)
}
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);
// 设置游标指向已固定的缓冲区内部
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; }
// ... 在缓冲区中推进游标 ...
Some(42) // 存根示例
}
}
关键洞察: 对于自引用结构体问题,Pin 是“正确构建”的解决方案。如果没有它,你需要使用 unsafe 并手动追踪生命周期。有了它之后,编译器会自动防止移动操作,从而维持内部指针的不变式。
在以下情况使用 Pin…… | 在以下情况不要用 Pin…… |
|---|---|
| 状态机持有结构体内部引用 | 所有字段的所有权都是独立的 |
需要跨 .await 借用的异步 Future | 不需要自引用 |
| 绝不能在内存中移位的 DMA 描述符 | 数据可以自由移动 |
| 带有内部游标的硬件环形缓冲区 | 简单的基于索引的迭代即可满足需求 |
技巧 12 —— 将 RAII / Drop 作为正确性保证
Rust 的 Drop 特性是一种“正确构建”机制:清理代码 绝不会被遗忘,因为编译器会自动插入它。对于必须被 精确释放一次 的硬件资源来说,这具有极高的价值。
use std::io;
/// 一个在完成后必须被关闭的 IPMI 会话。
/// `Drop` 实现保证了即使在发生 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) {
// 关闭会话命令:无论发生 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 与类型状态 (第 5 章) 相组合以实现有序清理:
你无法针对泛型参数特化 (specialize) Drop(Rust 错误 E0366)。取而代之的是,针对每种状态使用 独立的包装类型:
use std::marker::PhantomData;
pub struct Open;
pub struct Locked;
pub struct GpuContext<S> {
device_id: u32,
_state: PhantomData<S>,
}
impl GpuContext<Open> {
pub fn lock_clocks(self) -> LockedGpu {
// ... 锁定 GPU 时钟以实现稳定的基准测试 ...
LockedGpu { device_id: self.device_id }
}
}
/// 锁定状态下的独立类型 —— 拥有自己的 Drop 实现。
/// 我们无法实现 `impl Drop for GpuContext<Locked>` (E0366),
/// 因此我们使用一个持有被锁定资源的独立包装器。
pub struct LockedGpu {
device_id: u32,
}
impl LockedGpu {
pub fn run_benchmark(&self) -> f64 {
// ... 在锁定频率下运行基准测试 ...
42.0
}
}
impl Drop for LockedGpu {
fn drop(&mut self) {
// 在 drop 时解锁频率 —— 仅对锁定状态的包装器触发。
eprintln!("[RAII] GPU {} 时钟已解锁", self.device_id);
}
}
// GpuContext<Open> 没有特殊的 Drop —— 无需解锁时钟。
// LockedGpu 在 drop 时总是会解锁,哪怕发生了 panic 或提前返回。
为什么不能实现
impl Drop for GpuContext<Locked>? Rust 要求Drop实现在泛型类型的所有实例化中都适用。想要实现特定状态的清理,请从以下方案中任选其一:
方案 优点 缺点 独立的包装类型 (如上) 清晰、零成本 额外的类型名称 泛型 Drop+ 运行时的TypeId检查单一类型 需要 'static约束,有运行时成本带穷尽匹配的 enum状态单一泛型类型 运行时分发 (Dispatch),类型安全性稍低
何时使用: BMC 会话、GPU 频率锁、DMA 缓冲区映射、文件句柄、互斥锁守卫 (Mutex guards)、任何具有强制释放步骤的资源。如果你发现自己在写
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("完成码 (Completion code) {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(())
}
为什么这是“正确构建”:
| 没有结构化错误时 | 使用 thiserror 枚举时 |
|---|---|
fn op() -> Result<T, String> | fn op() -> Result<T, DiagError> |
| 调用者只能得到不透明的字符串 | 调用者可以对特定变体进行 match 匹配 |
| 无法区分身份验证失败与超时 | DiagError::Ipmi(IpmiError::AuthFailed) vs Timeout |
| 日志吞掉了错误 | match 迫使你处理每一种情况 |
| 增加了新的错误变体 → 没人注意到 | 增加新变体 → 编译器会警告 match 分支不穷尽 |
anyhow 与 thiserror 的权衡决策:
在以下情况使用 thiserror…… | 在以下情况使用 anyhow…… |
|---|---|
| 编写库 (library) 或 crate 时 | 编写二进制文件 (binary) 或 CLI 时 |
| 调用者需要对错误变体进行 match 匹配时 | 调用者只需要记录日志并退出时 |
| 错误类型是公有 API 的一部分时 | 仅用于内部的错误传递逻辑时 |
protocol_lib, accel_diag, thermal_diag | diag_tool 主二进制程序 |
何时使用: 工作区中的每个 crate 都应当使用
thiserror定义自己的错误枚举。顶层的二进制 crate 则可以使用anyhow进行汇总。这既能让库的调用者享受到编译时的错误处理保证,又能保持二进制程序的简洁性。
技巧 14 —— 使用 #[must_use] 强制消费
#[must_use] 属性会将“被忽略的返回值”变为编译器警告。这是一个轻量级的“正确构建”工具,可以与本书中的每一个模式完美配合:
/// 一个必须被使用的校准令牌 —— 悄悄 drop 掉它是一个 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!()
}
编译器会告诉你什么:
warning: unused `CalibrationToken` that must be used
--> src/main.rs:5:5
|
5 | CalibrationToken { _private: () };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: 校准令牌必须传给 calibrate(),而非丢弃
将 #[must_use] 应用于以下模式:
| 模式 | 标注对象 | 原因 |
|---|---|---|
| 一次性令牌 (第 3 章) | CalibrationToken, FusePayload | 未经使用就 drop 等同于逻辑 Bug |
| 能力令牌 (第 4 章) | AdminToken | 进行了身份验证但忽略了令牌 |
| 类型状态转换 | authenticate()、activate() 的返回值 | 会话已创建但从未被使用 |
| 结果 (Results) | DiagResult, SensorReading | 可能导致静默发生的失败吞没 |
| RAII 句柄 (技巧 12) | IpmiSession, LockedGpu | 打开了资源却不使用 |
经验法则: 如果不使用直接丢弃某个值总是代表某种 Bug,那就加上
#[must_use]。如果有时是有意为之(例如Vec),则不要加。下划线前缀(let _ = foo())可以显式表示确认并消除警告 —— 这在确实有意 drop 时是没问题的。
关键要点
- 边界处:哨兵值 → Option —— 在解析时将魔数转换为
Option;编译器会强制调用者处理None的情况。 - 密封特性关闭了实现漏洞 —— 私有父特性约束意味着只有你自己的 crate 才能实现该特性。
#[non_exhaustive]+#[must_use]是只需一行的价值巨大的标注 —— 将它们加在不断演进的枚举和被消费的令牌上。- 类型状态构建器强制执行必填字段要求 ——
finish()方法只有在所有必填的类型参数都被设为Set时才存在。 - 每一个技巧都针对一类特定的 Bug —— 逐步采用它们即可;没有任何一个技巧需要重写整个架构。
练习 🟡
你将学到:
- 在现实的硬件场景中应用“正确构建 (Correct-by-Construction)”模式的动手实践 —— 包括 NVMe 管理命令、固件更新状态机、传感器流水线、PCIe 幽灵类型、多协议健康检查以及会话类型化 (Session-typed) 的诊断协议。
参考: 第 2 章(练习 1)、第 5 章(练习 2)、第 6 章(练习 3)、第 9 章(练习 4)、第 10 章(练习 5)。
实践问题
练习 1:NVMe 管理命令 (类型化命令)
为 NVMe 管理命令设计一个类型化的命令接口:
Identify(识别) →IdentifyResponse(识别响应:型号、序列号、固件版本)GetLogPage(获取日志页) →SmartLog(SMART 日志:温度、可用备用容量、读取的数据量)GetFeature(获取特性) → 特定于特性的响应
要求:
- 命令类型决定响应类型。
- 无运行时分发 —— 仅限静态分发。
- 增加一个
NamespaceId新类型 (Newtype),防止将命名空间 ID 与其他u32混淆。
提示: 参考第 2 章中的 IpmiCmd 特性模式,但使用 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 (Submission Queue Entry),
// 提交至 SQ,等待 CQ (Completion Queue),然后:
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 …>,无需运行时向下转换 (downcasting)。
练习 2:固件更新状态机 (类型状态)
对 BMC 固件更新生命周期进行建模:
stateDiagram-v2
[*] --> Idle
Idle --> Uploading : begin_upload()
Uploading --> Uploading : send_chunk(data)
Uploading --> Verifying : finish_upload()
Uploading --> Idle : abort()
Verifying --> Applying : verify() ✅ + VerifiedImage 证明令牌
Verifying --> Idle : verify() ❌ 或 abort()
Applying --> Rebooting : apply(token)
Rebooting --> Complete : reboot_complete()
Complete --> [*]
note right of Applying : 无法 abort() —— 该操作不可逆
note right of Verifying : VerifiedImage 是一个证明令牌
要求:
- 每个状态都是一个独立的类型。
- 只有从
Idle状态才能开始上传。 - 验证要求上传必须已完成。
- 只有在验证成功后才能进行“应用 (Apply)”操作 —— 需要一个
VerifiedImage证明令牌。 - 应用操作后,唯一的选项是重启。
- 为
Uploading和Verifying状态增加abort()方法(但在Applying阶段不可用 —— 此时太迟了)。
提示: 将类型状态 (第 5 章) 与能力令牌 (第 4 章) 相结合。
参考答案 (练习 2)
// --- 状态类型 ---
// 设计决策:此处我们将状态存储在结构体内部 (`_state: S`),而不是使用
// 第 5 章所用的 `PhantomData<S>`。这允许状态携带数据 ——
// 例如:`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(|_| "验证失败")?;
// let fw = fw.apply(proof).wait_for_reboot();
// println!("新版本:{}", fw.version());
关键点:
abort()仅存在于FwUpdate<Uploading>和FwUpdate<Verifying>上 —— 在FwUpdate<Applying>上调用它会导致 编译错误,而非运行时检查。VerifiedImage包含私有字段,因此只有verify()才能创建一个。apply()会消费证明令牌 —— 你无法绕过验证步骤。
练习 3:传感器读取流水线 (维度分析)
构建一个完整的传感器流水线:
- 定义新类型:
RawAdc,Celsius,Fahrenheit,Volts,Millivolts,Watts - 实现
From<Celsius> for Fahrenheit及其逆转换 - 为
Amperes实现impl Mul<Volts, Output=Watts>(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) 在物理学上是存疑的 ——
// 参阅第 6 章关于 ΔT 新类型的讨论,以了解更严谨的方法。
// 此处为了练习目的,我们保持简单。
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 扩展了第 6 章中的 Threshold,
// 增加了泛型 ThresholdResult<T>,它携带了触发状态的读数 ——
// 这是对第 6 章中较简单的 { Normal, Warning, Critical } 枚举的演进。
pub enum ThresholdResult<T> {
Normal(T),
Warning(T),
Critical(T),
}
pub struct Threshold<T> {
pub warning: T,
pub critical: T,
}
// 泛型实现 —— 适用于任何支持 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 计数值
pub offset: f64, // ADC 为 0 时的摄氏度
}
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); // 错误:类型不匹配
关键点:
- 每个物理单位都是不同的类型 —— 杜绝了意外混用。
Volts的Mul<Amperes>产生Watts,在类型系统中编码了 P = V × I 这一真理。- 显式的
From转换用于相关的单位(mV ↔ V, °C ↔ °F)。 Threshold<Celsius>仅接受Celsius—— 你无法不小心将 RPM 传给它进行阈值检查。
练习 4:PCIe 能力链表遍历 (幽灵类型 + 验证边界)
为 PCIe 能力 (Capability) 链表建模:
RawCapability(原始能力) —— 来自配置空间的未经验证的字节ValidCapability(有效能力) —— 已解析且已验证(通过 TryFrom)- 每个能力类型(MSI, MSI-X, PCIe Express, Power Management)都有其专属的幽灵类型化的寄存器布局。
- 遍历链表会返回一个
ValidCapability数值的迭代器。
提示: 将验证边界 (第 7 章) 与幽灵类型 (第 9 章) 相结合。
参考答案 (练习 4)
use std::marker::PhantomData;
// --- 能力类型的幽灵标记 ---
pub struct Msi;
pub struct MsiX;
pub struct PciExpress;
pub struct PowerMgmt;
// 规范中的 PCI 能力 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>,
}
/// 已验证且带有类型标记的能力。
#[derive(Debug)]
pub struct ValidCapability<Kind> {
id: u8,
next_ptr: u8,
data: Vec<u8>,
_kind: PhantomData<Kind>,
}
// --- TryFrom:解析而不验证 (Parse-don't-validate) 边界 ---
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 能力"); }
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 能力"); }
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 —— 为简洁起见省略)
// --- 类型安全访问器:仅对正确的能力可用 ---
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
}
}
// --- 能力遍历器 (Walker):遍历链表 ---
pub struct CapabilityWalker<'a> {
config_space: &'a [u8],
next_ptr: u8,
}
impl<'a> CapabilityWalker<'a> {
pub fn new(config_space: &'a [u8]) -> Self {
// 能力指针位于 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 控制:0x{:04X}", pm.pm_control());
// }
// }
关键点:
RawCapability→ValidCapability<Kind>是“解析而不验证”边界。pm_control()仅存在于ValidCapability<PowerMgmt>上 —— 在 MSI 能力上调用它将导致编译错误。CapabilityWalker迭代器产出原始能力;调用者根据其需要,使用TryFrom对感兴趣的能力进行验证。
练习 5:多协议健康检查 (能力混入)
创建一个健康检查框架:
- 定义成分特性 (Ingredient Traits):
HasIpmi,HasRedfish,HasNvmeCli,HasGpio - 创建混入特性 (Mixin Traits):
ThermalHealthMixin(要求 HasIpmi + HasGpio) —— 读取温度,检查告警。StorageHealthMixin(要求 HasNvmeCli) —— SMART 数据检查。BmcHealthMixin(要求 HasIpmi + HasRedfish) —— 交叉验证 BMC 数据。
- 构建一个
FullPlatformController(全平台控制器),实现所有的成分特性。 - 构建一个
StorageOnlyController(仅存储控制器),仅实现HasNvmeCli。 - 验证
StorageOnlyController获得了StorageHealthMixin,但没有获得其他的混入。
参考答案 (练习 5)
// --- 成分特性 (Ingredient traits) ---
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,
}
// --- 带有全局实现 (Blanket impls) 的混入特性 ---
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() 会导致编译错误。
关键点:
- 全局实现
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}—— 任何同时实现了两个成分特性的类型都会自动获得该混入。 StorageOnlyController仅实现了HasNvmeCli,因此编译器授予其实现StorageHealthMixin的权利,但拒绝了thermal_check()和bmc_health()—— 无需任何运行时检查。- 增加新的混入(例如:
NetworkHealthMixin: HasRedfish + HasGpio)只需要一个特性 + 一个全局实现 —— 现有的只要符合条件的控制器都会自动获取它。
练习 6:会话类型化诊断协议 (一次性 + 类型状态)
设计一个带有一次性测试执行令牌的诊断会话:
DiagSession初始处于Setup(设置) 状态。- 转换至
Running(运行) 状态 —— 产出N个执行令牌(每个测试用例一个)。 - 每个
TestToken在测试运行时被消费 —— 防止同一个测试被运行两次。 - 在所有令牌都被消费后,转换至
Complete(完成) 状态。 - 生成报告(仅在
Complete状态下可用)。
进阶: 使用常量泛型 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> {
/// 消费一个令牌来运行一个测试。移动操作防止了重复运行。
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 通过"
关键点:
TestToken既没有Clone也没有Copy—— 通过run_test(token)消费它会发生移动操作,因此再次运行同一个测试会导致编译错误。report()仅存在于DiagSession<Complete>上 —— 在运行中途调用它是无法实现的。- 进阶 变体将使用带常量泛型的
DiagSession<Running, N>,其中run_test会返回DiagSession<Running, {N-1}>,而finish仅在DiagSession<Running, 0>上可用 —— 这保证了在完成前 所有 令牌都已被消费。
关键要点
- 针对真实协议进行练习 —— NVMe、固件更新、传感器流水线、PCIe 都是应用这些模式的真实目标。
- 每个练习都对应一个核心章节 —— 在尝试之前,利用参考链接回顾相关模式。
- 答案使用了可展开的详情块 —— 请先尝试自行解决每个练习,再查看参考答案。
- 练习 5 展示了模式的组合 —— 多协议健康检查结合了类型化命令、维度类型以及验证边界。
- 会话类型 (练习 6) 是前沿领域 —— 它们强制要求了信道上的消息顺序,将类型状态扩展到了分布式系统中。
速记卡 🟡
14+ 种“正确构建 (Correct-by-Construction)”模式的快速参考指南 —— 包含模式选择流程图、模式目录、组合规则、Crate 映射以及“类型即保证”速记表。
参考: 涉及本书的每一章 —— 本页是全书的速览表。
快速参考:正确构建模式
模式选择指南
遗漏了该 Bug 是否会造成灾难性后果?
├── 是 → 它能否通过类型编码来实现?
│ ├── 能 → 使用“正确构建”模式
│ └── 不能 → 运行时检查 + 详尽测试
└── 否 → 正常的运行时检查即可
模式目录
| # | 模式 | 核心特性/类型 | 防止的错误 | 运行时开销 | 章节 |
|---|---|---|---|---|---|
| 1 | 类型化命令 | trait IpmiCmd { type Response; } | 错误的响应类型 | 零 | ch02 |
| 2 | 一次性类型 | struct Nonce (无 Clone/Copy) | Nonce/密钥被重复使用 | 零 | ch03 |
| 3 | 能力令牌 | struct AdminToken { _private: () } | 未经授权的访问 | 零 | ch04 |
| 4 | 类型状态 | Session<Active> | 违反协议约定 | 零 | ch05 |
| 5 | 维度类型 | struct Celsius(f64) | 单位混淆 | 零 | ch06 |
| 6 | 验证边界 | struct ValidFru (通过 TryFrom) | 使用未经验证的数据 | 仅一次解析 | ch07 |
| 7 | 能力混入 | trait FanDiagMixin: HasSpi + HasI2c | 缺失总线访问权限 | 零 | ch08 |
| 8 | 幽灵类型 | Register<Width16> | 位宽/方向不匹配 | 零 | ch09 |
| 9 | 哨兵 → Option | Option<u8> (而非 0xFF) | 将哨兵值当作普通值的 Bug | 零 | ch11 |
| 10 | 密封特性 | trait Cmd: private::Sealed | 不健壮的外部实现 | 零 | ch11 |
| 11 | 非穷尽枚举 | #[non_exhaustive] enum Sku | match 分支的静默漏接 | 零 | ch11 |
| 12 | 类型状态构建器 | DerBuilder<Set, Missing> | 对象构建不完整 | 零 | ch11 |
| 13 | FromStr 验证 | impl FromStr for DiagLevel | 未经验证的字符串输入 | 仅一次解析 | ch11 |
| 14 | 常量泛型大小 | RegisterBank<const N: usize> | 缓冲区大小不匹配 | 零 | ch11 |
| 15 | 安全的 unsafe 封装 | MmioRegion::read_u32() | 未检查的 MMIO/FFI | 零 | ch11 |
| 16 | 异步类型状态 | AsyncSession<Active> | 异步协议违规 | 零 | ch11 |
| 17 | 常量断言 | 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 |
组合规则
能力令牌 + 类型状态 = 已授权的状态转换
类型化命令 + 维度类型 = 具备物理含义类型的响应
验证边界 + 幽灵类型 = 针对已验证配置的类型化寄存器访问
能力混入 + 类型化命令 = 总线感知的类型化操作
一次性类型 + 类型状态 = 转换即消费的协议约定
密封特性 + 类型化命令 = 封闭且鲁棒的命令集
哨兵 → Option + 验证边界 = 清晰的一次性解析流水线
类型状态构建器 + 能力令牌 = 构建完备性的证明
FromStr + #[non_exhaustive] = 可演进、强制快速失败的枚举解析
常量泛型大小 + 验证边界 = 定长且已验证的协议缓冲区
安全的 unsafe 封装 + 幽灵类型 = 类型化且安全的 MMIO 访问
异步类型状态 + 能力令牌 = 已授权的异步转换
会话类型 + 类型化命令 = 完全类型化的“请求-响应”信道
Pin + 类型状态 = 无法移动的自引用状态机
RAII (Drop) + 类型状态 = 依赖状态的清理保证
错误层级 + 验证边界 = 具备详尽处理机制的类型化解析错误
#[must_use] + 一次性类型 = 难以忽略、难以重用的令牌
应避免的反模式
| 反模式 | 为什么它是错的 | 正确的替代方案 |
|---|---|---|
fn read_sensor() -> f64 | 无单位 —— 可能是 °C, °F 或 RPM | fn read_sensor() -> Celsius |
fn encrypt(nonce: &[u8; 12]) | Nonce 可能会被重复使用(通过借用) | fn encrypt(nonce: Nonce) (通过移动) |
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 | 破坏了一次性使用的保证 | 不要派生 (derive) Clone |
let vendor_id: u16 = 0xFFFF | 哨兵值在内部传递 | let vendor_id: Option<u16> = None |
带默认回退逻辑的 fn route(level: &str) | 拼写错误会被静默忽略 | let level: DiagLevel = s.parse()? |
缺少字段也能 Builder::new().finish() | 构建出的对象不完整 | 类型状态构建器:finish() 挂钩在 Set 状态上 |
为定长硬件缓冲区使用 let buf: Vec<u8> | 大小仅在运行时检查 | RegisterBank<4096> (常量泛型) |
散落在处的原始 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 | 类型化命令、类型状态会话 |
thermal_diag | 能力混入、维度类型 |
accel_diag | 验证边界、幽灵寄存器 |
network_diag | 类型状态 (链路训练)、能力令牌 |
pci_topology | 幽灵类型 (寄存器位宽)、已验证配置、哨兵 → Option |
event_handler | 一次性审计令牌、能力令牌、FromStr (Component) |
event_log | 验证边界 (SEL 记录解析) |
compute_diag | 维度类型 (温度、频率) |
memory_diag | 验证边界 (SPD 数据)、维度类型 |
switch_diag | 类型状态 (端口枚举)、幽灵类型 |
config_loader | FromStr (DiagLevel, FaultStatus, DiagAction) |
log_analyzer | 验证边界 (CompiledPatterns) |
diag_framework | 类型状态构建器 (DerBuilder)、会话类型 (编排器 ↔ 工作线程) |
topology_lib | 常量泛型寄存器组、安全 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> |
| “始终为真” | 单元类型 () (unit type) | 始终可构造 |
| “不可能发生” | never 类型 ! 或 enum Void {} | 永远无法被构造 |
测试类型层级的保证 🟡
你将学到:
- 如何测试无效代码 无法通过编译 (
trybuild)、如何对验证边界进行模糊测试 (proptest)、如何验证 RAII 不变式,以及如何通过cargo-show-asm证明零成本抽象。参考: 第 3 章(Nonce 的编译失败测试)、第 7 章(边界的 proptest 测试)、第 5 章(会话的 RAII 验证)。
测试类型层级的保证
“正确构建 (Correct-by-Construction)”模式将 Bug 从运行时转移到了编译时。但是,你该如何 测试 无效的代码确实无法通过编译呢?又该如何确保验证边界在模糊测试下依然稳健?本章将介绍与类型层级正确性相辅相成的各种测试工具。
使用 trybuild 进行编译失败测试
trybuild crate 允许你断言某些代码 不应通过编译。这对于在重构过程中维持类型层级的不变式至关重要 —— 如果有人不小心给你的一次性 Nonce 增加了 Clone 实现,编译失败测试就能捕获到它。
设置:
# 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); // 应当失败:此处使用了已被移动的值 (use of moved value)
}
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
针对不同章节的更多编译失败测试用例:
| 模式 (章节) | 测试断言 | 文件 |
|---|---|---|
| 一次性 Nonce (第 3 章) | 无法使用同一个 Nonce 两次 | nonce_reuse.rs |
| 能力令牌 (第 4 章) | 无令牌则无法调用 admin_op() | missing_token.rs |
| 类型状态 (第 5 章) | 无权在 Session<Idle> 上调用 send_command() | wrong_state.rs |
| 维度类型 (第 6 章) | 无法将 Celsius + Rpm 进行加和 | unit_mismatch.rs |
| 密封特性 (技巧 2) | 外部 crate 无法实现密封特性 | unseal_attempt.rs |
| 非穷尽枚举 (技巧 3) | 外部 match 如果没有通配符则报错 | missing_wildcard.rs |
CI 集成:
# .github/workflows/ci.yml
- name: Run compile-fail tests
run: cargo test --test compile_fail
验证边界的基于属性的测试
验证边界 (第 7 章) 仅在解析阶段对数据进行一次校验,之后便拒绝任何非法输入。但是,你如何知道你的验证逻辑捕捉到了 所有 的非法输入呢?使用 proptest 来进行基于属性的测试,它可以生成数千个随机输入,对边界进行压力测试:
# Cargo.toml
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;
/// 选自第 7 章:ValidFru 封装了符合规范的 FRU 负载。
/// 这些测试使用了带 board_area()、product_area()
/// 以及 format_version() 方法的完整第 7 章 ValidFru 实现。
/// 注意:第 7 章定义了 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
// (来自第 7 章 ValidFru 实现的方法):
let _ = fru.format_version();
let _ = fru.board_area();
let _ = fru.product_area();
}
}
/// 往返测试 (Round-trip):重新解析后 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 规范头部的字节向量。
/// 头部格式与第 7 章的 `TryFrom<RawFruData>` 验证逻辑一致:
/// - 字节 0: 版本 = 0x01
/// - 字节 1-6: 区域偏移量 (乘以 8 = 实际字节偏移)
/// - 字节 7: 校验和 (字节 0-7 的总和对 256 取模结果为 0)
/// Body 是随机生成的,但长度足够以保证偏移量在合法范围内。
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
})
}
针对“正确构建”代码的测试金字塔:
┌───────────────────────────────────┐
│ 编译失败测试 (trybuild) │ ← “无效代码绝不能通过编译”
├───────────────────────────────────┤
│ 属性测试 (proptest/quickcheck) │ ← “合法输入绝不能产生 panic”
├───────────────────────────────────┤
│ 单元测试 (#[test]) │ ← “特定输入产生预期输出”
├───────────────────────────────────┤
│ 类型系统 (第 2–13 章模式) │ ← “整类 Bug 根本无法存在”
└───────────────────────────────────┘
RAII 验证
RAII (技巧 12) 保证了清理工作的执行。要对此进行测试,只需验证 Drop 实现确实被触发了即可:
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), "在 panic 时必须触发 Drop");
}
在你的代码库中的运用
以下是向工作区中添加类型层级测试的优先级计划:
| Crate | 测试类型 | 测试内容 |
|---|---|---|
protocol_lib | 编译失败 | Session<Idle> 无法调用 send_command() |
protocol_lib | 属性测试 | 任意字节序列 → TryFrom 要么成功,要么返回 Err (绝不 panic) |
thermal_diag | 编译失败 | 如果没有 HasSpi 混入,则无法构造 FanReading |
accel_diag | 属性测试 | GPU 传感器解析:随机字节 → 要么被通过验证,要么被拒绝 |
config_loader | 属性测试 | 随机字符串 → DiagLevel 的 FromStr 实现绝不 panic |
pci_topology | 编译失败 | 在需要 Width32 的地方无法传入 Register<Width16> |
event_handler | 编译失败 | 审计令牌无法由外部克隆 (Clone) |
diag_framework | 编译失败 | DerBuilder<Missing, _> 无法调用 finish() |
零成本抽象:通过汇编代码进行证明
一个常见的担忧是:“新类型 (Newtypes) 和幽灵类型 (Phantom Types) 会增加运行时开销吗?” 答案是 不会 —— 它们编译后的汇编代码与原始基元 (Raw Primitives) 完全一致。以下是验证方法:
设置:
cargo install cargo-show-asm
示例:新类型 vs 原始 u32:
// src/lib.rs
#[derive(Clone, Copy)]
pub struct Rpm(pub u32);
#[derive(Clone, Copy)]
pub struct Celsius(pub f64);
// 使用新类型进行算术运算
#[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 (新类型) ; 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 是否运行 —— 原子计数器或 mock 标记可以证明清理工作确已执行。
- cargo-show-asm 证明零成本特性 —— 幽灵类型、ZST 和新类型产生的汇编代码与原始 C 代码无异。
- 为每一个“不可能”的状态增加编译失败测试 —— 如果有人不小心给一次性类型派生了
Clone,测试就能及时发现它。
《Rust 类型驱动的正确性》全书完
Type-Driven Correctness in Rust
Speaker Intro
- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team
- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems
- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since
A practical guide to using Rust’s type system to make entire classes of bugs impossible to compile. While the companion Rust Patterns book covers the mechanics (traits, associated types, type-state), this guide shows how to apply those mechanics to real-world domains — hardware diagnostics, cryptography, protocol validation, and embedded systems.
Every pattern here follows one principle: push invariants from runtime checks into the type system so the compiler enforces them.
How to Use This Book
Difficulty Legend
| Symbol | Level | Audience |
|---|---|---|
| 🟢 | Introductory | Comfortable with ownership + traits |
| 🟡 | Intermediate | Familiar with generics + associated types |
| 🔴 | Advanced | Ready for type-state, phantom types, and session types |
Pacing Guide
| Goal | Path | Time |
|---|---|---|
| Quick overview | ch01, ch13 (reference card) | 30 min |
| IPMI / BMC developer | ch02, ch05, ch07, ch10, ch17 | 2.5 hrs |
| GPU / PCIe developer | ch02, ch06, ch09, ch10, ch15 | 2.5 hrs |
| Redfish implementer | ch02, ch05, ch07, ch08, ch17, ch18 | 3 hrs |
| Framework / infrastructure | ch04, ch08, ch11, ch14, ch18 | 2.5 hrs |
| New to correct-by-construction | ch01 → ch10 in order, then ch12 exercises | 4 hrs |
| Full deep dive | All chapters sequentially | 7 hrs |
Annotated Table of Contents
| Ch | Title | Difficulty | Key Idea |
|---|---|---|---|
| 1 | The Philosophy — Why Types Beat Tests | 🟢 | Three levels of correctness; types as compiler-checked guarantees |
| 2 | Typed Command Interfaces | 🟡 | Associated types bind request → response |
| 3 | Single-Use Types | 🟡 | Move semantics as linear types for crypto |
| 4 | Capability Tokens | 🟡 | Zero-sized proof-of-authority tokens |
| 5 | Protocol State Machines | 🔴 | Type-state for IPMI sessions + PCIe LTSSM |
| 6 | Dimensional Analysis | 🟢 | Newtype wrappers prevent unit mix-ups |
| 7 | Validated Boundaries | 🟡 | Parse once at the edge, carry proof in types |
| 8 | Capability Mixins | 🟡 | Ingredient traits + blanket impls |
| 9 | Phantom Types | 🟡 | PhantomData for register width, DMA direction |
| 10 | Putting It All Together | 🟡 | All 7 patterns in one diagnostic platform |
| 11 | Fourteen Tricks from the Trenches | 🟡 | Sentinel→Option, sealed traits, builders, etc. |
| 12 | Exercises | 🟡 | Six capstone problems with solutions |
| 13 | Reference Card | — | Pattern catalogue + decision flowchart |
| 14 | Testing Type-Level Guarantees | 🟡 | trybuild, proptest, cargo-show-asm |
| 15 | Const Fn | 🟠 | Compile-time proofs for memory maps, registers, bitfields |
| 16 | Send & Sync | 🟠 | Compile-time concurrency proofs |
| 17 | Redfish Client Walkthrough | 🟡 | Eight patterns composed into a type-safe Redfish client |
| 18 | Redfish Server Walkthrough | 🟡 | Builder type-state, source tokens, health rollup, mixins |
Prerequisites
| Concept | Where to learn it |
|---|---|
| Ownership and borrowing | Rust Patterns, ch01 |
| Traits and associated types | Rust Patterns, ch02 |
| Newtypes and type-state | Rust Patterns, ch03 |
| PhantomData | Rust Patterns, ch04 |
| Generics and trait bounds | Rust Patterns, ch01 |
The Correct-by-Construction Spectrum
← Less Safe More Safe →
Runtime checks Unit tests Property tests Correct by Construction
───────────── ────────── ────────────── ──────────────────────
if temp > 100 { #[test] proptest! { struct Celsius(f64);
panic!("too fn test_temp() { |t in 0..200| { // Can't confuse with Rpm
hot"); assert!( assert!(...) // at the type level
} check(42)); }
} }
Invalid program?
Invalid program? Invalid program? Invalid program? Won't compile.
Crashes in prod. Fails in CI. Fails in CI Never exists.
(probabilistic).
This guide operates at the rightmost position — where bugs don’t exist because the type system cannot express them.
The Philosophy — Why Types Beat Tests 🟢
What you’ll learn: The three levels of compile-time correctness (value, state, protocol), how generic function signatures act as compiler-checked guarantees, and when correct-by-construction patterns are — and aren’t — worth the investment.
Cross-references: ch02 (typed commands), ch05 (type-state), ch13 (reference card)
The Cost of Runtime Checking
Consider a typical runtime guard in a diagnostics codebase:
fn read_sensor(sensor_type: &str, raw: &[u8]) -> f64 {
match sensor_type {
"temperature" => raw[0] as i8 as f64, // signed byte
"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}"),
}
}
This function has four failure modes the compiler cannot catch:
- Typo:
"temperture"→ panic at runtime - Wrong
rawlength:fan_speedwith 1 byte → panic at runtime - Caller uses the returned
f64as RPM when it’s actually °C → logic bug, silent - New sensor type added but this
matchnot updated → panic at runtime
Every failure mode is discovered after deployment. Tests help, but they only cover the cases someone thought to write. The type system covers all cases, including ones nobody imagined.
Three Levels of Correctness
Level 1 — Value Correctness
Make invalid values unrepresentable.
// ❌ Any u16 can be a "port" — 0 is invalid but compiles
fn connect(port: u16) { /* ... */ }
// ✅ Only validated ports can exist
pub struct Port(u16); // private field
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) can never be constructed — invariant holds everywhere
Hardware example: SensorId(u8) — wraps a raw sensor number with validation that it’s in the SDR range.
Level 2 — State Correctness
Make invalid transitions unrepresentable.
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> has no send() method — compile error if you try
Hardware example: GPIO pin modes — Pin<Input> has read() but not write().
Level 3 — Protocol Correctness
Make invalid interactions unrepresentable.
use std::io;
trait IpmiCmd {
type Response;
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
// Simplified for illustration — see ch02 for the full trait with
// net_fn(), cmd_byte(), payload(), and 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 always returns Celsius — can't accidentally get Rpm
Hardware example: IPMI, Redfish, NVMe Admin commands — the request type determines the response type.
Types as Compiler-Checked Guarantees
When you write:
fn execute<C: IpmiCmd>(cmd: &C) -> io::Result<C::Response>
You’re not just writing a function — you’re stating a guarantee: “for any command type C that implements IpmiCmd, executing it produces exactly C::Response.” The compiler verifies this guarantee every time it builds your code. If the types don’t line up, the program won’t compile.
This is why Rust’s type system is so powerful — it’s not just catching mistakes, it’s enforcing correctness at compile time.
When NOT to Use These Patterns
Correct-by-construction is not always the right choice:
| Situation | Recommendation |
|---|---|
| Safety-critical boundary (power sequencing, crypto) | ✅ Always — a bug here melts hardware or leaks secrets |
| Cross-module public API | ✅ Usually — misuse should be a compile error |
| State machine with 3+ states | ✅ Usually — type-state prevents wrong transitions |
| Internal helper within one 50-line function | ❌ Overkill — a simple assert! suffices |
| Prototyping / exploring unknown hardware | ❌ Raw types first — refine after behaviour is understood |
| User-facing CLI parsing | ⚠️ clap + TryFrom at the boundary, raw types inside is fine |
The key question: “If this bug happens in production, how bad is it?”
- Fan stops → GPU melts → use types
- Wrong DER record → customer gets bad data → use types
- Debug log message slightly wrong → use
assert!
Key Takeaways
- Three levels of correctness — value (newtypes), state (type-state), protocol (associated types) — each eliminates a broader class of bugs.
- Types as guarantees — every generic function signature is a contract the compiler checks on each build.
- The cost question — “if this bug ships, how bad is it?” determines whether types or tests are the right tool.
- Types complement tests — they eliminate entire categories; tests cover specific values and edge cases.
- Know when to stop — internal helpers and throwaway prototypes rarely need type-level enforcement.
Typed Command Interfaces — Request Determines Response 🟡
What you’ll learn: How associated types on a command trait create a compile-time binding between request and response, eliminating mismatched parsing, unit confusion, and silent type coercion across IPMI, Redfish, and NVMe protocols.
Cross-references: ch01 (philosophy), ch06 (dimensional types), ch07 (validated boundaries), ch10 (integration)
The Untyped Swamp
Most hardware management stacks — IPMI, Redfish, NVMe Admin, PLDM — start life as
raw bytes in → raw bytes out. This creates a category of bugs that tests can only
partially find:
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; // 🤞 is byte 0 the reading?
let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?;
let fan_rpm = raw[0] as u32; // 🐛 fan speed is 2 bytes LE
let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?;
let voltage = raw[0] as f64; // 🐛 need to divide by 1000
if cpu_temp > fan_rpm as f64 { // 🐛 comparing °C to RPM
println!("uh oh");
}
log_temp(voltage); // 🐛 passing Volts as temperature
Ok(())
}
fn log_temp(t: f64) { println!("Temp: {t}°C"); }
| # | Bug | Discovered |
|---|---|---|
| 1 | Fan RPM parsed as 1 byte instead of 2 | Production, 3 AM |
| 2 | Voltage not scaled | Every PSU flagged as overvoltage |
| 3 | Comparing °C to RPM | Maybe never |
| 4 | Volts passed to temp logger | 6 months later, reading historical data |
Root cause: Everything is Vec<u8> → f64 → pray.
The Typed Command Pattern
Step 1 — Domain newtypes
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub u32); // u32: raw IPMI sensor value (integer RPM)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);
Note on
Rpm(u32)vsRpm(f64): In this chapter the inner type isu32because IPMI sensor readings are integer values. In ch06 (Dimensional Analysis),Rpmusesf64to support arithmetic operations (averaging, scaling). Both are valid — the newtype prevents cross-unit confusion regardless of inner type.
Step 2 — The command trait (type-indexed dispatch)
The associated type Response is the key — it binds each command struct to its
return type. Each implementing struct pins Response to a specific domain type,
so execute() always returns exactly the right type:
pub trait IpmiCmd {
/// The "type index" — determines what execute() returns.
type Response;
fn net_fn(&self) -> u8;
fn cmd_byte(&self) -> u8;
fn payload(&self) -> Vec<u8>;
/// Parsing encapsulated here — each command knows its own byte layout.
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
Step 3 — One struct per command
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"));
}
// Note: ch01's untyped example uses `raw[0] as i8 as f64` (signed)
// because that function was demonstrating generic parsing without
// SDR metadata. Here we use unsigned (`as f64`) because the SDR
// linearization formula in IPMI spec §35.5 converts the unsigned
// raw reading to a calibrated value. In production, apply the
// full SDR formula: result = (M × raw + B) × 10^(R_exp).
Ok(Celsius(raw[0] as f64)) // unsigned raw byte, converted per SDR formula
}
}
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))
}
}
Step 4 — The executor (zero dyn, monomorphised)
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
}
}
Step 5 — All four bugs become compile errors
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 — IMPOSSIBLE: parsing lives in ReadFanSpeed::parse_response
// Bug #2 — IMPOSSIBLE: unit scaling lives in ReadVoltage::parse_response
// Bug #3 — COMPILE ERROR:
// if cpu_temp > fan_rpm { }
// ^^^^^^^^ ^^^^^^^ Celsius vs Rpm → "mismatched types" ❌
// Bug #4 — COMPILE ERROR:
// log_temperature(voltage);
// ^^^^^^^ Volts, expected 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: Sensor Reads That Can’t Be Confused
Adding a new sensor is one struct + one impl — no scattered parsing:
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))
}
}
// Every caller that uses bmc.execute(&ReadPowerDraw { domain: 0 })
// automatically gets Watts back — no parsing code elsewhere
Testing Each Command in Isolation
#[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: Schema-Typed REST Endpoints
Redfish is an even better fit — each endpoint returns a DMTF-defined 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>,
}
/// Typed Redfish endpoint — each knows its response type.
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 — real impl uses reqwest/hyper
}
}
// Usage — fully typed, self-documenting
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(),
})?;
// ❌ Compile error — can't pass PowerResponse to a thermal check:
// check_thermals(&power); → "expected ThermalResponse, found 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 Doesn’t Return Log Pages
NVMe admin commands follow the same shape. The controller distinguishes command opcodes, but in C the caller must know which struct to overlay on the 4 KB completion buffer. The typed-command pattern makes this impossible to get wrong:
use std::io;
/// The NVMe Admin command trait — same shape as 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, // bytes 24–63
pub serial_number: String, // bytes 4–23
pub firmware_rev: String, // bytes 64–71
pub total_capacity_gb: u64,
}
pub struct Identify {
pub nsid: u32, // 0 = controller, >0 = namespace
}
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()),
})
}
}
// ── Executor ──
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 — real impl issues doorbell + waits for CQ entry
}
}
// ── Usage ──
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 })?;
// ❌ Compile error — Identify returns IdentifyResponse, not 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(())
}
The three-protocol progression now follows a graduated arc (the same technique ch07 uses for validated boundaries):
| Beat | Protocol | Complexity | What it adds |
|---|---|---|---|
| 1 | IPMI | Simple: sensor ID → reading | Core pattern: trait + associated type |
| 2 | Redfish | REST: endpoint → typed JSON | Serde integration, schema-typed responses |
| 3 | NVMe | Binary: opcode → 4 KB struct overlay | Raw buffer parsing, multi-struct completion data |
Extension: Macro DSL for Command Scripts
/// Execute a series of typed IPMI commands, returning a tuple of results.
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 },
);
// Type: (Celsius, Rpm, Volts) — fully inferred, swap = compile error
assert!(temp < Celsius(95.0), "CPU too hot");
assert!(rpm > Rpm(3000), "Fan too slow");
assert!(volts > Volts(11.4), "12V rail sagging");
Ok(())
}
Extension: Enum Dispatch for Dynamic Scripts
When commands come from JSON config at runtime:
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()
}
The Pattern Family
This pattern applies to every hardware management protocol:
| Protocol | Request Type | Response Type |
|---|---|---|
| IPMI Sensor Reading | ReadTemp | Celsius |
| Redfish REST | GetThermal | ThermalResponse |
| NVMe Admin | Identify | IdentifyResponse |
| PLDM | GetFwParams | FwParamsResponse |
| MCTP | GetEid | EidResponse |
| PCIe Config Space | ReadCapability | CapabilityHeader |
| SMBIOS/DMI | ReadType17 | MemoryDeviceInfo |
The request type determines the response type — the compiler enforces it everywhere.
Typed Command Flow
flowchart LR
subgraph "Compile Time"
RT["ReadTemp"] -->|"type Response = Celsius"| C[Celsius]
RF["ReadFanSpeed"] -->|"type Response = Rpm"| R[Rpm]
RV["ReadVoltage"] -->|"type Response = Volts"| V[Volts]
end
subgraph "Runtime"
E["bmc.execute(&cmd)"] -->|"monomorphised"| 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
Exercise: PLDM Typed Commands
Design a PldmCmd trait (same shape as IpmiCmd) for two PLDM commands:
GetFwParams→FwParamsResponse { active_version: String, pending_version: Option<String> }QueryDeviceIds→DeviceIdResponse { descriptors: Vec<Descriptor> }
Requirements: static dispatch, parse_response returns io::Result<Self::Response>.
Solution
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> {
// Simplified — real impl decodes PLDM FW Update spec fields
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
}
}
Key Takeaways
- Associated type = compile-time contract —
type Responseon the command trait locks each request to exactly one response type. - Parsing is encapsulated — byte-layout knowledge lives in
parse_response, not scattered across callers. - Zero-cost dispatch — generic
execute<C: IpmiCmd>monomorphises to direct calls with no vtable. - One pattern, many protocols — IPMI, Redfish, NVMe, PLDM, MCTP all fit the same
trait Cmd { type Response; }shape. - Enum dispatch bridges static and dynamic — wrap typed commands in an enum for runtime-driven scripts without losing type safety inside each arm.
- Graduated complexity strengthens intuition — IPMI (sensor ID → reading), Redfish (endpoint → JSON schema), and NVMe (opcode → 4 KB struct overlay) all use the same trait shape, but each beat adds a layer of parsing complexity.
Single-Use Types — Cryptographic Guarantees via Ownership 🟡
What you’ll learn: How Rust’s move semantics act as a linear type system, making nonce reuse, double key-agreement, and accidental fuse re-programming impossible at compile time.
Cross-references: ch01 (philosophy), ch04 (capability tokens), ch05 (type-state), ch14 (testing compile-fail)
The Nonce Reuse Catastrophe
In authenticated encryption (AES-GCM, ChaCha20-Poly1305), reusing a nonce with the same key is catastrophic — it leaks the XOR of two plaintexts and often the authentication key itself. This isn’t a theoretical concern:
- 2016: Forbidden Attack on AES-GCM in TLS — nonce reuse allowed plaintext recovery
- 2020: Multiple IoT firmware update systems found reusing nonces due to poor RNG
In C/C++, a nonce is just a uint8_t[12]. Nothing prevents you from using it twice.
// C — nothing stops nonce reuse
uint8_t nonce[12];
generate_nonce(nonce);
encrypt(key, nonce, msg1, out1); // ✅ first use
encrypt(key, nonce, msg2, out2); // 🐛 CATASTROPHIC: same nonce
Move Semantics as Linear Types
Rust’s ownership system is effectively a linear type system — a value can be used
exactly once (moved) unless it implements Copy. The ring crate exploits this:
// ring::aead::Nonce is:
// - NOT Clone
// - NOT Copy
// - Consumed by value when used
pub struct Nonce(/* private */);
impl Nonce {
pub fn try_assume_unique_for_key(value: &[u8]) -> Result<Self, Unspecified> {
// ...
}
// No Clone, no Copy — can only be used once
}
When you pass a Nonce to seal_in_place(), it moves:
// Pseudocode mirroring ring's API shape
fn seal_in_place(
key: &SealingKey,
nonce: Nonce, // ← moved, not borrowed
data: &mut Vec<u8>,
) -> Result<(), Error> {
// ... encrypt data in place ...
// nonce is consumed — cannot be used again
Ok(())
}
Attempting to reuse it:
fn bad_encrypt(key: &SealingKey, data1: &mut Vec<u8>, data2: &mut Vec<u8>) {
// .unwrap() is safe — a 12-byte array is always a valid nonce.
let nonce = Nonce::try_assume_unique_for_key(&[0u8; 12]).unwrap();
seal_in_place(key, nonce, data1).unwrap(); // ✅ nonce moved here
// seal_in_place(key, nonce, data2).unwrap();
// ^^^^^ ERROR: use of moved value ❌
}
The compiler proves that each nonce is used exactly once. No test required.
Case Study: ring’s Nonce
The ring crate goes further with NonceSequence — a trait that generates
nonces and is also non-cloneable:
/// A sequence of unique nonces.
/// Not Clone — once bound to a key, cannot be duplicated.
pub trait NonceSequence {
fn advance(&mut self) -> Result<Nonce, Unspecified>;
}
/// SealingKey wraps a NonceSequence — each seal() auto-advances.
pub struct SealingKey<N: NonceSequence> {
key: UnboundKey, // consumed during construction
nonce_seq: N,
}
impl<N: NonceSequence> SealingKey<N> {
pub fn new(key: UnboundKey, nonce_seq: N) -> Self {
// UnboundKey is moved — can't be used for both sealing AND opening
SealingKey { key, nonce_seq }
}
pub fn seal_in_place_append_tag(
&mut self, // &mut — exclusive access
aad: Aad<&[u8]>,
in_out: &mut Vec<u8>,
) -> Result<(), Unspecified> {
let nonce = self.nonce_seq.advance()?; // auto-generate unique nonce
// ... encrypt with nonce ...
Ok(())
}
}
pub struct UnboundKey;
pub struct Aad<T>(T);
pub struct Unspecified;
The ownership chain prevents:
- Nonce reuse —
Nonceis notClone, consumed on each call - Key duplication —
UnboundKeyis moved intoSealingKey, can’t also make anOpeningKey - Sequence duplication —
NonceSequenceis notClone, so no two keys share a counter
None of these require runtime checks. The compiler enforces all three.
Case Study: Ephemeral Key Agreement
Ephemeral Diffie-Hellman keys must be used exactly once (that’s what “ephemeral” means).
ring enforces this:
/// An ephemeral private key. Not Clone, not Copy.
/// Consumed by agree_ephemeral().
pub struct EphemeralPrivateKey { /* ... */ }
/// Compute shared secret — consumes the private key.
pub fn agree_ephemeral(
my_private_key: EphemeralPrivateKey, // ← moved
peer_public_key: &UnparsedPublicKey,
error_value: Unspecified,
kdf: impl FnOnce(&[u8]) -> Result<SharedSecret, Unspecified>,
) -> Result<SharedSecret, Unspecified> {
// ... DH computation ...
// my_private_key is consumed — can never be reused
kdf(&[])
}
pub struct UnparsedPublicKey;
pub struct SharedSecret;
pub struct Unspecified;
After calling agree_ephemeral(), the private key no longer exists in memory
(it’s been dropped). A C++ developer would need to remember to memset(key, 0, len)
and hope the compiler doesn’t optimise it away. In Rust, the key is simply gone.
Hardware Application: One-Time Fuse Programming
Server platforms have OTP (one-time programmable) fuses for security keys, board serial numbers, and feature bits. Writing a fuse is irreversible — doing it twice with different data bricks the board. This is a perfect fit for move semantics:
use std::io;
/// A fuse write payload. Not Clone, not Copy.
/// Consumed when the fuse is programmed.
pub struct FusePayload {
address: u32,
data: Vec<u8>,
// private constructor — only created via validated builder
}
/// Proof that the fuse programmer is in the correct state.
pub struct FuseController {
/* hardware handle */
}
impl FuseController {
/// Program a fuse — consumes the payload, preventing double-write.
pub fn program(
&mut self,
payload: FusePayload, // ← moved — can't be used twice
) -> io::Result<()> {
// ... write to OTP hardware ...
// payload is consumed — trying to program again with the same
// payload is a compile error
Ok(())
}
}
/// Builder with validation — only way to create a 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 required")?;
let data = self.data.ok_or("data required")?;
if data.len() > 32 { return Err("fuse data too long"); }
Ok(FusePayload { address, data })
}
}
// Usage:
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 consumed
// ctrl.program(payload); // ❌ ERROR: use of moved value
// ^^^^^^^ value used after move
Ok(())
}
Hardware Application: Single-Use Calibration Token
Some sensors require a calibration step that must happen exactly once per power cycle. A calibration token enforces this:
/// Issued once at power-on. Not Clone, not Copy.
pub struct CalibrationToken {
_private: (),
}
pub struct SensorController {
calibrated: bool,
}
impl SensorController {
/// Called once at power-on — returns a calibration token.
pub fn power_on() -> (Self, CalibrationToken) {
(
SensorController { calibrated: false },
CalibrationToken { _private: () },
)
}
/// Calibrate the sensor — consumes the token.
pub fn calibrate(&mut self, _token: CalibrationToken) -> io::Result<()> {
// ... run calibration sequence ...
self.calibrated = true;
Ok(())
}
/// Read a sensor — only meaningful after calibration.
///
/// **Limitation:** The move-semantics guarantee is *partial*. The caller
/// can `drop(cal_token)` without calling `calibrate()` — the token will
/// be destroyed but calibration won't run. The `#[must_use]` annotation
/// (see below) generates a warning but not a hard error.
///
/// The runtime `self.calibrated` check here is the **safety net** for
/// that gap. For a fully compile-time solution, see the type-state
/// pattern in ch05 where `send_command()` only exists on `IpmiSession<Active>`.
pub fn read(&self) -> io::Result<f64> {
if !self.calibrated {
return Err(io::Error::new(io::ErrorKind::Other, "not calibrated"));
}
Ok(25.0) // stub
}
}
fn sensor_workflow() -> io::Result<()> {
let (mut ctrl, cal_token) = SensorController::power_on();
// Must use cal_token somewhere — it's not Copy, so dropping it
// without consuming it generates a warning (or error with #[must_use])
ctrl.calibrate(cal_token)?;
// Now reads work:
let temp = ctrl.read()?;
println!("Temperature: {temp}°C");
// Can't calibrate again — token was consumed:
// ctrl.calibrate(cal_token); // ❌ use of moved value
Ok(())
}
When to Use Single-Use Types
| Scenario | Use single-use (move) semantics? |
|---|---|
| Cryptographic nonces | ✅ Always — nonce reuse is catastrophic |
| Ephemeral keys (DH, ECDH) | ✅ Always — reuse weakens forward secrecy |
| OTP fuse writes | ✅ Always — double-write bricks hardware |
| License activation codes | ✅ Usually — prevent double-activation |
| Calibration tokens | ✅ Usually — enforce once-per-session |
| File write handles | ⚠️ Sometimes — depends on protocol |
| Database transaction handles | ⚠️ Sometimes — commit/rollback is single-use |
| General data buffers | ❌ These need reuse — use &mut [u8] |
Single-Use Ownership Flow
flowchart LR
N["Nonce::new()"] -->|move| E["encrypt(nonce, msg)"]
E -->|consumed| X["❌ nonce gone"]
N -.->|"reuse attempt"| ERR["COMPILE ERROR:\nuse of moved value"]
style N fill:#e1f5fe,color:#000
style E fill:#c8e6c9,color:#000
style X fill:#ffcdd2,color:#000
style ERR fill:#ffcdd2,color:#000
Exercise: Single-Use Firmware Signing Token
Design a SigningToken that can be used exactly once to sign a firmware image:
SigningToken::issue(key_id: &str) -> SigningToken(not Clone, not Copy)sign(token: SigningToken, image: &[u8]) -> SignedImage(consumes the token)- Attempting to sign twice should be a compile error.
Solution
pub struct SigningToken {
key_id: String,
// NOT Clone, NOT 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 {
// Token consumed by move — can't be reused
SignedImage {
signature: vec![0xDE, 0xAD], // stub
key_id: token.key_id,
}
}
// ✅ Compiles:
// let tok = SigningToken::issue("release-key");
// let signed = sign(tok, &firmware_bytes);
//
// ❌ Compile error:
// let signed2 = sign(tok, &other_bytes); // ERROR: use of moved value
Key Takeaways
- Move = linear use — a non-Clone, non-Copy type can be consumed exactly once; the compiler enforces this.
- Nonce reuse is catastrophic — Rust’s ownership system prevents it structurally, not by discipline.
- Pattern applies beyond crypto — OTP fuses, calibration tokens, audit entries — anything that must happen at most once.
- Ephemeral keys get forward secrecy for free — the key agreement value is moved into the derived secret and vanishes.
- When in doubt, remove
Clone— you can always add it later; removing it from a published API is a breaking change.
Capability Tokens — Zero-Cost Proof of Authority 🟡
What you’ll learn: How zero-sized types (ZSTs) act as compile-time proof tokens, enforcing privilege hierarchies, power sequencing, and revocable authority — all at zero runtime cost.
Cross-references: ch03 (single-use types), ch05 (type-state), ch08 (mixins), ch10 (integration)
The Problem: Who Is Allowed to Do What?
In hardware diagnostics, some operations are dangerous:
- Programming BMC firmware
- Resetting PCIe links
- Writing OTP fuses
- Enabling high-voltage test modes
In C/C++, these are guarded by runtime checks:
// C — runtime permission check
int reset_pcie_link(bmc_handle_t bmc, int slot) {
if (!bmc->is_admin) { // runtime check
return -EPERM;
}
if (!bmc->link_trained) { // another runtime check
return -EINVAL;
}
// ... do the dangerous thing ...
return 0;
}
Every function that does something dangerous must repeat these checks. Forget one, and you have a privilege escalation bug.
Zero-Sized Types as Proof Tokens
A capability token is a zero-sized type (ZST) that proves the caller has the authority to perform an action. It costs zero bytes at runtime — it exists only in the type system:
use std::marker::PhantomData;
/// Proof that the caller has admin privileges.
/// Zero-sized — compiles away completely.
/// Not Clone, not Copy — must be explicitly passed.
pub struct AdminToken {
_private: (), // prevents construction outside this module
}
/// Proof that the PCIe link is trained and ready.
pub struct LinkTrainedToken {
_private: (),
}
pub struct BmcController { /* ... */ }
impl BmcController {
/// Authenticate as admin — returns a capability token.
/// This is the ONLY way to create an AdminToken.
pub fn authenticate_admin(
&mut self,
credentials: &[u8],
) -> Result<AdminToken, &'static str> {
// ... validate credentials ...
let valid = true;
if valid {
Ok(AdminToken { _private: () })
} else {
Err("authentication failed")
}
}
/// Train the PCIe link — returns proof that it's trained.
pub fn train_link(&mut self) -> Result<LinkTrainedToken, &'static str> {
// ... perform link training ...
Ok(LinkTrainedToken { _private: () })
}
/// Reset a PCIe link — requires BOTH admin + link-trained proof.
/// No runtime checks needed — the tokens ARE the proof.
pub fn reset_pcie_link(
&mut self,
_admin: &AdminToken, // zero-cost proof of authority
_trained: &LinkTrainedToken, // zero-cost proof of state
slot: u32,
) -> Result<(), &'static str> {
println!("Resetting PCIe link on slot {slot}");
Ok(())
}
}
Usage — the type system enforces the workflow:
fn maintenance_workflow(bmc: &mut BmcController) -> Result<(), &'static str> {
// Step 1: Authenticate — get admin proof
let admin = bmc.authenticate_admin(b"secret")?;
// Step 2: Train link — get trained proof
let trained = bmc.train_link()?;
// Step 3: Reset — compiler requires both tokens
bmc.reset_pcie_link(&admin, &trained, 0)?;
Ok(())
}
// This WON'T compile:
fn unprivileged_attempt(bmc: &mut BmcController) -> Result<(), &'static str> {
let trained = bmc.train_link()?;
// bmc.reset_pcie_link(???, &trained, 0)?;
// ^^^ no AdminToken — can't call this
Ok(())
}
The AdminToken and LinkTrainedToken are zero bytes in the compiled binary.
They exist only during type-checking. The function signature fn reset_pcie_link(&mut self, _admin: &AdminToken, ...) is a proof obligation — “you may only
call this if you can produce an AdminToken” — and the only way to produce one is
through authenticate_admin().
Power Sequencing Authority
Server power sequencing has strict ordering: standby → auxiliary → main → CPU. Reversing the sequence can damage hardware. Capability tokens enforce ordering:
/// State tokens — each one proves the previous step completed.
pub struct StandbyOn { _p: () }
pub struct AuxiliaryOn { _p: () }
pub struct MainOn { _p: () }
pub struct CpuPowered { _p: () }
pub struct PowerController { /* ... */ }
impl PowerController {
/// Step 1: Enable standby power. No precondition.
pub fn enable_standby(&mut self) -> Result<StandbyOn, &'static str> {
println!("Standby power ON");
Ok(StandbyOn { _p: () })
}
/// Step 2: Enable auxiliary — requires standby proof.
pub fn enable_auxiliary(
&mut self,
_standby: &StandbyOn,
) -> Result<AuxiliaryOn, &'static str> {
println!("Auxiliary power ON");
Ok(AuxiliaryOn { _p: () })
}
/// Step 3: Enable main — requires auxiliary proof.
pub fn enable_main(
&mut self,
_aux: &AuxiliaryOn,
) -> Result<MainOn, &'static str> {
println!("Main power ON");
Ok(MainOn { _p: () })
}
/// Step 4: Power CPU — requires main proof.
pub fn power_cpu(
&mut self,
_main: &MainOn,
) -> Result<CpuPowered, &'static str> {
println!("CPU powered ON");
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)
}
// Trying to skip a step:
// fn wrong_order(ctrl: &mut PowerController) {
// ctrl.power_cpu(???); // ❌ can't produce MainOn without enable_main()
// }
Hierarchical Capabilities
Real systems have hierarchies — an admin can do everything a user can do, plus more. Model this with a trait hierarchy:
/// Base capability — anyone who is authenticated.
pub trait Authenticated {
fn token_id(&self) -> u64;
}
/// Operator can read sensors and run non-destructive diagnostics.
pub trait Operator: Authenticated {}
/// Admin can do everything an operator can, plus destructive operations.
pub trait Admin: Operator {}
// Concrete tokens:
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 {
/// Anyone authenticated can read sensors.
pub fn read_sensor(&self, _who: &impl Authenticated, id: u32) -> f64 {
42.0 // stub
}
/// Only operators and above can run diagnostics.
pub fn run_diag(&mut self, _who: &impl Operator, test: &str) -> bool {
true // stub
}
/// Only admins can flash firmware.
pub fn flash_firmware(&mut self, _who: &impl Admin, image: &[u8]) -> Result<(), &'static str> {
Ok(()) // stub
}
}
An AdminCapToken can be passed to any function — it satisfies Authenticated,
Operator, and Admin. A UserToken can only call read_sensor(). The compiler
enforces the entire privilege model at zero runtime cost.
Lifetime-Bounded Capability Tokens
Sometimes a capability should be scoped — valid only within a certain lifetime. Rust’s borrow checker handles this naturally:
/// A scoped admin session. The token borrows the session,
/// so it cannot outlive it.
pub struct AdminSession {
_active: bool,
}
pub struct ScopedAdminToken<'session> {
_session: &'session AdminSession,
}
impl AdminSession {
pub fn begin(credentials: &[u8]) -> Result<Self, &'static str> {
// ... authenticate ...
Ok(AdminSession { _active: true })
}
/// Create a scoped token — lives only as long as the session.
pub fn token(&self) -> ScopedAdminToken<'_> {
ScopedAdminToken { _session: self }
}
}
fn scoped_example() -> Result<(), &'static str> {
let session = AdminSession::begin(b"credentials")?;
let token = session.token();
// Use token within this scope...
// When session drops, token is invalidated by the borrow checker.
// No need for runtime expiry checks.
// drop(session);
// ❌ ERROR: cannot move out of `session` because it is borrowed
// (by `token`, which holds &session)
//
// Even if we skip drop() and just try to use `token` after
// session goes out of scope — same error: lifetime mismatch.
Ok(())
}
When to Use Capability Tokens
| Scenario | Pattern |
|---|---|
| Privileged hardware operations | ZST proof token (AdminToken) |
| Multi-step sequencing | Chain of state tokens (StandbyOn → AuxiliaryOn → …) |
| Role-based access control | Trait hierarchy (Authenticated → Operator → Admin) |
| Time-limited privileges | Lifetime-bounded tokens (ScopedAdminToken<'a>) |
| Cross-module authority | Public token type, private constructor |
Cost Summary
| What | Runtime cost |
|---|---|
| ZST token in memory | 0 bytes |
| Token parameter passing | Optimised away by LLVM |
| Trait hierarchy dispatch | Static dispatch (monomorphised) |
| Lifetime enforcement | Compile-time only |
Total runtime overhead: zero. The privilege model exists only in the type system.
Capability Token Hierarchy
flowchart TD
AUTH["authenticate(user, pass)"] -->|returns| AT["AdminToken"]
AT -->|"&AdminToken"| FW["firmware_update()"]
AT -->|"&AdminToken"| RST["reset_pcie_link()"]
AT -->|downgrade| OP["OperatorToken"]
OP -->|"&OperatorToken"| RD["read_sensors()"]
OP -.->|"attempt firmware_update"| ERR["❌ Compile Error"]
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
Exercise: Tiered Diagnostic Permissions
Design a three-tier capability system: ViewerToken, TechToken, EngineerToken.
- Viewers can call
read_status() - Techs can also call
run_quick_diag() - Engineers can also call
flash_firmware() - Higher tiers can do everything lower tiers can (use trait bounds or token conversion).
Solution
// Tokens — zero-sized, private constructors
pub struct ViewerToken { _private: () }
pub struct TechToken { _private: () }
pub struct EngineerToken { _private: () }
// Capability traits — hierarchical
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]) {
// Only engineers reach here
}
Key Takeaways
- ZST tokens cost zero bytes — they exist only in the type system; LLVM optimises them away completely.
- Private constructors = unforgeable — only your module’s
authenticate()can mint a token. - Trait hierarchies model permission levels —
CanFlash: CanDiag: CanViewmirrors real RBAC. - Lifetime-bounded tokens revoke automatically —
ScopedAdminToken<'session>can’t outlive the session. - Combine with type-state (ch05) for protocols that require authentication and sequenced operations.
Protocol State Machines — Type-State for Real Hardware 🔴
What you’ll learn: How type-state encoding makes protocol violations (wrong-order commands, use-after-close) into compile errors, applied to IPMI session lifecycles and PCIe link training.
Cross-references: ch01 (level 2 — state correctness), ch04 (tokens), ch09 (phantom types), ch11 (trick 4 — typestate builder, trick 8 — async type-state)
The Problem: Protocol Violations
Hardware protocols have strict state machines. An IPMI session has states: Unauthenticated → Authenticated → Active → Closed. PCIe link training goes through Detect → Polling → Configuration → L0. Sending a command in the wrong state corrupts the session or hangs the bus.
IPMI session state machine:
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() only exists here
note right of Idle : send_command() → compile error
PCIe Link Training State Machine (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 transmit only in L0
In C/C++, state is tracked with an enum and runtime checks:
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) { // runtime check — easy to forget
return -EINVAL;
}
// ... send command ...
return 0;
}
Type-State Pattern
With type-state, each protocol state is a distinct type. Transitions are methods that consume one state and return another. The compiler prevents calling methods in the wrong state because those methods don’t exist on that type.
use std::marker::PhantomData;
// States — zero-sized marker types
pub struct Idle;
# Case Study: IPMI Session Lifecycle
pub struct Authenticated;
pub struct Active;
pub struct Closed;
/// IPMI session parameterised by its current state.
/// The state exists ONLY in the type system (PhantomData is zero-sized).
pub struct IpmiSession<State> {
transport: String, // e.g., "192.168.1.100"
session_id: Option<u32>,
_state: PhantomData<State>,
}
// Transition: 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, // ← consumes Idle session
user: &str,
pass: &str,
) -> Result<IpmiSession<Authenticated>, String> {
println!("Authenticating {user} on {}", self.transport);
Ok(IpmiSession {
transport: self.transport,
session_id: Some(42),
_state: PhantomData,
})
}
}
// Transition: Authenticated → Active
impl IpmiSession<Authenticated> {
pub fn activate(self) -> Result<IpmiSession<Active>, String> {
// session_id is guaranteed Some by the type-state transition path.
println!("Activating session {}", self.session_id.unwrap());
Ok(IpmiSession {
transport: self.transport,
session_id: self.session_id,
_state: PhantomData,
})
}
}
// Operations available ONLY in Active state
impl IpmiSession<Active> {
pub fn send_command(&mut self, netfn: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
// session_id is guaranteed Some in Active state.
println!("Sending cmd 0x{cmd:02X} on session {}", self.session_id.unwrap());
vec![0x00] // stub: completion code OK
}
pub fn close(self) -> IpmiSession<Closed> {
// session_id is guaranteed Some in Active state.
println!("Closing session {}", 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, &[]);
// ^^^^^^ ERROR: no method `send_command` on IpmiSession<Idle> ❌
let session = session.authenticate("admin", "password")?;
// session.send_command(0x04, 0x2D, &[]);
// ^^^^^^ ERROR: no method `send_command` on IpmiSession<Authenticated> ❌
let mut session = session.activate()?;
// ✅ NOW send_command exists:
let response = session.send_command(0x04, 0x2D, &[1]);
let _closed = session.close();
// _closed.send_command(0x04, 0x2D, &[]);
// ^^^^^^ ERROR: no method `send_command` on IpmiSession<Closed> ❌
Ok(())
}
No runtime state checks anywhere. The compiler enforces:
- Authentication before activation
- Activation before sending commands
- No commands after close
PCIe Link Training State Machine
PCIe link training is a multi-phase protocol defined in the PCIe specification. Type-state prevents sending data before the link is ready:
use std::marker::PhantomData;
// PCIe LTSSM states (simplified)
pub struct Detect;
pub struct Polling;
pub struct Configuration;
pub struct L0; // fully operational
pub struct Recovery;
pub struct PcieLink<State> {
slot: u32,
width: u8, // negotiated width (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!("Slot {}: receiver detected", 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!("Slot {}: polling complete, entering configuration", 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!("Slot {}: negotiated x{width} Gen{speed}", self.slot);
Ok(PcieLink {
slot: self.slot, width, speed,
_state: PhantomData,
})
}
}
impl PcieLink<L0> {
/// Send a TLP — only possible when the link is fully trained (L0).
pub fn send_tlp(&mut self, tlp: &[u8]) -> Vec<u8> {
println!("Slot {}: sending {} byte TLP", self.slot, tlp.len());
vec![0x00] // stub
}
/// Enter recovery — returns to Recovery state.
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!("Slot {}: retrained at 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]); // ❌ no method `send_tlp` on PcieLink<Detect>
let link = link.detect_receiver()?;
let link = link.poll_compliance()?;
let mut link = link.negotiate(16, 5)?; // x16 Gen5
// ✅ NOW we can send TLPs:
let _resp = link.send_tlp(&[0x00, 0x01, 0x02]);
println!("Link: {}", link.link_info());
// Recovery and retrain:
let recovery = link.enter_recovery();
let mut link = recovery.retrain(4)?; // downgrade to Gen4
let _resp = link.send_tlp(&[0x03]);
Ok(())
}
Combining Type-State with Capability Tokens
Type-state and capability tokens compose naturally. A diagnostic that requires an active IPMI session AND admin privileges:
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![] }
}
/// Run a firmware update — requires:
/// 1. Active IPMI session (type-state)
/// 2. Admin privileges (capability token)
pub fn firmware_update(
session: &mut IpmiSession<Active>, // proves session is active
_admin: &AdminToken, // proves caller is admin
image: &[u8],
) -> Result<(), String> {
// No runtime checks needed — the signature IS the check
session.send_command(0x2C, 0x01, image);
Ok(())
}
The caller must:
- Create a session (
Idle) - Authenticate it (
Authenticated) - Activate it (
Active) - Obtain an
AdminToken - Then and only then call
firmware_update()
All enforced at compile time, zero runtime cost.
Beat 3: Firmware Update — Multi-Phase FSM with Composition
A firmware update lifecycle has more states than a session and composition with both capability tokens AND single-use types (ch03). This is the most complex type-state example in the book — if you’re comfortable with it, you’ve mastered the pattern.
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 token consumed by apply()
note right of Uploading : abort() returns to Idle (safe)
use std::marker::PhantomData;
// ── States ──
pub struct Idle;
pub struct Uploading;
pub struct Verifying;
pub struct Verified;
pub struct Applying;
pub struct WaitingReboot;
// ── Single-use proof that image passed verification (ch03) ──
pub struct VerifiedImage {
_private: (),
pub digest: [u8; 32],
}
// ── Capability token: only admins can initiate (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 }
}
/// Begin upload — requires admin privilege.
pub fn begin_upload(
self,
_admin: &FirmwareAdminToken,
version: &str,
) -> FwUpdate<Uploading> {
println!("Uploading firmware v{version}...");
FwUpdate { version: version.to_string(), _state: PhantomData }
}
}
impl FwUpdate<Uploading> {
pub fn finish_upload(self) -> FwUpdate<Verifying> {
println!("Upload complete, verifying v{}...", self.version);
FwUpdate { version: self.version, _state: PhantomData }
}
/// Abort returns to Idle — safe at any point during upload.
pub fn abort(self) -> FwUpdate<Idle> {
println!("Upload aborted.");
FwUpdate { version: String::new(), _state: PhantomData }
}
}
impl FwUpdate<Verifying> {
/// On success, produces a single-use VerifiedImage token.
pub fn verify_ok(self, digest: [u8; 32]) -> (FwUpdate<Verified>, VerifiedImage) {
println!("Verification passed for v{}", self.version);
(
FwUpdate { version: self.version, _state: PhantomData },
VerifiedImage { _private: (), digest },
)
}
pub fn verify_fail(self) -> FwUpdate<Idle> {
println!("Verification failed — returning to idle.");
FwUpdate { version: String::new(), _state: PhantomData }
}
}
impl FwUpdate<Verified> {
/// Apply CONSUMES the VerifiedImage token — can't apply twice.
pub fn apply(self, proof: VerifiedImage) -> FwUpdate<Applying> {
println!("Applying v{} (digest: {:02x?})", self.version, &proof.digest[..4]);
// proof is moved — can't be reused
FwUpdate { version: self.version, _state: PhantomData }
}
}
impl FwUpdate<Applying> {
pub fn apply_complete(self) -> FwUpdate<WaitingReboot> {
println!("Apply complete — waiting for reboot.");
FwUpdate { version: self.version, _state: PhantomData }
}
}
impl FwUpdate<WaitingReboot> {
pub fn reboot(self) {
println!("Rebooting into v{}...", self.version);
}
}
// ── Usage ──
fn firmware_workflow() {
let fw = FwUpdate::new();
// fw.finish_upload(); // ❌ no method `finish_upload` on FwUpdate<Idle>
let admin = FirmwareAdminToken { _private: () }; // from auth system
let fw = fw.begin_upload(&admin, "2.10.1");
let fw = fw.finish_upload();
let digest = [0xAB; 32]; // computed during verification
let (fw, token) = fw.verify_ok(digest);
let fw = fw.apply(token);
// fw.apply(token); // ❌ use of moved value: `token`
let fw = fw.apply_complete();
fw.reboot();
}
What the three beats illustrate together:
| Beat | Protocol | States | Composition |
|---|---|---|---|
| 1 | IPMI session | 4 | Pure type-state |
| 2 | PCIe LTSSM | 5 | Type-state + recovery branch |
| 3 | Firmware update | 6 | Type-state + capability tokens (ch04) + single-use proof (ch03) |
Each beat adds a layer of complexity. By beat 3, the compiler enforces state ordering, admin privilege, AND one-time application — three bug classes eliminated in a single FSM.
When to Use Type-State
| Protocol | Type-State worthwhile? |
|---|---|
| IPMI session lifecycle | ✅ Yes — authenticate → activate → command → close |
| PCIe link training | ✅ Yes — detect → poll → configure → L0 |
| TLS handshake | ✅ Yes — ClientHello → ServerHello → Finished |
| USB enumeration | ✅ Yes — Attached → Powered → Default → Addressed → Configured |
| Simple request/response | ⚠️ Probably not — only 2 states |
| Fire-and-forget messages | ❌ No — no state to track |
Exercise: USB Device Enumeration Type-State
Model a USB device that must go through: Attached → Powered → Default → Addressed → Configured. Each transition should consume the previous state and produce the next. send_data() should only be available in Configured.
Solution
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]) {
// Only available in Configured state
}
}
Key Takeaways
- Type-state makes wrong-order calls impossible — methods only exist on the state where they’re valid.
- Each transition consumes
self— you can’t hold onto an old state after transitioning. - Combine with capability tokens —
firmware_update()requires bothSession<Active>andAdminToken. - Three beats, increasing complexity — IPMI (pure FSM), PCIe LTSSM (recovery branches), and firmware update (FSM + tokens + single-use proofs) show the pattern scales from simple to richly composed.
- Don’t over-apply — two-state request/response protocols are simpler without type-state.
- The pattern extends to full Redfish workflows — ch17 applies type-state to Redfish session lifecycles, and ch18 uses builder type-state for response construction.
Dimensional Analysis — Making the Compiler Check Your Units 🟢
What you’ll learn: How newtype wrappers and the
uomcrate turn the compiler into a unit-checking engine, preventing the class of bug that destroyed a $328M spacecraft.Cross-references: ch02 (typed commands use these types), ch07 (validated boundaries), ch10 (integration)
The Mars Climate Orbiter
In 1999, NASA’s Mars Climate Orbiter was lost because one team sent thrust data in pound-force seconds while the navigation team expected newton-seconds. The spacecraft entered the atmosphere at 57 km instead of 226 km and disintegrated. Cost: $327.6 million.
The root cause: both values were double. The compiler couldn’t distinguish them.
This same class of bug lurks in every hardware diagnostic that deals with physical quantities:
// C — all doubles, no unit checking
double read_temperature(int sensor_id); // Celsius? Fahrenheit? Kelvin?
double read_voltage(int channel); // Volts? Millivolts?
double read_fan_speed(int fan_id); // RPM? Radians per second?
// Bug: comparing Celsius to Fahrenheit
if (read_temperature(0) > read_temperature(1)) { ... } // units might differ!
Newtypes for Physical Quantities
The simplest correct-by-construction approach: wrap each unit in its own type.
use std::fmt;
/// Temperature in degrees Celsius.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
/// Temperature in degrees Fahrenheit.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);
/// Voltage in volts.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
/// Voltage in millivolts.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);
/// Fan speed in RPM.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Rpm(pub f64);
// Conversions are explicit:
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)
}
}
Now the compiler catches unit mismatches:
#[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 // ✅ same units — compiles
}
// fn bad_comparison(temp: Celsius, voltage: Volts) -> bool {
// temp > voltage // ❌ ERROR: mismatched types — Celsius vs Volts
// }
Zero runtime cost — newtypes compile down to raw f64 values. The wrapper is
purely a type-level concept.
Newtype Macro for Hardware Quantities
Writing newtypes by hand gets repetitive. A macro eliminates the boilerplate:
/// Generate a newtype for a physical quantity.
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) }
}
};
}
// Usage:
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");
Each line generates a complete type with Display, Add, Sub, and comparison operators. All at zero runtime cost.
Physics caveat: The macro generates
Addfor all quantities, includingCelsius. Adding absolute temperatures (25°C + 30°C = 55°C) is not physically meaningful — you’d need a separateTemperatureDeltatype for differences. Theuomcrate (shown later) handles this correctly. For simple sensor diagnostics where you only compare and display, you can omitAdd/Subfrom temperature types and keep them for quantities where addition makes sense (Watts, Volts, Bytes). If you need delta arithmetic, define aCelsiusDelta(f64)newtype withimpl Add<CelsiusDelta> for Celsius.
Applied Example: Sensor Pipeline
A typical diagnostic reads raw ADC values, converts them to physical units, and compares against thresholds. With dimensional types, each step is type-checked:
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");
/// Raw ADC reading — not yet a physical quantity.
#[derive(Debug, Clone, Copy)]
pub struct AdcReading {
pub channel: u8,
pub raw: u16, // 12-bit ADC value (0–4095)
}
/// Calibration coefficients for converting ADC → physical unit.
pub struct TemperatureCalibration {
pub offset: f64,
pub scale: f64, // °C per ADC count
}
pub struct VoltageCalibration {
pub reference_mv: f64,
pub divider_ratio: f64,
}
impl TemperatureCalibration {
/// Convert raw ADC → Celsius. The return type guarantees the output is Celsius.
pub fn convert(&self, adc: AdcReading) -> Celsius {
Celsius::new(adc.raw as f64 * self.scale + self.offset)
}
}
impl VoltageCalibration {
/// Convert raw ADC → Volts. The return type guarantees the output is Volts.
pub fn convert(&self, adc: AdcReading) -> Volts {
Volts::new(adc.raw as f64 * self.reference_mv / 4096.0 / self.divider_ratio / 1000.0)
}
}
/// Threshold check — only compiles if units match.
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:?}");
// This won't compile — can't check a Celsius reading against a Volts threshold:
// let volt_threshold = Threshold {
// warning: Volts::new(11.4),
// critical: Volts::new(10.8),
// };
// volt_threshold.check(&temp); // ❌ ERROR: expected &Volts, found &Celsius
}
The entire pipeline is statically type-checked:
- ADC readings are raw counts (not units)
- Calibration produces typed quantities (Celsius, Volts)
- Thresholds are generic over the quantity type
- Comparing Celsius against Volts is a compile error
The uom Crate
For production use, the uom crate provides
a comprehensive dimensional analysis system with hundreds of units, automatic
conversion, and zero runtime overhead:
// 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; // ❌ compile error — can't add temperature to voltage
// // power > temp; // ❌ compile error — can't compare power to temperature
Use uom when you need automatic derived-unit support (e.g., Watts = Volts × Amperes).
Use hand-rolled newtypes when you need only simple quantities without derived-unit
arithmetic.
When to Use Dimensional Types
| Scenario | Recommendation |
|---|---|
| Sensor readings (temp, voltage, fan) | ✅ Always — prevents unit confusion |
| Threshold comparisons | ✅ Always — generic Threshold<T> |
| Cross-subsystem data exchange | ✅ Always — enforce contracts at API boundaries |
| Internal calculations (same unit throughout) | ⚠️ Optional — less bug-prone |
| String/display formatting | ❌ Use Display impl on the quantity type |
Sensor Pipeline Type Flow
flowchart LR
RAW["raw: &[u8]"] -->|parse| C["Celsius(f64)"]
RAW -->|parse| R["Rpm(u32)"]
RAW -->|parse| V["Volts(f64)"]
C -->|threshold check| TC["Threshold<Celsius>"]
R -->|threshold check| TR["Threshold<Rpm>"]
C -.->|"C + R"| ERR["❌ mismatched types"]
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
Exercise: Power Budget Calculator
Create Watts(f64) and Amperes(f64) newtypes. Implement:
Watts::from_vi(volts: Volts, amps: Amperes) -> Watts(P = V × I)- A
PowerBudgetthat tracks total watts and rejects additions that exceed a configured limit. - Attempting
Watts + Celsiusshould be a compile error.
Solution
#[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!("budget exceeded: {:?} > {:?}", new_total, self.limit));
}
self.total = new_total;
Ok(())
}
}
// ❌ Compile error: Watts + Celsius → "mismatched types"
// let bad = Watts(100.0) + Celsius(50.0);
Key Takeaways
- Newtypes prevent unit confusion at zero cost —
CelsiusandRpmare bothf64inside, but the compiler treats them as different types. - The Mars Climate Orbiter bug is impossible — passing
PoundswhereNewtonsis expected is a compile error. quantity!macro reduces boilerplate — stamp out Display, arithmetic, and threshold logic for each unit.uomcrate handles derived units — use it when you needWatts = Volts × Amperesautomatically.- Threshold is generic over the quantity —
Threshold<Celsius>can’t accidentally compare toThreshold<Rpm>.
Validated Boundaries — Parse, Don’t Validate 🟡
What you’ll learn: How to validate data exactly once at the system boundary, carry the proof of validity in a dedicated type, and never re-check — applied to IPMI FRU records (flat bytes), Redfish JSON (structured documents), and IPMI SEL records (polymorphic binary with nested dispatch), with a complete end-to-end walkthrough.
Cross-references: ch02 (typed commands), ch06 (dimensional types), ch11 (trick 2 — sealed traits, trick 3 —
#[non_exhaustive], trick 5 — FromStr), ch14 (proptest)
The Problem: Shotgun Validation
In typical code, validation is scattered everywhere. Every function that receives data re-checks it “just in case”:
// C — validation scattered across the codebase
int process_fru_data(uint8_t *data, int len) {
if (data == NULL) return -1; // check: non-null
if (len < 8) return -1; // check: minimum length
if (data[0] != 0x01) return -1; // check: format version
if (checksum(data, len) != 0) return -1; // check: checksum
// ... 10 more functions that repeat the same checks ...
}
This pattern (“shotgun validation”) has two problems:
- Redundancy — the same checks appear in dozens of places
- Incompleteness — forget one check in one function and you have a bug
Parse, Don’t Validate
The correct-by-construction approach: validate once at the boundary, then carry the proof of validity in the type.
/// Raw bytes from the wire — not yet validated.
#[derive(Debug)]
pub struct RawFruData(Vec<u8>);
Case Study: IPMI FRU Data
#[derive(Debug)]
pub struct RawFruData(Vec<u8>);
/// Validated IPMI FRU data. Can only be created via TryFrom,
/// which enforces all invariants. Once you have a ValidFru,
/// all data is guaranteed correct.
#[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 data too short: {actual} bytes (minimum {minimum})"),
Self::BadFormatVersion(v) =>
write!(f, "unsupported FRU format version: {v}"),
Self::ChecksumMismatch { expected, actual } =>
write!(f, "checksum mismatch: expected 0x{expected:02X}, got 0x{actual:02X}"),
Self::InvalidAreaOffset { area, offset } =>
write!(f, "invalid {area} area offset: {offset}"),
}
}
}
impl TryFrom<RawFruData> for ValidFru {
type Error = FruError;
fn try_from(raw: RawFruData) -> Result<Self, FruError> {
let data = raw.0;
// 1. Length check
if data.len() < 8 {
return Err(FruError::TooShort {
actual: data.len(),
minimum: 8,
});
}
// 2. Format version
if data[0] != 0x01 {
return Err(FruError::BadFormatVersion(data[0]));
}
// 3. Checksum (header is first 8 bytes, checksum at byte 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. Area offsets must be within bounds
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,
});
}
}
// All checks passed — construct the validated type
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 {
/// No validation needed — the type guarantees correctness.
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..]) // safe — bounds checked during parsing
}
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
}
}
Any function that takes &ValidFru knows the data is well-formed. No re-checking:
pub struct ValidFru { board_area_offset: u8, data: Vec<u8> }
impl ValidFru {
pub fn board_area(&self) -> Option<&[u8]> { None }
}
/// This function does NOT need to validate the FRU data.
/// The type signature guarantees it's already valid.
fn extract_board_serial(fru: &ValidFru) -> Option<String> {
let board = fru.board_area()?;
// ... parse serial from board area ...
// No bounds checks needed — ValidFru guarantees offsets are in range
Some("ABC123".to_string()) // stub
}
fn extract_board_manufacturer(fru: &ValidFru) -> Option<String> {
let board = fru.board_area()?;
// Still no validation needed — same guarantee
Some("Acme Corp".to_string()) // stub
}
Validated Redfish JSON
The same pattern applies to Redfish API responses. Parse once, carry validity in the type:
use std::collections::HashMap;
/// Raw JSON string from a Redfish endpoint.
pub struct RawRedfishResponse(pub String);
/// A validated Redfish Thermal response.
/// All required fields are guaranteed present and within range.
#[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, // guaranteed non-NaN, within sensor range
pub upper_critical: f64,
pub status: HealthStatus,
}
#[derive(Debug)]
pub struct ValidFanReading {
pub name: String,
pub reading_rpm: u32, // guaranteed > 0 for present fans
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, "missing required field: {name}"),
Self::OutOfRange { field, value } =>
write!(f, "field {field} out of range: {value}"),
Self::InvalidStatus(s) => write!(f, "invalid health status: {s}"),
}
}
}
// Once validated, downstream code never re-checks:
fn check_thermal_health(thermal: &ValidThermalResponse) -> bool {
// No need to check for missing fields or NaN values.
// ValidThermalResponse guarantees all readings are sensible.
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
})
}
Polymorphic Validation: IPMI SEL Records
The first two case studies validated flat structures — a fixed byte layout (FRU) and a known JSON schema (Redfish). Real-world data is often polymorphic: the interpretation of later bytes depends on earlier bytes. IPMI System Event Log (SEL) records are the canonical example.
The Shape of the Problem
Every SEL record is exactly 16 bytes. But what those bytes mean depends on a dispatch chain:
Byte 2: Record Type
├─ 0x02 → System Event
│ Byte 10[6:4]: Event Type
│ ├─ 0x01 → Threshold event (reading + threshold in data bytes 2-3)
│ ├─ 0x02-0x0C → Discrete event (bit in offset field)
│ └─ 0x6F → Sensor-specific (meaning depends on Sensor Type in byte 7)
│ Byte 7: Sensor Type
│ ├─ 0x01 → Temperature events
│ ├─ 0x02 → Voltage events
│ ├─ 0x04 → Fan events
│ ├─ 0x07 → Processor events
│ ├─ 0x0C → Memory events
│ ├─ 0x08 → Power Supply events
│ └─ ... → (42 sensor types in IPMI 2.0 Table 42-3)
├─ 0xC0-0xDF → OEM Timestamped
└─ 0xE0-0xFF → OEM Non-Timestamped
In C, this is a switch inside a switch inside a switch, with each level sharing
the same uint8_t *data pointer. Forget one level, misread the spec table, or index
the wrong byte — the bug is silent.
// C — the polymorphic parsing problem
void process_sel_entry(uint8_t *data, int len) {
if (data[2] == 0x02) { // system event
uint8_t event_type = (data[10] >> 4) & 0x07;
if (event_type == 0x01) { // threshold
uint8_t reading = data[11]; // 🐛 or is it data[13]?
uint8_t threshold = data[12]; // 🐛 spec says byte 12 is trigger, not threshold
printf("Temp: %d crossed %d\n", reading, threshold);
} else if (event_type == 0x6F) { // sensor-specific
uint8_t sensor_type = data[7];
if (sensor_type == 0x0C) { // memory
// 🐛 forgot to check event data 1 offset bits
printf("Memory ECC error\n");
}
// 🐛 no else — silently drops 30+ other sensor types
}
}
// 🐛 OEM record types silently ignored
}
Step 1 — Parse the Outer Frame
The first TryFrom dispatches on record type — the outermost layer of the union:
/// Raw 16-byte SEL record, straight from `Get SEL Entry` (IPMI cmd 0x43).
pub struct RawSelRecord(pub [u8; 16]);
/// Validated SEL record — record type dispatched, all fields checked.
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, "unknown record type: 0x{t:02X}"),
Self::UnknownSensorType(t) => write!(f, "unknown sensor type: 0x{t:02X}"),
Self::UnknownEventType(t) => write!(f, "unknown event type: 0x{t:02X}"),
Self::InvalidEventData { reason } => write!(f, "invalid event data: {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)),
}
}
}
After this boundary, every consumer matches on the enum. The compiler enforces handling all three record types — you can’t “forget” OEM records.
Step 2 — Parse the System Event: Sensor Type → Typed Event
The inner dispatch turns the event data bytes into a sum type indexed by sensor
type. This is where the C switch-in-a-switch becomes a nested 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, // ← the key: event data is TYPED
}
#[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 }
// ──── The Sensor/Event Type Hierarchy ────
/// Sensor types from IPMI Table 42-3. Non-exhaustive because future
/// IPMI revisions and OEM ranges will add variants (see ch11 trick 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
}
/// The polymorphic payload — each variant carries its own typed data.
#[derive(Debug)]
pub enum TypedEvent {
Threshold(ThresholdEvent),
SensorSpecific(SensorSpecificEvent),
Discrete { offset: u8, event_data: [u8; 3] },
}
/// Threshold events carry the trigger reading and threshold value.
/// Both are raw sensor values (pre-linearization), kept as u8.
/// After SDR linearization, they become dimensional types (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,
}
/// Sensor-specific events — each sensor type gets its own variant
/// with an exhaustive enum of that sensor's defined events.
#[derive(Debug)]
pub enum SensorSpecificEvent {
Temperature(TempEvent),
Voltage(VoltageEvent),
Fan(FanEvent),
Processor(ProcessorEvent),
PowerSupply(PowerSupplyEvent),
Memory(MemoryEvent),
PhysicalSecurity(PhysicalSecurityEvent),
Watchdog(WatchdogEvent),
}
// ──── Per-sensor-type event enums (from IPMI Table 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,
}
Step 3 — The Parser Wiring
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 => {
// Threshold — event data byte 2 is trigger reading, byte 3 is threshold
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 => {
// Sensor-specific — dispatch on sensor type
let offset = event_data[0] & 0x0F;
let specific = parse_sensor_specific(&sensor_type, offset)?;
TypedEvent::SensorSpecific(specific)
}
0x02..=0x0C => {
// Generic discrete
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: "threshold offset out of range",
}),
}
}
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: "unknown memory event offset",
}),
};
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: "unknown power supply event offset",
}),
};
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: "unknown processor event offset",
}),
};
Ok(SensorSpecificEvent::Processor(ev))
}
// Pattern repeats for Temperature, Voltage, Fan, etc.
// Each sensor type maps its offsets to a dedicated enum.
_ => Err(SelParseError::InvalidEventData {
reason: "sensor-specific dispatch not implemented for this sensor type",
}),
}
}
Step 4 — Consuming Typed SEL Records
Once parsed, downstream code pattern-matches on the nested enums. The compiler enforces exhaustive handling — no silent fallthrough, no forgotten sensor type:
/// Determine whether a SEL event should trigger a hardware alert.
/// The compiler ensures every variant is handled.
fn should_alert(record: &ValidSelRecord) -> bool {
match record {
ValidSelRecord::SystemEvent(sys) => match &sys.event {
TypedEvent::Threshold(t) => {
// Any critical or non-recoverable threshold crossing → alert
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
),
// New sensor type variant added in a future version?
// ❌ Compile error: non-exhaustive patterns
_ => false,
},
TypedEvent::Discrete { .. } => false,
},
// OEM records are not alertable in this policy
ValidSelRecord::OemTimestamped(_) => false,
ValidSelRecord::OemNonTimestamped(_) => false,
}
}
/// Generate a human-readable description.
/// Every branch produces a specific message — no "unknown event" fallback.
fn describe(record: &ValidSelRecord) -> String {
match record {
ValidSelRecord::SystemEvent(sys) => {
let sensor = format!("{:?} sensor #{}", sys.sensor_type, sys.sensor_number);
let dir = match sys.event_direction {
EventDirection::Assertion => "asserted",
EventDirection::Deassertion => "deasserted",
};
match &sys.event {
TypedEvent::Threshold(t) => {
format!("{sensor}: {:?} {dir} (reading: 0x{:02X}, threshold: 0x{:02X})",
t.crossing, t.trigger_reading, t.threshold_value)
}
TypedEvent::SensorSpecific(ss) => {
format!("{sensor}: {ss:?} {dir}")
}
TypedEvent::Discrete { offset, .. } => {
format!("{sensor}: discrete offset {offset:#x} {dir}")
}
}
}
ValidSelRecord::OemTimestamped(oem) =>
format!("OEM record 0x{:04X} (mfr {:02X}{:02X}{:02X})",
oem.record_id,
oem.manufacturer_id[0], oem.manufacturer_id[1], oem.manufacturer_id[2]),
ValidSelRecord::OemNonTimestamped(oem) =>
format!("OEM non-ts record 0x{:04X}", oem.record_id),
}
}
Walkthrough: End-to-End SEL Processing
Here’s a complete flow — from raw bytes off the wire to an alert decision — showing every typed handoff:
/// Process all SEL entries from a BMC, producing typed alerts.
fn process_sel_log(raw_entries: &[[u8; 16]]) -> Vec<String> {
let mut alerts = Vec::new();
for (i, raw_bytes) in raw_entries.iter().enumerate() {
// ─── Boundary: raw bytes → validated record ───
let raw = RawSelRecord(*raw_bytes);
let record = match ValidSelRecord::try_from(raw) {
Ok(r) => r,
Err(e) => {
eprintln!("SEL entry {i}: parse error: {e}");
continue;
}
};
// ─── From here, everything is typed ───
// 1. Describe the event (exhaustive match — every variant covered)
let description = describe(&record);
println!("SEL[{i}]: {description}");
// 2. Check alert policy (exhaustive match — compiler proves completeness)
if should_alert(&record) {
alerts.push(description);
}
// 3. Extract dimensional readings from threshold events
if let ValidSelRecord::SystemEvent(sys) = &record {
if let TypedEvent::Threshold(t) = &sys.event {
// The compiler knows t.trigger_reading is a threshold event reading,
// not an arbitrary byte. After SDR linearization (ch06), this becomes:
// let temp: Celsius = linearize(t.trigger_reading, &sdr);
// And then Celsius can't be compared with Rpm.
println!(
" → raw reading: 0x{:02X}, raw threshold: 0x{:02X}",
t.trigger_reading, t.threshold_value
);
}
}
}
alerts
}
fn main() {
// Example: two SEL entries (fabricated for illustration)
let sel_data: Vec<[u8; 16]> = vec![
// Entry 1: System event, Memory sensor #3, sensor-specific,
// offset 0x00 = CorrectableEcc, assertion
[
0x01, 0x00, // record ID: 1
0x02, // record type: system event
0x00, 0x00, 0x00, 0x00, // timestamp (stub)
0x20, // generator: IPMB slave addr 0x20
0x00, // channel/lun
0x04, // event message rev
0x0C, // sensor type: Memory (0x0C)
0x03, // sensor number: 3
0x6F, // event dir: assertion, event type: sensor-specific
0x00, // event data 1: offset 0x00 = CorrectableEcc
0x00, 0x00, // event data 2-3
],
// Entry 2: System event, Temperature sensor #1, threshold,
// offset 0x09 = UpperCriticalHigh, reading=95, threshold=90
[
0x02, 0x00, // record ID: 2
0x02, // record type: system event
0x00, 0x00, 0x00, 0x00, // timestamp (stub)
0x20, // generator
0x00, // channel/lun
0x04, // event message rev
0x01, // sensor type: Temperature (0x01)
0x01, // sensor number: 1
0x01, // event dir: assertion, event type: threshold (0x01)
0x09, // event data 1: offset 0x09 = UpperCriticalHigh
0x5F, // event data 2: trigger reading (95 raw)
0x5A, // event data 3: threshold value (90 raw)
],
];
let alerts = process_sel_log(&sel_data);
println!("\n=== ALERTS ({}) ===", alerts.len());
for alert in &alerts {
println!(" 🚨 {alert}");
}
}
Expected output:
SEL[0]: Memory sensor #3: Memory(CorrectableEcc) asserted
SEL[1]: Temperature sensor #1: UpperCriticalHigh asserted (reading: 0x5F, threshold: 0x5A)
→ raw reading: 0x5F, raw threshold: 0x5A
=== ALERTS (1) ===
🚨 Temperature sensor #1: UpperCriticalHigh asserted (reading: 0x5F, threshold: 0x5A)
Entry 0 (correctable ECC) is logged but not alerted. Entry 1 (upper critical temperature) triggers an alert. Both decisions are enforced by exhaustive pattern matching — the compiler proves every sensor type and threshold crossing is handled.
From Parsed Events to Redfish Health: The Consumer Pipeline
The walkthrough above ends with alerts — but in a real BMC, parsed SEL records
flow into the Redfish health rollup (ch18).
The current handoff is a lossy bool:
// ❌ Lossy — throws away per-subsystem detail
pub struct SelSummary {
pub has_critical_events: bool,
pub total_entries: u32,
}
This loses everything the type system just gave us: which subsystem is affected, what severity level, and whether the reading carries dimensional data. Let’s build the full pipeline.
Step 1 — SDR Linearization: Raw Bytes → Dimensional Types (ch06)
Threshold SEL events carry raw sensor readings in event data bytes 2-3. The IPMI SDR (Sensor Data Record) provides the linearization formula. After linearization, the raw byte becomes a dimensional type:
/// SDR linearization coefficients for a single sensor.
/// See IPMI spec section 36.3 for the full formula.
pub struct SdrLinearization {
pub sensor_type: SensorType,
pub m: i16, // multiplier
pub b: i16, // offset
pub r_exp: i8, // result exponent (power-of-10)
pub b_exp: i8, // B exponent
}
/// A linearized sensor reading with its unit attached.
/// The return type depends on the sensor type — the compiler
/// enforces that temperature sensors produce Celsius, not 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 {
/// Apply the IPMI linearization formula:
/// y = (M × raw + B × 10^B_exp) × 10^R_exp
/// Returns a dimensional type based on the sensor type.
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)),
// Other sensor types — extend as needed
_ => LinearizedReading::Temperature(Celsius(y)),
}
}
}
With this, the raw byte 0x5F (95 decimal) from our SEL walkthrough becomes
Celsius(95.0) — and the compiler prevents comparing it with Rpm or Watts.
Step 2 — Per-Subsystem Health Classification
Instead of collapsing everything into has_critical_events: bool, classify each
parsed SEL event into a per-subsystem health bucket:
/// Worst-of health value — Ord gives us `.max()` for free.
/// (Full definition in ch18; reproduced here for the SEL pipeline.)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HealthValue { OK, Warning, Critical }
/// Health contribution from a single SEL event, classified by subsystem.
#[derive(Debug, Clone)]
pub enum SubsystemHealth {
Processor(HealthValue),
Memory(HealthValue),
PowerSupply(HealthValue),
Thermal(HealthValue),
Fan(HealthValue),
Storage(HealthValue),
Security(HealthValue),
}
/// Classify a typed SEL event into per-subsystem health.
/// Exhaustive matching ensures every sensor type contributes.
fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth {
match &record.event {
TypedEvent::Threshold(t) => {
// Threshold severity depends on the crossing level
let health = match t.crossing {
// Non-critical → Warning
ThresholdCrossing::UpperNonCriticalLow
| ThresholdCrossing::UpperNonCriticalHigh
| ThresholdCrossing::LowerNonCriticalLow
| ThresholdCrossing::LowerNonCriticalHigh => HealthValue::Warning,
// Critical or Non-recoverable → Critical
ThresholdCrossing::UpperCriticalLow
| ThresholdCrossing::UpperCriticalHigh
| ThresholdCrossing::LowerCriticalLow
| ThresholdCrossing::LowerCriticalHigh
| ThresholdCrossing::UpperNonRecoverableLow
| ThresholdCrossing::UpperNonRecoverableHigh
| ThresholdCrossing::LowerNonRecoverableLow
| ThresholdCrossing::LowerNonRecoverableHigh => HealthValue::Critical,
};
// Route to the correct subsystem based on sensor type
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),
// Temperature, Voltage, Fan sensor-specific events
SensorSpecificEvent::Temperature(_) =>
SubsystemHealth::Thermal(HealthValue::Warning),
SensorSpecificEvent::Voltage(_) =>
SubsystemHealth::PowerSupply(HealthValue::Warning),
SensorSpecificEvent::Fan(_) =>
SubsystemHealth::Fan(HealthValue::Warning),
},
TypedEvent::Discrete { .. } => {
// Generic discrete — classify by sensor type with Warning
match record.sensor_type {
SensorType::Processor => SubsystemHealth::Processor(HealthValue::Warning),
SensorType::Memory => SubsystemHealth::Memory(HealthValue::Warning),
_ => SubsystemHealth::Thermal(HealthValue::OK),
}
}
}
}
Every match arm is exhaustive — add a new MemoryEvent variant and the compiler
forces you to decide its severity. Add a new SensorSpecificEvent variant and
every consumer must classify it. This is the payoff of the enum tree from the
parsing section.
Step 3 — Aggregate into a Typed SEL Summary
Replace the lossy bool with a structured summary that preserves per-subsystem
health:
use std::collections::HashMap;
/// Rich SEL summary — per-subsystem health derived from typed events.
/// This is what gets handed to the Redfish server (ch18) for health rollup.
#[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,
/// Dimensional readings from threshold events (post-linearization).
pub threshold_readings: Vec<LinearizedThresholdEvent>,
}
/// A threshold event with linearized readings attached.
#[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,
}
/// Build a TypedSelSummary from parsed SEL records.
/// This is the consumer pipeline: parse (Step 0 above) → classify → aggregate.
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 records don't contribute to health
};
// ── Classify event → per-subsystem health ──
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),
}
// ── Linearize threshold readings if SDR is available ──
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,
}
}
Step 4 — The Full Pipeline: Raw Bytes → Redfish Health
Here’s the complete consumer pipeline, showing every typed handoff from raw SEL bytes to Redfish-ready health values:
flowchart LR
RAW["Raw [u8; 16]\nSEL entries"]
PARSE["TryFrom:\nValidSelRecord\n(enum tree)"]
CLASSIFY["classify_event_health\n(exhaustive match)"]
LINEARIZE["SDR linearize\nraw → Celsius/Rpm/Watts"]
SUMMARY["TypedSelSummary\n(per-subsystem health\n+ dimensional readings)"]
REDFISH["ch18: health rollup\n→ Status.Health JSON"]
RAW -->|"ch07 §Parse"| PARSE
PARSE -->|"typed events"| CLASSIFY
PARSE -->|"threshold bytes"| 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() {
// ── Raw SEL data from BMC ──
let raw_entries: Vec<[u8; 16]> = vec![
// Memory correctable ECC on sensor #3
[0x01,0x00, 0x02, 0x00,0x00,0x00,0x00,
0x20,0x00, 0x04, 0x0C, 0x03, 0x6F, 0x00, 0x00,0x00],
// Temperature upper critical on sensor #1, reading=95, threshold=90
[0x02,0x00, 0x02, 0x00,0x00,0x00,0x00,
0x20,0x00, 0x04, 0x01, 0x01, 0x01, 0x09, 0x5F,0x5A],
// PSU failure on sensor #5
[0x03,0x00, 0x02, 0x00,0x00,0x00,0x00,
0x20,0x00, 0x04, 0x08, 0x05, 0x6F, 0x01, 0x00,0x00],
];
// ── Step 0: Parse at the boundary (ch07 TryFrom) ──
let records: Vec<ValidSelRecord> = raw_entries.iter()
.filter_map(|raw| ValidSelRecord::try_from(RawSelRecord(*raw)).ok())
.collect();
// ── Step 1-3: Classify + linearize + aggregate ──
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 mapping for this example
});
let summary = summarize_sel(&records, &sdr_table);
// ── Result: structured, typed, Redfish-ready ──
println!("SEL Summary:");
println!(" Total entries: {}", summary.total_entries);
println!(" Processor: {:?}", summary.processor_health); // OK
println!(" Memory: {:?}", summary.memory_health); // OK (correctable → OK)
println!(" Power: {:?}", summary.power_health); // Critical (PSU failure)
println!(" Thermal: {:?}", summary.thermal_health); // Critical (upper critical)
println!(" Fan: {:?}", summary.fan_health); // OK
println!(" Security: {:?}", summary.security_health); // OK
// Dimensional readings preserved from threshold events:
for r in &summary.threshold_readings {
println!(" Threshold: sensor {:?} #{} — {:?} crossed {:?}",
r.sensor_type, r.sensor_number,
r.trigger_reading, r.crossing);
// trigger_reading is LinearizedReading::Temperature(Celsius(95.0))
// — not a raw byte, not an untyped f64
}
// ── This summary feeds directly into ch18's health rollup ──
// compute_system_health() can now use per-subsystem values
// instead of a single `has_critical_events: bool`
}
Expected output:
SEL Summary:
Total entries: 3
Processor: OK
Memory: OK
Power: Critical
Thermal: Critical
Fan: OK
Security: OK
Threshold: sensor Temperature #1 — Temperature(Celsius(95.0)) crossed UpperCriticalHigh
What the Consumer Pipeline Proves
| Stage | Pattern | What’s Enforced |
|---|---|---|
| Parse | Validated boundary (ch07) | Every consumer works with typed enums, never raw bytes |
| Classify | Exhaustive matching | Every sensor type and event variant maps to a health value — can’t forget one |
| Linearize | Dimensional analysis (ch06) | Raw byte 0x5F becomes Celsius(95.0), not f64 — can’t confuse with RPM |
| Aggregate | Typed fold | Per-subsystem health uses HealthValue::max() — Ord guarantees correctness |
| Handoff | Structured summary | ch18 receives TypedSelSummary with 7 subsystem health values, not a bool |
Compare with the untyped C pipeline:
| Step | C | Rust |
|---|---|---|
| Parse record type | switch with possible fallthrough | match on enum — exhaustive |
| Classify severity | manual if chain, forgot PSU | exhaustive match — compiler error on missing variant |
| Linearize reading | double — no unit | Celsius / Rpm / Watts — distinct types |
| Aggregate health | bool has_critical | 7 typed subsystem fields |
| Handoff to Redfish | untyped json_object_set("Health", "OK") | TypedSelSummary → typed health rollup (ch18) |
The Rust pipeline doesn’t just prevent more bugs — it produces richer output. The C pipeline loses information at every stage (polymorphic → flat, dimensional → untyped, per-subsystem → single bool). The Rust pipeline preserves it all, because the type system makes it easier to keep the structure than to throw it away.
What the Compiler Proves
| Bug in C | How Rust prevents it |
|---|---|
| Forgot to check record type | match on ValidSelRecord — must handle all three variants |
| Wrong byte index for trigger reading | Parsed once into ThresholdEvent.trigger_reading — consumers never touch raw bytes |
Missing case for a sensor type | SensorSpecificEvent match is exhaustive — compiler error on missing variant |
| Silently dropped OEM records | Enum variant exists — must be handled or explicitly _ => ignored |
| Compared threshold reading (°C) with fan offset | After SDR linearization, Celsius ≠ Rpm (ch06) |
| Added new sensor type, forgot alert logic | #[non_exhaustive] + exhaustive match → compiler error in downstream crates |
| Event data parsed differently in two code paths | Single parse_system_event() boundary — one source of truth |
The Three-Beat Pattern
Looking back at this chapter’s three case studies, notice the graduated arc:
| Case Study | Input Shape | Parsing Complexity | Key Technique |
|---|---|---|---|
| FRU (bytes) | Flat, fixed layout | One TryFrom, check fields | Validated boundary type |
| Redfish (JSON) | Structured, known schema | One TryFrom, check fields + nesting | Same technique, different transport |
| SEL (polymorphic bytes) | Nested discriminated union | Dispatch chain: record type → event type → sensor type | Enum tree + exhaustive matching |
The principle is identical in all three: validate once at the boundary, carry the proof in the type, never re-check. The SEL case study shows this principle scales to arbitrarily complex polymorphic data — the type system handles nested dispatch just as naturally as flat field validation.
Composing Validated Types
Validated types compose — a struct of validated fields is itself validated:
#[derive(Debug)]
pub struct ValidFru { format_version: u8 }
#[derive(Debug)]
pub struct ValidThermalResponse { }
/// A fully validated system snapshot.
/// Each field was validated independently; the composite is also valid.
#[derive(Debug)]
pub struct ValidSystemSnapshot {
pub fru: ValidFru,
pub thermal: ValidThermalResponse,
// Each field carries its own validity guarantee.
// No need for a "validate_snapshot()" function.
}
/// Because ValidSystemSnapshot is composed of validated parts,
/// any function that receives it can trust ALL the data.
fn generate_health_report(snapshot: &ValidSystemSnapshot) {
println!("FRU version: {}", snapshot.fru.format_version);
// No validation needed — the type guarantees everything
}
The Key Insight
Validate at the boundary. Carry the proof in the type. Never re-check.
This eliminates an entire class of bugs: “forgot to validate in this one function.”
If a function takes &ValidFru, the data IS valid. Period.
When to Use Validated Boundary Types
| Data Source | Use validated boundary type? |
|---|---|
| IPMI FRU data from BMC | ✅ Always — complex binary format |
| Redfish JSON responses | ✅ Always — many required fields |
| PCIe configuration space | ✅ Always — register layout is strict |
| SMBIOS tables | ✅ Always — versioned format with checksums |
| User-provided test parameters | ✅ Always — prevent injection |
| Internal function calls | ❌ Usually not — types already constrain |
| Log messages | ❌ No — best-effort, not safety-critical |
Validation Boundary Flow
flowchart LR
RAW["Raw bytes / JSON"] -->|"TryFrom / serde"| V{"Valid?"}
V -->|Yes| VT["ValidFru / ValidRedfish"]
V -->|No| 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
Exercise: Validated SMBIOS Table
Design a ValidSmbiosType17 type for SMBIOS Type 17 (Memory Device) records:
- Raw input is
&[u8]; minimum length 21 bytes, byte 0 must be 0x11. - Fields:
handle: u16,size_mb: u16,speed_mhz: u16. - Use
TryFrom<&[u8]>so that all downstream functions take&ValidSmbiosType17.
Solution
#[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!("too short: {} < 21", raw.len()));
}
if raw[0] != 0x11 {
return Err(format!("wrong type: 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]]),
})
}
}
// Downstream functions take the validated type — no re-checking
pub fn report_dimm(dimm: &ValidSmbiosType17) -> String {
format!("DIMM handle 0x{:04X}: {}MB @ {}MHz",
dimm.handle, dimm.size_mb, dimm.speed_mhz)
}
Key Takeaways
- Parse once at the boundary —
TryFromvalidates raw data exactly once; all downstream code trusts the type. - Eliminate shotgun validation — if a function takes
&ValidFru, the data IS valid. Period. - The pattern scales from flat to polymorphic — FRU (flat bytes), Redfish (structured JSON), and SEL (nested discriminated union) all use the same technique at increasing complexity.
- Exhaustive matching is validation — for polymorphic data like SEL, the compiler’s enum exhaustiveness check prevents the “forgot a sensor type” class of bugs with zero runtime cost.
- The consumer pipeline preserves structure — parsing → classification → linearization → aggregation keeps per-subsystem health and dimensional readings intact, where C lossy-reduces to a single
bool. The type system makes it easier to keep information than to throw it away. serdeis a natural boundary —#[derive(Deserialize)]with#[serde(try_from)]validates JSON at parse time.- Compose validated types — a
ValidServerHealthcan requireValidFru+ValidThermal+ValidPower. - Pair with proptest (ch14) — fuzz the
TryFromboundary to ensure no valid input is rejected and no invalid input sneaks through. - These patterns compose into full Redfish workflows — ch17 applies validated boundaries on the client side (parsing JSON responses into typed structs), while ch18 inverts the pattern on the server side (builder type-state ensures every required field is present before serialization). The SEL consumer pipeline built here feeds directly into ch18’s
TypedSelSummaryhealth rollup.
Capability Mixins — Compile-Time Hardware Contracts 🟡
What you’ll learn: How ingredient traits (bus capabilities) combined with mixin traits and blanket impls eliminate diagnostic code duplication while guaranteeing every hardware dependency is satisfied at compile time.
Cross-references: ch04 (capability tokens), ch09 (phantom types), ch10 (integration)
The Problem: Diagnostic Code Duplication
Server platforms share diagnostic patterns across subsystems. Fan diagnostics, temperature monitoring, and power sequencing all follow similar workflows but operate on different hardware buses. Without abstraction, you get copy-paste:
// C — duplicated logic across subsystems
int run_fan_diag(spi_bus_t *spi, i2c_bus_t *i2c) {
// ... 50 lines of SPI sensor read ...
// ... 30 lines of I2C register check ...
// ... 20 lines of threshold comparison (same as CPU diag) ...
}
int run_cpu_temp_diag(i2c_bus_t *i2c, gpio_t *gpio) {
// ... 30 lines of I2C register check (same as fan diag) ...
// ... 15 lines of GPIO alert check ...
// ... 20 lines of threshold comparison (same as fan diag) ...
}
The threshold comparison logic is identical, but you can’t extract it because the bus types differ. With capability mixins, each hardware bus is an ingredient trait, and diagnostic behaviors are automatically provided when the right ingredients are present.
Ingredient Traits (Hardware Capabilities)
Each bus or peripheral is an associated type on a trait. A diagnostic controller declares which buses it has:
/// SPI bus capability.
pub trait HasSpi {
type Spi: SpiBus;
fn spi(&self) -> &Self::Spi;
}
/// I2C bus capability.
pub trait HasI2c {
type I2c: I2cBus;
fn i2c(&self) -> &Self::I2c;
}
/// GPIO pin access capability.
pub trait HasGpio {
type Gpio: GpioController;
fn gpio(&self) -> &Self::Gpio;
}
/// IPMI access capability.
pub trait HasIpmi {
type Ipmi: IpmiClient;
fn ipmi(&self) -> &Self::Ipmi;
}
// Bus trait definitions:
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>;
}
Mixin Traits (Diagnostic Behaviors)
A mixin provides behavior automatically to any type that has the required capabilities:
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; }
/// Fan diagnostic mixin — auto-implemented for anything with SPI + I2C.
pub trait FanDiagMixin: HasSpi + HasI2c {
fn read_fan_speed(&self, fan_id: u8) -> u32 {
// Read tachometer via 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) {
// Set PWM via I2C controller
self.i2c().write_register(0x2E, fan_id, duty_percent);
}
fn run_fan_diagnostic(&self) -> bool {
// Full diagnostic: read all fans, check thresholds
for fan_id in 0..6 {
let speed = self.read_fan_speed(fan_id);
if speed < 1000 || speed > 20000 {
println!("Fan {fan_id}: FAIL ({speed} RPM)");
return false;
}
}
true
}
}
// Blanket implementation — ANY type with SPI + I2C gets FanDiagMixin for free
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}
/// Temperature monitoring mixin — requires 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 // 0.5°C per LSB
}
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!("Sensor 0x{addr:02X}: CRITICAL ({temp}°C)");
return false;
}
if self.check_thermal_alert(addr as u32) {
println!("Sensor 0x{addr:02X}: ALERT pin asserted");
return false;
}
}
true
}
}
impl<T: HasI2c + HasGpio> TempMonitorMixin for T {}
/// Power sequencing mixin — requires 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 // 10mV per LSB
}
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 {}
Concrete Controller — Mix and Match
A concrete diagnostic controller declares its capabilities, and automatically inherits all matching mixins:
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 {}
// Concrete bus implementations (stubs for illustration)
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 has ALL buses → gets ALL mixins.
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 now automatically has:
// - FanDiagMixin (because it HasSpi + HasI2c)
// - TempMonitorMixin (because it HasI2c + HasGpio)
// - PowerSeqMixin (because it HasI2c + HasIpmi)
// No manual implementation needed — blanket impls do it all.
Correct-by-Construction Aspect
The mixin pattern is correct-by-construction because:
- You can’t call
read_fan_speed()without SPI — the method only exists on types that implementHasSpi + HasI2c - You can’t forget a bus — if you remove
HasSpifromBaseBoardController,FanDiagMixinmethods disappear at compile time - Mock testing is automatic — replace
LinuxSpiwithMockSpiand all mixin logic works with the mock - New platforms just declare capabilities — a GPU daughter card with only I2C
gets
TempMonitorMixin(if it also has GPIO) but notFanDiagMixin(no SPI)
When to Use Capability Mixins
| Scenario | Use mixins? |
|---|---|
| Cross-cutting diagnostic behaviors | ✅ Yes — prevent copy-paste |
| Multi-bus hardware controllers | ✅ Yes — declare capabilities, get behaviors |
| Platform-specific test harnesses | ✅ Yes — mock capabilities for testing |
| Single-bus simple peripherals | ⚠️ Overhead may not be worth it |
| Pure business logic (no hardware) | ❌ Simpler patterns suffice |
Mixin Trait Architecture
flowchart TD
subgraph "Ingredient Traits"
SPI["HasSpi"]
I2C["HasI2c"]
GPIO["HasGpio"]
end
subgraph "Mixin Traits (blanket impls)"
FAN["FanDiagMixin"]
TEMP["TempMonitorMixin"]
end
SPI & I2C -->|"requires both"| FAN
I2C & GPIO -->|"requires both"| TEMP
subgraph "Concrete Types"
BBC["BaseBoardController"]
end
BBC -->|"impl 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
Exercise: Network Diagnostic Mixins
Design a mixin system for network diagnostics:
- Ingredient traits:
HasEthernet,HasIpmi - Mixin:
LinkHealthMixin(requiresHasEthernet) withcheck_link_status(&self) - Mixin:
RemoteDiagMixin(requiresHasEthernet + HasIpmi) withremote_health_check(&self) - Concrete type:
NicControllerthat implements both ingredients.
Solution
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 automatically gets both mixin methods
Key Takeaways
- Ingredient traits declare hardware capabilities —
HasSpi,HasI2c,HasGpioare associated-type traits. - Mixin traits provide behaviour via blanket impls —
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}. - Adding a new platform = listing its capabilities — the compiler provides all matching mixin methods.
- Removing a bus = compile errors everywhere it’s used — you can’t forget to update downstream code.
- Mock testing is free — swap
LinuxSpiforMockSpi; all mixin logic works unchanged.
Phantom Types for Resource Tracking 🟡
What you’ll learn: How
PhantomDatamarkers encode register width, DMA direction, and file-descriptor state at the type level — preventing an entire class of resource-mismatch bugs at zero runtime cost.Cross-references: ch05 (type-state), ch06 (dimensional types), ch08 (mixins), ch10 (integration)
The Problem: Mixing Up Resources
Hardware resources look alike in code but aren’t interchangeable:
- A 32-bit register and a 16-bit register are both “registers”
- A DMA buffer for read and a DMA buffer for write both look like
*mut u8 - An open file descriptor and a closed one are both
i32
In C:
// C — all registers look the same
uint32_t read_reg32(volatile void *base, uint32_t offset);
uint16_t read_reg16(volatile void *base, uint32_t offset);
// Bug: reading a 16-bit register with the 32-bit function
uint32_t status = read_reg32(pcie_bar, LINK_STATUS_REG); // should be reg16!
Phantom Type Parameters
A phantom type is a type parameter that appears in the struct definition but not in any field. It exists purely to carry type-level information:
use std::marker::PhantomData;
// Register width markers — zero-sized
pub struct Width8;
pub struct Width16;
pub struct Width32;
pub struct Width64;
/// A register handle parameterised by its width.
/// PhantomData<W> costs zero bytes — it's a compile-time-only marker.
pub struct Register<W> {
base: usize,
offset: usize,
_width: PhantomData<W>,
}
impl Register<Width8> {
pub fn read(&self) -> u8 {
// ... read 1 byte from base + offset ...
0 // stub
}
pub fn write(&self, _value: u8) {
// ... write 1 byte ...
}
}
impl Register<Width16> {
pub fn read(&self) -> u16 {
// ... read 2 bytes from base + offset ...
0 // stub
}
pub fn write(&self, _value: u16) {
// ... write 2 bytes ...
}
}
impl Register<Width32> {
pub fn read(&self) -> u32 {
// ... read 4 bytes from base + offset ...
0 // stub
}
pub fn write(&self, _value: u32) {
// ... write 4 bytes ...
}
}
/// PCIe config space register definitions.
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(); // returns u16 ✅
let bar: u32 = cfg.bar0().read(); // returns u32 ✅
// Can't mix them up:
// let bad: u32 = cfg.vendor_id().read(); // ❌ ERROR: expected u16
// cfg.bar0().write(0u16); // ❌ ERROR: expected u32
}
DMA Buffer Access Control
DMA buffers have direction: some are for device-to-host (read), others for host-to-device (write). Using the wrong direction corrupts data or causes bus errors:
use std::marker::PhantomData;
// Direction markers
pub struct ToDevice; // host writes, device reads
pub struct FromDevice; // device writes, host reads
/// A DMA buffer with direction enforcement.
pub struct DmaBuffer<Dir> {
ptr: *mut u8,
len: usize,
dma_addr: u64, // physical address for the device
_dir: PhantomData<Dir>,
}
impl DmaBuffer<ToDevice> {
/// Fill the buffer with data to send to the device.
pub fn write_data(&mut self, data: &[u8]) {
assert!(data.len() <= self.len);
// SAFETY: ptr is valid for self.len bytes (allocated at construction),
// and data.len() <= self.len (asserted above).
unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), self.ptr, data.len()) }
}
/// Get the DMA address for the device to read from.
pub fn device_addr(&self) -> u64 {
self.dma_addr
}
}
impl DmaBuffer<FromDevice> {
/// Read data that the device wrote into the buffer.
pub fn read_data(&self) -> &[u8] {
// SAFETY: ptr is valid for self.len bytes, and the device
// has finished writing (caller ensures DMA transfer is complete).
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
/// Get the DMA address for the device to write to.
pub fn device_addr(&self) -> u64 {
self.dma_addr
}
}
// Can't write to a FromDevice buffer:
// fn oops(buf: &mut DmaBuffer<FromDevice>) {
// buf.write_data(&[1, 2, 3]); // ❌ no method `write_data` on DmaBuffer<FromDevice>
// }
// Can't read from a ToDevice buffer:
// fn oops2(buf: &DmaBuffer<ToDevice>) {
// let data = buf.read_data(); // ❌ no method `read_data` on DmaBuffer<ToDevice>
// }
File Descriptor Ownership
A common bug: using a file descriptor after it’s been closed. Phantom types can track open/closed state:
use std::marker::PhantomData;
pub struct Open;
pub struct Closed;
/// A file descriptor with state tracking.
pub struct Fd<State> {
raw: i32,
_state: PhantomData<State>,
}
impl Fd<Open> {
pub fn open(path: &str) -> Result<Self, String> {
// ... open the file ...
Ok(Fd { raw: 3, _state: PhantomData }) // stub
}
pub fn read(&self, buf: &mut [u8]) -> Result<usize, String> {
// ... read from fd ...
Ok(0) // stub
}
pub fn write(&self, data: &[u8]) -> Result<usize, String> {
// ... write to fd ...
Ok(data.len()) // stub
}
/// Close the fd — returns a Closed handle.
/// The Open handle is consumed, preventing use-after-close.
pub fn close(self) -> Fd<Closed> {
// ... close the fd ...
Fd { raw: self.raw, _state: PhantomData }
}
}
impl Fd<Closed> {
// No read() or write() methods — they don't exist on Fd<Closed>.
// This makes use-after-close a compile error.
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)?; // ❌ no method `read` on Fd<Closed>
// closed.write(&[1])?; // ❌ no method `write` on Fd<Closed>
Ok(())
}
Combining Phantom Types with Earlier Patterns
Phantom types compose with everything we’ve seen:
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);
/// Combine phantom types (register width) with dimensional types (Celsius).
fn read_temp_sensor(reg: &Register<Width16>) -> Celsius {
let raw = reg.read(); // guaranteed u16 by phantom type
Celsius(raw as f64 * 0.0625) // guaranteed Celsius by return type
}
// The compiler enforces:
// 1. The register is 16-bit (phantom type)
// 2. The result is Celsius (newtype)
// Both at zero runtime cost.
When to Use Phantom Types
| Scenario | Use phantom parameter? |
|---|---|
| Register width encoding | ✅ Always — prevents width mismatch |
| DMA buffer direction | ✅ Always — prevents data corruption |
| File descriptor state | ✅ Always — prevents use-after-close |
| Memory region permissions (R/W/X) | ✅ Always — enforces access control |
| Generic container (Vec, HashMap) | ❌ No — use concrete type parameters |
| Runtime-variable attributes | ❌ No — phantom types are compile-time only |
Phantom Type Resource Matrix
flowchart TD
subgraph "Width Markers"
W8["Width8"]
W16["Width16"]
W32["Width32"]
end
subgraph "Direction Markers"
RD["Read"]
WR["Write"]
end
subgraph "Typed Resources"
R1["Register<Width16>"]
R2["DmaBuffer<Read>"]
R3["DmaBuffer<Write>"]
end
W16 --> R1
RD --> R2
WR --> R3
R2 -.->|"write attempt"| ERR["❌ Compile Error"]
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
Exercise: Memory Region Permissions
Design phantom types for memory regions with read, write, and execute permissions:
MemRegion<ReadOnly>hasfn read(&self, offset: usize) -> u8MemRegion<ReadWrite>has bothreadandwriteMemRegion<Executable>hasreadandfn execute(&self)- Writing to
ReadOnlyor executingReadWriteshould not compile.
Solution
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>,
}
// Read available on all permission types
impl<P> MemRegion<P> {
pub fn read(&self, offset: usize) -> u8 {
assert!(offset < self.len);
// SAFETY: offset < self.len (asserted above), base is valid for len bytes.
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 (asserted above), base is valid for len bytes,
// and &mut self ensures exclusive access.
unsafe { *self.base.add(offset) = val; }
}
}
impl MemRegion<Executable> {
pub fn execute(&self) {
// Jump to base address (conceptual)
}
}
// ❌ region_ro.write(0, 0xFF); // Compile error: no method `write`
// ❌ region_rw.execute(); // Compile error: no method `execute`
Key Takeaways
- PhantomData carries type-level information at zero size — the marker exists only for the compiler.
- Register width mismatches become compile errors —
Register<Width16>returnsu16, notu32. - DMA direction is enforced structurally —
DmaBuffer<Read>has nowrite()method. - Combine with dimensional types (ch06) —
Register<Width16>can returnCelsiusvia the parse step. - Phantom types are compile-time only — they don’t work for runtime-variable attributes; use enums for those.
Putting It All Together — A Complete Diagnostic Platform 🟡
What you’ll learn: How all seven core patterns (ch02–ch09) compose into a single diagnostic workflow — authentication, sessions, typed commands, audit tokens, dimensional results, validated data, and phantom-typed registers — with zero total runtime overhead.
Cross-references: Every core pattern chapter (ch02–ch09), ch14 (testing these guarantees)
Goal
This chapter combines seven patterns from chapters 2–9 into a single, realistic diagnostic workflow. We’ll build a server health check that:
- Authenticates (capability token — ch04)
- Opens an IPMI session (type-state — ch05)
- Sends typed commands (typed commands — ch02)
- Uses single-use tokens for audit logging (single-use types — ch03)
- Returns dimensional results (dimensional analysis — ch06)
- Validates FRU data (validated boundaries — ch07)
- Reads typed registers (phantom types — ch09)
use std::marker::PhantomData;
use std::io;
// ──── Pattern 1: Dimensional Types (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);
// ──── Pattern 2: Typed Commands (ch02) ────
/// Same trait shape as ch02, using methods (not associated constants)
/// for consistency. Associated constants (`const NETFN: u8`) are an
/// equally valid alternative when the value is truly fixed per type.
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; // ← dimensional type!
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))
}
}
// ──── Pattern 3: Capability Token (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")
}
}
// ──── Pattern 4: Type-State Session (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, // ← requires capability token
) -> Result<Session<Active>, String> {
println!("Session activated on {}", self.host);
Ok(Session { host: self.host, _state: PhantomData })
}
}
impl Session<Active> {
/// Execute a typed command — only available on Active sessions.
/// Returns io::Result to propagate transport errors (consistent with 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]) // stub: raw IPMI response
}
pub fn close(self) { println!("Session closed"); }
}
// ──── Pattern 5: Single-Use Audit Token (ch03) ────
/// Each diagnostic run gets a unique audit token.
/// Not Clone, not Copy — ensures each audit entry is unique.
pub struct AuditToken {
run_id: u64,
}
impl AuditToken {
pub fn issue(run_id: u64) -> Self {
AuditToken { run_id }
}
/// Consume the token to write an audit log entry.
pub fn log(self, message: &str) {
println!("[AUDIT run_id={}] {}", self.run_id, message);
// token is consumed — can't log the same run_id twice
}
}
// ──── Pattern 6: Validated Boundary (ch07) ────
// Simplified from ch07's full ValidFru — only the fields needed for this
// composite example. See ch07 for the complete TryFrom<RawFruData> version.
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 too short"); }
if raw[0] != 0x01 { return Err("bad FRU version"); }
Ok(ValidFru {
board_serial: "SN12345".to_string(), // stub
product_name: "ServerX".to_string(),
})
}
}
// ──── Pattern 7: Phantom-Typed Registers (ch09) ────
pub struct Width16;
pub struct Reg<W> { offset: u16, _w: PhantomData<W> }
impl Reg<Width16> {
pub fn read(&self) -> u16 { 0x8086 } // stub
}
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 },
}
}
}
// ──── Composite Workflow ────
fn full_diagnostic() -> Result<(), String> {
// 1. Authenticate → get capability token
let admin = authenticate("admin", "secret")
.map_err(|e| e.to_string())?;
// 2. Connect and activate session (type-state: Idle → Active)
let session = Session::connect("192.168.1.100");
let mut session = session.activate(&admin)?; // requires AdminToken
// 3. Send typed commands (response type matches command)
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())?;
// Type mismatch would be caught:
// let wrong: Volts = session.execute(&ReadTemp { sensor_id: 0 })?;
// ❌ ERROR: expected Celsius, found Volts
// 4. Read phantom-typed PCIe registers
let pcie = PcieDev::new();
let vid: u16 = pcie.vendor_id.read(); // guaranteed u16
// 5. Validate FRU data at the boundary
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. Issue single-use audit token
let audit = AuditToken::issue(1001);
// 7. Generate report (all data is typed and validated)
let report = format!(
"Server: {} (SN: {}), VID: 0x{:04X}, CPU: {:?}, Fan: {:?}",
fru.product_name, fru.board_serial, vid, temp, fan,
);
// 8. Consume audit token — can't log twice
audit.log(&report);
// audit.log("oops"); // ❌ use of moved value
// 9. Close session (type-state: Active → dropped)
session.close();
Ok(())
}
What the Compiler Proves
| Bug class | How it’s prevented | Pattern |
|---|---|---|
| Unauthenticated access | activate() requires &AdminToken | Capability token |
| Command in wrong session state | execute() only exists on Session<Active> | Type-state |
| Wrong response type | ReadTemp::Response = Celsius, fixed by trait | Typed commands |
| Unit confusion (°C vs RPM) | Celsius ≠ Rpm ≠ Volts | Dimensional types |
| Register width mismatch | Reg<Width16> returns u16 | Phantom types |
| Processing unvalidated data | Must call ValidFru::parse() first | Validated boundary |
| Duplicate audit entries | AuditToken is consumed on log | Single-use type |
| Out-of-order power sequencing | Each step requires previous token | Capability tokens (ch04) |
Total runtime overhead of ALL these guarantees: zero.
Every check happens at compile time. The generated assembly is identical to hand-written C code with no checks at all — but C can have bugs, this can’t.
Key Takeaways
- Seven patterns compose seamlessly — capability tokens, type-state, typed commands, single-use types, dimensional types, validated boundaries, and phantom types all work together.
- The compiler proves eight bug classes impossible — see the “What the Compiler Proves” table above.
- Zero total runtime overhead — the generated assembly is identical to unchecked C code.
- Each pattern is independently useful — you don’t need all seven; adopt them incrementally.
- The integration chapter is a design template — use it as a starting point for your own typed diagnostic workflows.
- From IPMI to Redfish at scale — ch17 and ch18 apply these same seven patterns (plus capability mixins from ch08) to a full Redfish client and server. The IPMI workflow here is the foundation; the Redfish walkthroughs show how the composition scales to production systems with multiple data sources and schema-version constraints.
Fourteen Tricks from the Trenches 🟡
What you’ll learn: Fourteen smaller correct-by-construction techniques — from sentinel elimination and sealed traits to session types,
Pin, RAII, and#[must_use]— each eliminating a specific bug class for near-zero effort.Cross-references: ch02 (sealed traits extend ch02), ch05 (typestate builder extends ch05), ch07 (FromStr extends ch07)
Fourteen Tricks from the Trenches
The eight core patterns (ch02–ch09) cover the major correct-by-construction techniques. This chapter collects fourteen smaller but high-value tricks that show up repeatedly in production Rust code — each one eliminates a specific class of bug for zero or near-zero effort.
Trick 1 — Sentinel → Option at the Boundary
Hardware protocols are full of sentinel values: IPMI uses 0xFF for
“sensor not present,” PCI uses 0xFFFF for “no device,” and SMBIOS uses
0x00 for “unknown.” If you carry these sentinels through your code as
plain integers, every consumer must remember to check for the magic value.
If even one comparison forgets, you get a phantom 255 °C reading or a
spurious vendor-ID match.
The rule: Convert sentinels to Option at the very first parse boundary,
and convert back to the sentinel only at the serialization boundary.
The anti-pattern (from pcie_tree/src/lspci.rs)
// Sentinel carried internally — every comparison must remember
let mut current_vendor_id: u16 = 0xFFFF;
let mut current_device_id: u16 = 0xFFFF;
// ... later, parsing fails silently ...
current_vendor_id = u16::from_str_radix(hex, 16)
.unwrap_or(0xFFFF); // sentinel hides the error
Every function that receives current_vendor_id must know that 0xFFFF is
special. If someone writes if vendor_id == target_id without checking
for 0xFFFF first, a missing device silently matches when the target also
happens to be parsed from bad input as 0xFFFF.
The correct pattern (from nic_sel/src/events.rs)
pub struct ThermalEvent {
pub record_id: u16,
pub temperature: Option<u8>, // None if sensor reports 0xFF
}
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
},
}
}
}
Now every consumer must handle the None case — the compiler forces it:
// Safe — compiler ensures we handle missing temps
fn is_overtemp(temp: Option<u8>, threshold: u8) -> bool {
temp.map_or(false, |t| t > threshold)
}
// Forgetting to handle None is a compile error:
// fn bad_check(temp: Option<u8>, threshold: u8) -> bool {
// temp > threshold // ERROR: can't compare Option<u8> with u8
// }
Real-world impact
inventory/src/events.rs uses the same pattern for GPU thermal alerts:
temperature: if data[1] != 0xFF {
Some(data[1] as i8)
} else {
None
},
The refactoring for pcie_tree/src/lspci.rs is straightforward: change
current_vendor_id: u16 to current_vendor_id: Option<u16>, replace
0xFFFF with None, and let the compiler find every site that needs
updating.
| Before | After |
|---|---|
let mut vendor_id: u16 = 0xFFFF | let mut vendor_id: Option<u16> = None |
.unwrap_or(0xFFFF) | .ok() (already returns Option) |
if vendor_id != 0xFFFF { ... } | if let Some(vid) = vendor_id { ... } |
Serialization: vendor_id | vendor_id.unwrap_or(0xFFFF) |
Trick 2 — Sealed Traits
Chapter 2 introduced IpmiCmd with an associated type that binds each command
to its response. But there’s a loophole: if any code can implement IpmiCmd,
someone could write a MaliciousCmd whose parse_response returns the wrong
type or panics. The type safety of the entire system rests on every
implementation being correct.
A sealed trait closes this loophole. The idea is simple: make the trait require a private supertrait that only your crate can implement.
// — Private module: not exported from the crate —
mod private {
pub trait Sealed {}
}
// — Public trait: requires Sealed, which outsiders can't implement —
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>;
}
Inside your crate, you implement Sealed for each approved command type:
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))
}
}
External code sees IpmiCmd and can call execute(), but cannot implement it:
// In another crate:
struct EvilCmd;
// impl private::Sealed for EvilCmd {} // ERROR: module `private` is private
// impl IpmiCmd for EvilCmd { ... } // ERROR: `Sealed` is not satisfied
When to seal
| Seal when… | Don’t seal when… |
|---|---|
| Safety depends on correct implementation (IpmiCmd, DiagModule) | Users should extend the system (custom report formatters) |
| Associated types must satisfy invariants | The trait is a simple capability marker (HasIpmi) |
| You own the canonical set of implementations | Third-party plugins are a design goal |
Real-world candidates
IpmiCmd— incorrect parse could corrupt typed responsesDiagModule— framework assumesrun()returns valid DER recordsSelEventFilter— broken filter could swallow critical SEL events
Trick 3 — #[non_exhaustive] for Evolving Enums
SkuVariant in inventory/src/types.rs today has five variants:
pub enum SkuVariant {
S1001, S2001, S2002, S2003, S3001,
}
When the next generation ships and you add S4001, any external code that
matches on SkuVariant and doesn’t have a wildcard arm will silently fail
to compile — which is the whole point. But what about internal code? Without
#[non_exhaustive], your match in the same crate compiles without a
wildcard, and adding the new variant breaks your own build.
Marking the enum #[non_exhaustive] forces external crates that match on
it to include a wildcard arm. Within the defining crate, #[non_exhaustive]
has no effect — you can still write exhaustive matches.
Why this is useful: When you publish SkuVariant from a library crate
(or a shared sub-crate in a workspace), downstream code is forced to handle
unknown future variants. When you add S4001 next generation, downstream
code already compiles — they have a wildcard arm.
// In gpu_sel crate (the defining crate):
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SkuVariant {
S1001,
S2001,
S2002,
S2003,
S3001,
// When the next SKU ships, add it here.
// External consumers already have a wildcard — zero breakage for them.
}
// Within gpu_sel itself — exhaustive match is allowed (no wildcard needed):
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",
// No wildcard needed inside the defining crate.
// Adding S4001 here will cause a compile error at this match,
// which is exactly what you want — it forces you to update it.
}
}
// In the binary crate (a downstream crate that depends on inventory):
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", // REQUIRED by #[non_exhaustive] for external crates
}
}
Workspace tip: If all your code is in a single crate,
#[non_exhaustive]won’t help — it only affects cross-crate boundaries. For the project’s large workspace, place evolving enums in a shared crate (core_liborinventory) so the attribute protects consumers in other workspace crates.
Candidates
| Enum | Module | Why |
|---|---|---|
SkuVariant | inventory, net_inventory | New SKUs every generation |
SensorType | protocol_lib | IPMI spec reserves 0xC0–0xFF for OEM |
CompletionCode | protocol_lib | Custom BMC vendors add codes |
Component | event_handler | New hardware categories (NewSoC was recently added) |
Trick 4 — Typestate Builder
Chapter 5 showed type-state for protocols (session lifecycles, link training).
The same idea applies to builders — structs whose build() / finish()
can only be called when all required fields have been set.
The problem with fluent builders
DerBuilder in diag_framework/src/der.rs today looks like this (simplified):
// Current fluent builder — finish() always available
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 } // ← always callable!
}
This compiles without error, but produces an incomplete DER record:
let bad = DerBuilder::new("CSI_ERR", 62691)
.finish(); // oops — no mnemonic, no fault_class
Typestate builder: finish() requires both fields
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>,
}
// Constructor: starts with both required fields 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,
}
}
}
// Set mnemonic (works regardless of fault_class's state)
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,
}
}
}
// Set fault_class (works regardless of mnemonic's state)
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,
}
}
}
// Optional fields — available in ANY state
impl<MN, FC> DerBuilder<MN, FC> {
pub fn description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
}
/// The fully-built DER record.
pub struct Der {
pub marker: String,
pub fault_code: u32,
pub mnemonic: String,
pub fault_class: String,
pub description: Option<String>,
}
// finish() ONLY available when both required fields are 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,
}
}
}
Now the buggy call is a compile error:
// ✅ Compiles — both required fields set (in any order)
let der = DerBuilder::new("CSI_ERR", 62691)
.fault_class("GPU Module") // order doesn't matter
.mnemonic("ACCEL_CARD_ER691")
.description("Thermal throttle")
.finish();
// ❌ Compile error — finish() doesn't exist on DerBuilder<Set<String>, Missing>
let bad = DerBuilder::new("CSI_ERR", 62691)
.mnemonic("ACCEL_CARD_ER691")
.finish(); // ERROR: method `finish` not found
When to use typestate builders
| Use when… | Don’t bother when… |
|---|---|
| Omitting a field causes silent bugs (DER missing mnemonic) | All fields have sensible defaults |
| The builder is part of a public API | The builder is test-only scaffolding |
| More than 2–3 required fields | Single required field (just take it in new()) |
Trick 5 — FromStr as a Validation Boundary
Chapter 7 showed TryFrom<&[u8]> for binary data (FRU records, SEL entries).
For string inputs — config files, CLI arguments, JSON fields — the
analogous boundary is FromStr.
The problem
// C++ / unvalidated Rust: silently falls through to a default
fn route_diag(level: &str) -> DiagMode {
if level == "quick" { ... }
else if level == "standard" { ... }
else { QuickMode } // typo in config? ¯\_(ツ)_/¯
}
A config file with "diag_level": "extendedd" (typo) silently gets QuickMode.
The pattern (from 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}'")),
}
}
}
Now a typo is caught immediately:
let level: DiagLevel = "extendedd".parse()?;
// Err("unknown diag level: 'extendedd'")
The three benefits
- Fail-fast: Bad input is caught at the parsing boundary, not three layers deep in diagnostic logic.
- Aliases are explicit:
"MEM","DIMM", and"MEMORY"all map toComponent::Memory— the match arms document the mapping. .parse()is ergonomic: BecauseFromStrintegrates withstr::parse(), you get clean one-liners:let level: DiagLevel = config["level"].parse()?;
Real codebase usage
The project already has 8 FromStr implementations:
| Type | Module | Notable aliases |
|---|---|---|
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 | Same aliases (separate module, same pattern) |
FaultStatus | config_loader | Fault lifecycle states |
DiagAction | config_loader | Remediation action types |
ActionType | config_loader | Action categories |
DiagMode | cluster_diag | Multi-node test modes |
The contrast with TryFrom:
TryFrom<&[u8]> | FromStr | |
|---|---|---|
| Input | Raw bytes (binary protocols) | Strings (configs, CLI, JSON) |
| Typical source | IPMI, PCIe config space, FRU | JSON fields, env vars, user input |
| Chapter | ch07 | ch11 |
| Both use | Result — forcing the caller to handle invalid input |
Trick 6 — Const Generics for Compile-Time Size Validation
When hardware buffers, register banks, or protocol frames have fixed sizes, const generics let the compiler enforce them:
/// A fixed-size register bank. The size is part of the type.
/// `RegisterBank<256>` and `RegisterBank<4096>` are different types.
pub struct RegisterBank<const N: usize> {
data: [u8; N],
}
impl<const N: usize> RegisterBank<N> {
/// Read a register at the given offset.
/// Compile-time: N is known, so the array size is fixed.
/// Runtime: only the offset is checked.
pub fn read(&self, offset: usize) -> Option<u8> {
self.data.get(offset).copied()
}
}
// PCIe conventional config space: 256 bytes
type PciConfigSpace = RegisterBank<256>;
// PCIe extended config space: 4096 bytes
type PcieExtConfigSpace = RegisterBank<4096>;
// These are different types — can't accidentally pass one for the other:
fn read_extended_cap(config: &PcieExtConfigSpace, offset: usize) -> Option<u8> {
config.read(offset)
}
// read_extended_cap(&pci_config, 0x100);
// ^^^^^^^^^^^ expected RegisterBank<4096>, found RegisterBank<256> ❌
Compile-time assertions with const generics:
/// NVMe admin commands use 4096-byte buffers. Enforce at compile time.
pub struct NvmeBuffer<const N: usize> {
data: Box<[u8; N]>,
}
impl<const N: usize> NvmeBuffer<N> {
pub fn new() -> Self {
// Runtime assertion: only 512 or 4096 allowed
assert!(N == 4096 || N == 512, "NVMe buffers must be 512 or 4096 bytes");
NvmeBuffer { data: Box::new([0u8; N]) }
}
}
// NvmeBuffer::<1024>::new(); // panics at runtime with this form
// For true compile-time enforcement, see Trick 9 (const assertions).
When to use: Fixed-size protocol buffers (NVMe, PCIe config space), DMA descriptors, hardware FIFO depths. Anywhere the size is a hardware constant that should never vary at runtime.
Trick 7 — Safe Wrappers Around unsafe
The project currently has zero unsafe blocks. But when you
add MMIO register access, DMA, or FFI to accel-mgmt/accel-query, you’ll need
unsafe. The correct-by-construction approach: wrap every unsafe block
in a safe abstraction so the unsafety is contained and auditable.
/// MMIO-mapped register. The pointer is valid for the lifetime of the mapping.
/// All unsafe is contained in this module — callers use safe methods.
pub struct MmioRegion {
base: *mut u8,
len: usize,
}
impl MmioRegion {
/// # Safety
/// - `base` must be a valid pointer to an MMIO-mapped region
/// - The region must remain mapped for the lifetime of this struct
/// - No other code may alias this region
pub unsafe fn new(base: *mut u8, len: usize) -> Self {
MmioRegion { base, len }
}
/// Safe read — bounds checking prevents out-of-bounds MMIO access.
pub fn read_u32(&self, offset: usize) -> Option<u32> {
if offset + 4 > self.len { return None; }
// SAFETY: offset is bounds-checked above, base is valid per new() contract
Some(unsafe {
core::ptr::read_volatile(self.base.add(offset) as *const u32)
})
}
/// Safe write — bounds checking prevents out-of-bounds MMIO access.
pub fn write_u32(&self, offset: usize, value: u32) -> bool {
if offset + 4 > self.len { return false; }
// SAFETY: offset is bounds-checked above, base is valid per new() contract
unsafe {
core::ptr::write_volatile(self.base.add(offset) as *mut u32, value);
}
true
}
}
Combine with phantom types (ch09) for typed 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)
}
// No write method — compile error if you try to write to a ReadOnly region
}
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)
}
}
Guidelines for
unsafewrappers:
Rule Why One unsafe fn new()with documented# SafetyinvariantsCaller takes responsibility once All other methods are safe Callers can’t trigger UB # SAFETY:comment on everyunsafeblockAuditors can verify locally Wrap in a module with #[deny(unsafe_op_in_unsafe_fn)]Even inside unsafe fn, individual ops needunsafeRun cargo +nightly miri teston the wrapperVerify memory model compliance
✅ Checkpoint: Tricks 1–7
You now have seven everyday tricks. Here’s a quick scorecard:
| Trick | Bug class eliminated | Effort to adopt |
|---|---|---|
| 1 | Sentinel confusion (0xFF) | Low — one match at the boundary |
| 2 | Unauthorized trait impls | Low — add Sealed supertrait |
| 3 | Broken consumers after enum growth | Low — one-line attribute |
| 4 | Missing builder fields | Medium — extra type parameters |
| 5 | Typos in string-typed config | Low — impl FromStr |
| 6 | Wrong buffer sizes | Low — const generic parameter |
| 7 | Unsafe scattered across codebase | Medium — wrapper module |
Tricks 8–14 are more advanced — they touch async, const evaluation, session
types, Pin, and Drop. Take a break here if you need one; the techniques
above are already high-value, low-effort wins you can adopt tomorrow.
Trick 8 — Async Type-State Machines
When hardware drivers use async (e.g., async BMC communication, async NVMe
I/O), type-state still works — but ownership across .await points needs care:
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 }
}
/// Transition Idle → Authenticating → Active.
/// The Session is consumed (moved into the future) across the .await.
pub async fn authenticate(self, user: &str, pass: &str)
-> Result<AsyncSession<Active>, String>
{
// Phase 1: send credentials (consumes Idle session)
let pending: AsyncSession<Authenticating> = AsyncSession {
host: self.host,
_state: PhantomData,
};
// Simulate async BMC authentication
// tokio::time::sleep(Duration::from_secs(1)).await;
// Phase 2: return 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 here...
vec![0x00]
}
}
// Usage:
// let session = AsyncSession::new("192.168.1.100");
// let mut session = session.authenticate("admin", "pass").await?;
// let resp = session.send_command(&[0x04, 0x2D]).await;
Key rules for async type-state:
| Rule | Why |
|---|---|
Transition methods take self (by value), not &mut self | Ownership transfer works across .await |
Return Result<NextState, (Error, PrevState)> for recoverable errors | Caller can retry from the previous state |
| Don’t split state across multiple futures | One future owns one session |
Use Send + 'static bounds if using tokio::spawn | The session must be movable across threads |
Caveat: If you need the previous state back on error (to retry), return
Result<AsyncSession<Active>, (Error, AsyncSession<Idle>)>so the caller gets ownership back. Without this, a failed.awaitdrops the session permanently.
Trick 9 — Refinement Types via Const Assertions
When a numeric constraint is a compile-time invariant (not runtime data),
use const evaluation to enforce it. This differs from Trick 6 (which
provides type-level size distinctions) — here we reject invalid values
at compile time:
/// A sensor ID that must be in the IPMI SDR range (0x01..=0xFE).
/// The constraint is checked at compile time when `N` is const.
pub struct SdrSensorId<const N: u8>;
impl<const N: u8> SdrSensorId<N> {
/// Compile-time validation: panics during compilation if N is out of range.
pub const fn validate() {
assert!(N >= 0x01, "Sensor ID must be >= 0x01");
assert!(N <= 0xFE, "Sensor ID must be <= 0xFE (0xFF is reserved)");
}
pub const VALIDATED: () = Self::validate();
pub const fn value() -> u8 { N }
}
// Usage:
fn read_sensor_const<const N: u8>() -> f64 {
let _ = SdrSensorId::<N>::VALIDATED; // compile-time check
// read sensor N...
42.0
}
// read_sensor_const::<0x20>(); // ✅ compiles — 0x20 is valid
// read_sensor_const::<0x00>(); // ❌ compile error — "Sensor ID must be >= 0x01"
// read_sensor_const::<0xFF>(); // ❌ compile error — 0xFF is reserved
Simpler form — bounded fan IDs:
pub struct BoundedFanId<const N: u8>;
impl<const N: u8> BoundedFanId<N> {
pub const VALIDATED: () = assert!(N < 8, "Server has at most 8 fans (0..7)");
pub const fn id() -> u8 {
let _ = Self::VALIDATED;
N
}
}
// BoundedFanId::<3>::id(); // ✅
// BoundedFanId::<10>::id(); // ❌ compile error
When to use: Hardware-defined fixed IDs (sensor IDs, fan slots, PCIe slot numbers) known at compile time. When the value comes from runtime data (config file, user input), use
TryFrom/FromStr(ch07, Trick 5) instead.
Trick 10 — Session Types for Channel Communication
When two components communicate over a channel (e.g., diagnostic orchestrator ↔ worker thread), session types encode the protocol in the type system:
use std::marker::PhantomData;
// Protocol: Client sends Request, Server sends Response, then done.
pub struct SendRequest;
pub struct RecvResponse;
pub struct Done;
/// A typed channel endpoint. `S` is the current protocol state.
pub struct Chan<S> {
// In real code: wraps a mpsc::Sender/Receiver pair
_state: PhantomData<S>,
}
impl Chan<SendRequest> {
/// Send a request — transitions to RecvResponse state.
pub fn send(self, request: DiagRequest) -> Chan<RecvResponse> {
// ... send on channel ...
Chan { _state: PhantomData }
}
}
impl Chan<RecvResponse> {
/// Receive a response — transitions to Done state.
pub fn recv(self) -> (DiagResponse, Chan<Done>) {
// ... recv from channel ...
(DiagResponse { passed: true }, Chan { _state: PhantomData })
}
}
impl Chan<Done> {
/// Closing the channel — only possible when the protocol is complete.
pub fn close(self) { /* drop */ }
}
pub struct DiagRequest { pub test_name: String }
pub struct DiagResponse { pub passed: bool }
// The protocol MUST be followed in order:
fn orchestrator(chan: Chan<SendRequest>) {
let chan = chan.send(DiagRequest { test_name: "gpu_stress".into() });
let (response, chan) = chan.recv();
chan.close();
println!("Result: {}", if response.passed { "PASS" } else { "FAIL" });
}
// Can't recv before send:
// fn wrong_order(chan: Chan<SendRequest>) {
// chan.recv(); // ❌ no method `recv` on Chan<SendRequest>
// }
When to use: Inter-thread diagnostic protocols, BMC command sequences, any request-response pattern where order matters. For complex multi-message protocols, consider the
session-typesorrumpsteakcrates.
Trick 11 — Pin for Self-Referential State Machines
Some type-state machines need to hold references into their own data (e.g., a
parser that tracks a position within its owned buffer). Rust normally forbids
this because moving the struct would invalidate the internal pointer. Pin<T>
solves this by guaranteeing the value will not be moved:
use std::pin::Pin;
use std::marker::PhantomPinned;
/// A streaming parser that holds a reference into its own buffer.
/// Once pinned, it cannot be moved — the internal reference stays valid.
pub struct StreamParser {
buffer: Vec<u8>,
/// Points into `buffer`. Only valid while pinned.
cursor: *const u8,
_pin: PhantomPinned, // opts out of Unpin — prevents accidental unpinning
}
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);
// Set cursor to point into the pinned buffer
let cursor = boxed.buffer.as_ptr();
// SAFETY: we have exclusive access and the parser is pinned
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).cursor = cursor;
}
boxed
}
/// Read the next byte — only callable through Pin<&mut Self>.
pub fn next_byte(self: Pin<&mut Self>) -> Option<u8> {
// The parser can't be moved, so cursor remains valid
if self.cursor.is_null() { return None; }
// ... advance cursor through buffer ...
Some(42) // stub
}
}
// Usage:
// let mut parser = StreamParser::new(vec![0x01, 0x02, 0x03]);
// let byte = parser.as_mut().next_byte();
Key insight: Pin is the correct-by-construction solution to the
self-referential struct problem. Without it, you’d need unsafe and manual
lifetime tracking. With it, the compiler prevents moves and the internal
pointer invariant is maintained.
Use Pin when… | Don’t use Pin when… |
|---|---|
| State machine holds intra-struct references | All fields are independently owned |
Async futures that borrow across .await | No self-referencing needed |
| DMA descriptors that must not relocate in memory | Data can be freely moved |
| Hardware ring buffers with internal cursor | Simple index-based iteration works |
Trick 12 — RAII / Drop as a Correctness Guarantee
Rust’s Drop trait is a correct-by-construction mechanism: cleanup code cannot
be forgotten because the compiler inserts it automatically. This is especially
valuable for hardware resources that must be released exactly once.
use std::io;
/// An IPMI session that MUST be closed when done.
/// The `Drop` impl guarantees cleanup even on panic or early `?` return.
pub struct IpmiSession {
handle: u32,
}
impl IpmiSession {
pub fn open(host: &str) -> io::Result<Self> {
// ... negotiate IPMI session ...
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 command: always runs, even on panic/early-return.
// In C, forgetting CloseSession() leaks a BMC session slot.
let _ = self.send_raw(&[0x06, 0x3C]);
eprintln!("[RAII] session {} closed", self.handle);
}
}
// Usage:
fn diagnose(host: &str) -> io::Result<()> {
let session = IpmiSession::open(host)?;
session.send_raw(&[0x04, 0x2D, 0x20])?;
// No explicit close needed — Drop runs here automatically
Ok(())
// Even if send_raw returns Err(...), the session is still closed.
}
The C/C++ failure mode that RAII eliminates:
C: session = ipmi_open(host);
ipmi_send(session, data);
if (error) return -1; // 🐛 leaked session — forgot close()
ipmi_close(session);
Rust: let session = IpmiSession::open(host)?;
session.send_raw(data)?; // ✅ Drop runs on ? return
// Drop always runs — leak is impossible
Combine RAII with type-state (ch05) for ordered cleanup:
You cannot specialize Drop on a generic parameter (Rust error E0366).
Instead, use separate wrapper types per state:
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 {
// ... lock GPU clocks for stable benchmarking ...
LockedGpu { device_id: self.device_id }
}
}
/// Separate type for the locked state — has its own Drop.
/// We can't do `impl Drop for GpuContext<Locked>` (E0366),
/// so we use a distinct wrapper that owns the locked resource.
pub struct LockedGpu {
device_id: u32,
}
impl LockedGpu {
pub fn run_benchmark(&self) -> f64 {
// ... benchmark with locked clocks ...
42.0
}
}
impl Drop for LockedGpu {
fn drop(&mut self) {
// Unlock clocks on drop — only fires for the locked wrapper.
eprintln!("[RAII] GPU {} clocks unlocked", self.device_id);
}
}
// GpuContext<Open> has no special Drop — no clocks to unlock.
// LockedGpu always unlocks on drop, even on panic or early return.
Why not
impl Drop for GpuContext<Locked>? Rust requiresDropimpls to apply to all instantiations of a generic type. To get state-specific cleanup, use one of:
Approach Pros Cons Separate wrapper type (above) Clean, zero-cost Extra type name Generic Drop+ runtimeTypeIdcheckSingle type Requires 'static, runtime costenumstate with exhaustive match inDropSingle generic type Runtime dispatch, less type safety
When to use: BMC sessions, GPU clock locks, DMA buffer mappings, file handles, mutex guards, any resource with a mandatory release step. If you find yourself writing
fn close(&mut self)orfn cleanup(), it should almost certainly beDropinstead.
Trick 13 — Error Type Hierarchies as Correctness
Well-designed error types prevent silent error swallowing and ensure callers
handle each failure mode appropriately. Using thiserror for structured errors
is a correct-by-construction pattern: the compiler forces exhaustive matching.
# Cargo.toml
[dependencies]
thiserror = "1"
# For application-level error handling (optional):
# anyhow = "1"
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DiagError {
#[error("IPMI communication failed: {0}")]
Ipmi(#[from] IpmiError),
#[error("sensor {sensor_id:#04x} reading out of range: {value}")]
SensorRange { sensor_id: u8, value: f64 },
#[error("GPU {gpu_id} not responding")]
GpuTimeout { gpu_id: u32 },
#[error("configuration invalid: {0}")]
Config(String),
}
#[derive(Debug, Error)]
pub enum IpmiError {
#[error("session authentication failed")]
AuthFailed,
#[error("command {net_fn:#04x}/{cmd:#04x} timed out")]
Timeout { net_fn: u8, cmd: u8 },
#[error("completion code {0:#04x}")]
CompletionCode(u8),
}
// Callers MUST handle each variant — no silent swallowing:
fn run_thermal_check() -> Result<(), DiagError> {
// If this returns IpmiError, it's automatically converted to DiagError::Ipmi
// via the #[from] attribute.
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) }
Why this is correct-by-construction:
| Without structured errors | With thiserror enums |
|---|---|
fn op() -> Result<T, String> | fn op() -> Result<T, DiagError> |
| Caller gets opaque string | Caller matches on specific variants |
| Can’t distinguish auth failure from timeout | DiagError::Ipmi(IpmiError::AuthFailed) vs Timeout |
| Logging swallows the error | match forces handling each case |
| New error variant → nobody notices | New variant → compiler warns unmatched arms |
The anyhow vs thiserror decision:
Use thiserror when… | Use anyhow when… |
|---|---|
| Writing a library/crate | Writing a binary/CLI |
| Callers need to match on error variants | Callers just log and exit |
| Error types are part of the public API | Internal error plumbing |
protocol_lib, accel_diag, thermal_diag | diag_tool main binary |
When to use: Every crate in the workspace should define its own error enum with
thiserror. The top-level binary crate can useanyhowto aggregate them. This gives library callers compile-time error handling guarantees while keeping the binary ergonomic.
Trick 14 — #[must_use] for Enforcing Consumption
The #[must_use] attribute turns ignored return values into compiler warnings.
This is a lightweight correct-by-construction tool that pairs with every pattern
in this guide:
/// A calibration token that MUST be used — dropping it silently is a bug.
#[must_use = "calibration token must be passed to calibrate(), not dropped"]
pub struct CalibrationToken {
_private: (),
}
/// A diagnostic result that MUST be checked — ignoring failures is a bug.
#[must_use = "diagnostic result must be inspected for failures"]
pub struct DiagResult {
pub passed: bool,
pub details: String,
}
/// Functions that return important values should be marked too:
#[must_use = "the authenticated session must be used or explicitly closed"]
pub fn authenticate(user: &str, pass: &str) -> Result<Session, AuthError> {
// ...
unimplemented!()
}
pub struct Session;
pub struct AuthError;
What the compiler tells you:
warning: unused `CalibrationToken` that must be used
--> src/main.rs:5:5
|
5 | CalibrationToken { _private: () };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: calibration token must be passed to calibrate(), not dropped
Apply #[must_use] to these patterns:
| Pattern | What to annotate | Why |
|---|---|---|
| Single-Use Tokens (ch03) | CalibrationToken, FusePayload | Dropping without use = logic bug |
| Capability Tokens (ch04) | AdminToken | Authenticating but ignoring the token |
| Type-State transitions | Return type of authenticate(), activate() | Session created but never used |
| Results | DiagResult, SensorReading | Silent failure swallowing |
| RAII handles (Trick 12) | IpmiSession, LockedGpu | Opening but not using a resource |
Rule of thumb: If dropping a value without using it is always a bug, add
#[must_use]. If it’s sometimes intentional (e.g., aVec), don’t. The_prefix (let _ = foo()) explicitly acknowledges and silences the warning — this is fine when the drop is intentional.
Key Takeaways
- Sentinel → Option at the boundary — convert magic values to
Optionon parse; the compiler forces callers to handleNone. - Sealed traits close the implementation loophole — private supertrait means only your crate can implement the trait.
#[non_exhaustive]+#[must_use]are one-line, high-value annotations — add them to evolving enums and consumed tokens.- Typestate builders enforce required fields —
finish()only exists when all required type parameters areSet. - Each trick targets a specific bug class — adopt them incrementally; no trick requires rewriting your architecture.
Exercises 🟡
What you’ll learn: Hands-on practice applying correct-by-construction patterns to realistic hardware scenarios — NVMe admin commands, firmware update state machines, sensor pipelines, PCIe phantom types, multi-protocol health checks, and session-typed diagnostic protocols.
Cross-references: ch02 (exercise 1), ch05 (exercise 2), ch06 (exercise 3), ch09 (exercise 4), ch10 (exercise 5)
Practice Problems
Exercise 1: NVMe Admin Command (Typed Commands)
Design a typed command interface for NVMe admin commands:
Identify→IdentifyResponse(model number, serial, firmware rev)GetLogPage→SmartLog(temperature, available spare, data units read)GetFeature→ feature-specific response
Requirements:
- The command type determines the response type
- No runtime dispatch — static dispatch only
- Add a
NamespaceIdnewtype that prevents mixing namespace IDs with otheru32s
Hint: Follow the IpmiCmd trait pattern from ch02, but use NVMe-specific constants.
Sample Solution (Exercise 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,
}
/// The core pattern: associated type pins each command's response.
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, "too short"));
}
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, "too short"));
}
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, "too short"));
}
Ok(ArbitrationFeature {
high_priority_weight: raw[0],
medium_priority_weight: raw[1],
low_priority_weight: raw[2],
})
}
}
/// Static dispatch — the compiler monomorphises per command type.
pub struct NvmeController;
impl NvmeController {
pub fn execute<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
// Build SQE from cmd.opcode()/cmd.nsid(),
// submit to SQ, wait for CQ, then:
let raw = self.submit_and_read(cmd.opcode())?;
cmd.parse_response(&raw)
}
fn submit_and_read(&self, _opcode: u8) -> io::Result<Vec<u8>> {
// Real implementation talks to /dev/nvme0
Ok(vec![0; 512])
}
}
Key points:
NamespaceId(u32)prevents mixing namespace IDs with arbitraryu32values.NvmeAdminCmd::Responseis the “type index” —execute()returns exactlyC::Response.- Fully static dispatch: no
Box<dyn …>, no runtime downcasting.
Exercise 2: Firmware Update State Machine (Type-State)
Model a BMC firmware update lifecycle:
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 : No abort() — irreversible
note right of Verifying : VerifiedImage is a proof token
Requirements:
- Each state is a distinct type
- Upload can only begin from Idle
- Verification requires upload to be complete
- Apply can only happen after successful verification — take a
VerifiedImageproof token - Reboot is the only option after applying
- Add an
abort()method available in Uploading and Verifying (but not Applying — too late)
Hint: Combine type-state (ch05) with capability tokens (ch04).
Sample Solution (Exercise 2)
// --- State types ---
// Design choice: here we store state inline (`_state: S`) rather than using
// `PhantomData<S>` (ch05's approach). This lets states carry data —
// e.g., `Uploading { bytes_sent: usize }` tracks progress. Use `PhantomData`
// when states are pure markers (zero-sized); use inline storage when
// states carry meaningful runtime data.
pub struct Idle;
pub struct Uploading { bytes_sent: usize } // not ZST — carries progress data
pub struct Verifying;
pub struct Applying;
pub struct Rebooting;
pub struct Complete;
/// Proof token: only constructed inside 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 available during upload — returns to Idle.
pub fn abort(self) -> FwUpdate<Idle> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
}
}
impl FwUpdate<Verifying> {
/// On success, returns the next state AND a VerifiedImage proof token.
pub fn verify(self) -> Result<(FwUpdate<Applying>, VerifiedImage), FwUpdate<Idle>> {
// Real: check CRC, signature, compatibility
let token = VerifiedImage { _private: () };
Ok((
FwUpdate { bmc_addr: self.bmc_addr, _state: Applying },
token,
))
}
/// Abort available during verification.
pub fn abort(self) -> FwUpdate<Idle> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
}
}
impl FwUpdate<Applying> {
/// Consumes the VerifiedImage proof — can't apply without verification.
/// Note: NO abort() method here — once flashing starts, it's too dangerous.
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" }
}
// Usage:
// 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!("New version: {}", fw.version());
Key points:
abort()exists only onFwUpdate<Uploading>andFwUpdate<Verifying>— calling it onFwUpdate<Applying>is a compile error, not a runtime check.VerifiedImagehas a private field, so onlyverify()can create one.apply()consumes the proof token — you can’t skip verification.
Exercise 3: Sensor Reading Pipeline (Dimensional Analysis)
Build a complete sensor pipeline:
- Define newtypes:
RawAdc,Celsius,Fahrenheit,Volts,Millivolts,Watts - Implement
From<Celsius> for Fahrenheitand vice versa - Create
impl Mul<Volts, Output=Watts> for Amperes(P = V × I) - Build a
Threshold<T>generic checker - Write a pipeline: ADC → calibration → threshold check → result
The compiler should reject: comparing Celsius to Volts, adding Watts to Rpm,
passing Millivolts where Volts is expected.
Sample Solution (Exercise 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);
// --- Safe conversions ---
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) }
}
// --- Arithmetic on same-unit types ---
// NOTE: Adding absolute temperatures (25°C + 30°C) is physically
// questionable — see ch06's discussion of ΔT newtypes for a more
// rigorous approach. Here we keep it simple for the exercise.
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 (cross-unit multiplication)
impl Mul<Amperes> for Volts {
type Output = Watts;
fn mul(self, rhs: Amperes) -> Watts { Watts(self.0 * rhs.0) }
}
// --- Generic threshold checker ---
// Exercise 3 extends ch06's Threshold with a generic ThresholdResult<T>
// that carries the triggering reading — an evolution of ch06's simpler
// ThresholdResult { Normal, Warning, Critical } enum.
pub enum ThresholdResult<T> {
Normal(T),
Warning(T),
Critical(T),
}
pub struct Threshold<T> {
pub warning: T,
pub critical: T,
}
// Generic impl — works for any unit type that supports 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)
}
}
}
// Now `Threshold<Rpm>`, `Threshold<Volts>`, etc. all work automatically.
// --- Pipeline: ADC → calibration → threshold → result ---
pub struct CalibrationParams {
pub scale: f64, // ADC counts per °C
pub offset: f64, // °C at ADC 0
}
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)
}
// Compile-time safety — these would NOT compile:
// let _ = Celsius(25.0) + Volts(12.0); // ERROR: mismatched types
// let _: Millivolts = Volts(1.0); // ERROR: no implicit coercion
// let _ = Watts(100.0) + Rpm(3000); // ERROR: mismatched types
Key points:
- Each physical unit is a distinct type — no accidental mixing.
Mul<Amperes> for VoltsyieldsWatts, encoding P = V × I in the type system.- Explicit
Fromconversions for related units (mV ↔ V, °C ↔ °F). Threshold<Celsius>only acceptsCelsius— can’t accidentally threshold-check RPM.
Exercise 4: PCIe Capability Walk (Phantom Types + Validated Boundary)
Model the PCIe capability linked list:
RawCapability— unvalidated bytes from config spaceValidCapability— parsed and validated (via TryFrom)- Each capability type (MSI, MSI-X, PCIe Express, Power Management) has its own phantom-typed register layout
- Walking the list returns an iterator of
ValidCapabilityvalues
Hint: Combine validated boundaries (ch07) with phantom types (ch09).
Sample Solution (Exercise 4)
use std::marker::PhantomData;
// --- Phantom markers for capability types ---
pub struct Msi;
pub struct MsiX;
pub struct PciExpress;
pub struct PowerMgmt;
// PCI capability IDs from the spec
const CAP_ID_PM: u8 = 0x01;
const CAP_ID_MSI: u8 = 0x05;
const CAP_ID_PCIE: u8 = 0x10;
const CAP_ID_MSIX: u8 = 0x11;
/// Unvalidated bytes — may be garbage.
#[derive(Debug)]
pub struct RawCapability {
pub id: u8,
pub next_ptr: u8,
pub data: Vec<u8>,
}
/// Validated and type-tagged capability.
#[derive(Debug)]
pub struct ValidCapability<Kind> {
id: u8,
next_ptr: u8,
data: Vec<u8>,
_kind: PhantomData<Kind>,
}
// --- TryFrom: parse-don't-validate boundary ---
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("not a PM capability"); }
if raw.data.len() < 2 { return Err("PM data too short"); }
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("not an MSI capability"); }
if raw.data.len() < 6 { return Err("MSI data too short"); }
Ok(ValidCapability {
id: raw.id, next_ptr: raw.next_ptr,
data: raw.data, _kind: PhantomData,
})
}
}
// (Similar TryFrom impls for MsiX, PciExpress — omitted for brevity)
// --- Type-safe accessors: only available on the correct 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 walker: iterates the linked list ---
pub struct CapabilityWalker<'a> {
config_space: &'a [u8],
next_ptr: u8,
}
impl<'a> CapabilityWalker<'a> {
pub fn new(config_space: &'a [u8]) -> Self {
// Capability pointer lives at offset 0x34 in PCI config space
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 })
}
}
// Usage:
// 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());
// }
// }
Key points:
RawCapability→ValidCapability<Kind>is the parse-don’t-validate boundary.pm_control()only exists onValidCapability<PowerMgmt>— calling it on an MSI capability is a compile error.- The
CapabilityWalkeriterator yields raw capabilities; the caller validates the ones they care about withTryFrom.
Exercise 5: Multi-Protocol Health Check (Capability Mixins)
Create a health-check framework:
- Define ingredient traits:
HasIpmi,HasRedfish,HasNvmeCli,HasGpio - Create mixin traits:
ThermalHealthMixin(requires HasIpmi + HasGpio) — reads temps, checks alertsStorageHealthMixin(requires HasNvmeCli) — SMART data checksBmcHealthMixin(requires HasIpmi + HasRedfish) — cross-validates BMC data
- Build a
FullPlatformControllerthat implements all ingredient traits - Build a
StorageOnlyControllerthat only implementsHasNvmeCli - Verify that
StorageOnlyControllergetsStorageHealthMixinbut NOT the others
Sample Solution (Exercise 5)
// --- Ingredient traits ---
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,
}
// --- Mixin traits with blanket impls ---
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 }
// --- Full platform: all ingredients → all three mixins for free ---
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 }
}
// --- Storage-only: only HasNvmeCli → only StorageHealthMixin ---
pub struct StorageOnlyController;
impl HasNvmeCli for StorageOnlyController {
fn nvme_smart_log(&self, _dev: &str) -> SmartData {
SmartData { temperature_kelvin: 315, spare_pct: 80 }
}
}
// StorageOnlyController automatically gets storage_check().
// Calling thermal_check() or bmc_health() on it is a COMPILE ERROR.
Key points:
- Blanket
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}— any type that implements both ingredients automatically gets the mixin. StorageOnlyControlleronly implementsHasNvmeCli, so the compiler grants itStorageHealthMixinbut rejectsthermal_check()andbmc_health()— zero runtime checks needed.- Adding a new mixin (e.g.,
NetworkHealthMixin: HasRedfish + HasGpio) is one trait- one blanket impl — existing controllers pick it up automatically if they qualify.
Exercise 6: Session-Typed Diagnostic Protocol (Single-Use + Type-State)
Design a diagnostic session with single-use test execution tokens:
DiagSessionstarts inSetupstate- Transition to
Runningstate — issuesNexecution tokens (one per test case) - Each
TestTokenis consumed when the test runs — prevents running the same test twice - After all tokens are consumed, transition to
Completestate - Generate a report (only in
Completestate)
Advanced: Use a const generic N to track how many tests remain at the type level.
Sample Solution (Exercise 6)
// --- State types ---
pub struct Setup;
pub struct Running;
pub struct Complete;
/// Single-use test token. NOT Clone, NOT Copy — consumed on use.
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,
}
}
/// Transition to Running — issues one token per test case.
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> {
/// Consume a token to run one test. The move prevents double-running.
pub fn run_test(mut self, token: TestToken) -> Self {
let passed = true; // real code runs actual diagnostics here
self.results.push(TestResult {
test_name: token.test_name,
passed,
});
self
}
/// Transition to Complete.
///
/// **Note:** This solution does NOT enforce that all tokens have been
/// consumed — `finish()` can be called with tokens still outstanding.
/// The tokens will simply be dropped (they're not `#[must_use]`).
/// For full compile-time enforcement, use the const-generic variant
/// described in the "Advanced" note below, where `finish()` is only
/// available on `DiagSession<Running, 0>`.
pub fn finish(self) -> DiagSession<Complete> {
DiagSession {
name: self.name,
results: self.results,
_state: Complete,
}
}
}
impl DiagSession<Complete> {
/// Report is ONLY available in Complete state.
pub fn report(&self) -> String {
let total = self.results.len();
let passed = self.results.iter().filter(|r| r.passed).count();
format!("{}: {}/{} passed", self.name, passed, total)
}
}
// Usage:
// let session = DiagSession::new("GPU stress");
// 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 stress: 3/3 passed"
//
// // These would NOT compile:
// // session.run_test(used_token); → ERROR: use of moved value
// // running_session.report(); → ERROR: no method `report` on DiagSession<Running>
Key points:
TestTokenis notCloneorCopy— consuming it viarun_test(token)moves it, so re-running the same test is a compile error.report()only exists onDiagSession<Complete>— calling it mid-run is impossible.- The Advanced variant would use
DiagSession<Running, N>with const generics whererun_testreturnsDiagSession<Running, {N-1}>andfinishis only available onDiagSession<Running, 0>— that ensures all tokens are consumed before finishing.
Key Takeaways
- Practice with realistic protocols — NVMe, firmware update, sensor pipelines, PCIe are all real-world targets for these patterns.
- Each exercise maps to a core chapter — use the cross-references to review the pattern before attempting.
- Solutions use expandable details — try each exercise before revealing the solution.
- Compose patterns in exercise 5 — multi-protocol health checks combine typed commands, dimensional types, and validated boundaries.
- Session types (exercise 6) are the frontier — they enforce message ordering across channels, extending type-state to distributed systems.
Reference Card
Quick-reference for all 14+ correct-by-construction patterns with selection flowchart, pattern catalogue, composition rules, crate mapping, and types-as-guarantees cheat sheet.
Cross-references: Every chapter — this is the lookup table for the entire book.
Quick Reference: Correct-by-Construction Patterns
Pattern Selection Guide
Is the bug catastrophic if missed?
├── Yes → Can it be encoded in types?
│ ├── Yes → USE CORRECT-BY-CONSTRUCTION
│ └── No → Runtime check + extensive testing
└── No → Runtime check is fine
Pattern Catalogue
| # | Pattern | Key Trait/Type | Prevents | Runtime Cost | Chapter |
|---|---|---|---|---|---|
| 1 | Typed Commands | trait IpmiCmd { type Response; } | Wrong response type | Zero | ch02 |
| 2 | Single-Use Types | struct Nonce (not Clone/Copy) | Nonce/key reuse | Zero | ch03 |
| 3 | Capability Tokens | struct AdminToken { _private: () } | Unauthorised access | Zero | ch04 |
| 4 | Type-State | Session<Active> | Protocol violations | Zero | ch05 |
| 5 | Dimensional Types | struct Celsius(f64) | Unit confusion | Zero | ch06 |
| 6 | Validated Boundaries | struct ValidFru (via TryFrom) | Unvalidated data use | Parse once | ch07 |
| 7 | Capability Mixins | trait FanDiagMixin: HasSpi + HasI2c | Missing bus access | Zero | ch08 |
| 8 | Phantom Types | Register<Width16> | Width/direction mismatch | Zero | ch09 |
| 9 | Sentinel → Option | Option<u8> (not 0xFF) | Sentinel-as-value bugs | Zero | ch11 |
| 10 | Sealed Traits | trait Cmd: private::Sealed | Unsound external impls | Zero | ch11 |
| 11 | Non-Exhaustive Enums | #[non_exhaustive] enum Sku | Silent match fallthrough | Zero | ch11 |
| 12 | Typestate Builder | DerBuilder<Set, Missing> | Incomplete construction | Zero | ch11 |
| 13 | FromStr Validation | impl FromStr for DiagLevel | Unvalidated string input | Parse once | ch11 |
| 14 | Const-Generic Size | RegisterBank<const N: usize> | Buffer size mismatch | Zero | ch11 |
| 15 | Safe unsafe Wrapper | MmioRegion::read_u32() | Unchecked MMIO/FFI | Zero | ch11 |
| 16 | Async Type-State | AsyncSession<Active> | Async protocol violations | Zero | ch11 |
| 17 | Const Assertions | SdrSensorId<const N: u8> | Invalid compile-time IDs | Zero | ch11 |
| 18 | Session Types | Chan<SendRequest> | Out-of-order channel ops | Zero | ch11 |
| 19 | Pin Self-Referential | Pin<Box<StreamParser>> | Dangling intra-struct pointer | Zero | ch11 |
| 20 | RAII / Drop | impl Drop for Session | Resource leak on any exit path | Zero | ch11 |
| 21 | Error Type Hierarchy | #[derive(Error)] enum DiagError | Silent error swallowing | Zero | ch11 |
| 22 | #[must_use] | #[must_use] struct Token | Silently dropped values | Zero | ch11 |
Composition Rules
Capability Token + Type-State = Authorised state transitions
Typed Command + Dimensional Type = Physically-typed responses
Validated Boundary + Phantom Type = Typed register access on validated config
Capability Mixin + Typed Command = Bus-aware typed operations
Single-Use Type + Type-State = Consume-on-transition protocols
Sealed Trait + Typed Command = Closed, sound command set
Sentinel → Option + Validated Boundary = Clean parse-once pipeline
Typestate Builder + Capability Token = Proof-of-complete construction
FromStr + #[non_exhaustive] = Evolvable, fail-fast enum parsing
Const-Generic Size + Validated Boundary = Sized, validated protocol buffers
Safe unsafe Wrapper + Phantom Type = Typed, safe MMIO access
Async Type-State + Capability Token = Authorised async transitions
Session Types + Typed Command = Fully-typed request-response channels
Pin + Type-State = Self-referential state machines that can't move
RAII (Drop) + Type-State = State-dependent cleanup guarantees
Error Hierarchy + Validated Boundary = Typed parse errors with exhaustive handling
#[must_use] + Single-Use Type = Hard-to-ignore, hard-to-reuse tokens
Anti-Patterns to Avoid
| Anti-Pattern | Why It’s Wrong | Correct Alternative |
|---|---|---|
fn read_sensor() -> f64 | Unitless — could be °C, °F, or RPM | fn read_sensor() -> Celsius |
fn encrypt(nonce: &[u8; 12]) | Nonce can be reused (borrow) | fn encrypt(nonce: Nonce) (move) |
fn admin_op(is_admin: bool) | Caller can lie (true) | fn admin_op(_: &AdminToken) |
fn send(session: &Session) | No state guarantee | fn send(session: &Session<Active>) |
fn process(data: &[u8]) | Not validated | fn process(data: &ValidFru) |
Clone on ephemeral keys | Defeats single-use guarantee | Don’t derive Clone |
let vendor_id: u16 = 0xFFFF | Sentinel carried internally | let vendor_id: Option<u16> = None |
fn route(level: &str) with fallback | Typos silently default | let level: DiagLevel = s.parse()? |
Builder::new().finish() without fields | Incomplete object constructed | Typestate builder: finish() gated on Set |
let buf: Vec<u8> for fixed-size HW buffer | Size only checked at runtime | RegisterBank<4096> (const generic) |
Raw unsafe { ptr::read(...) } scattered | UB risk, unauditable | MmioRegion::read_u32() safe wrapper |
async fn transition(&mut self) | Mutable borrows don’t enforce state | async fn transition(self) -> NextState |
fn cleanup() called manually | Forgotten on early return / panic | impl Drop — compiler inserts call |
fn op() -> Result<T, String> | Opaque error, no variant matching | fn op() -> Result<T, DiagError> enum |
Mapping to a Diagnostics Codebase
| Module | Applicable Pattern(s) |
|---|---|
protocol_lib | Typed commands, type-state sessions |
thermal_diag | Capability mixins, dimensional types |
accel_diag | Validated boundaries, phantom registers |
network_diag | Type-state (link training), capability tokens |
pci_topology | Phantom types (register width), validated config, sentinel → Option |
event_handler | Single-use audit tokens, capability tokens, FromStr (Component) |
event_log | Validated boundaries (SEL record parsing) |
compute_diag | Dimensional types (temperature, frequency) |
memory_diag | Validated boundaries (SPD data), dimensional types |
switch_diag | Type-state (port enumeration), phantom types |
config_loader | FromStr (DiagLevel, FaultStatus, DiagAction) |
log_analyzer | Validated boundaries (CompiledPatterns) |
diag_framework | Typestate builder (DerBuilder), session types (orchestrator↔worker) |
topology_lib | Const-generic register banks, safe MMIO wrappers |
Types as Guarantees — Quick Mapping
| Guarantee | Rust Equivalent | Example |
|---|---|---|
| “This proof exists” | A type | AdminToken |
| “I have the proof” | A value of that type | let tok = authenticate()?; |
| “A implies B” | Function fn(A) -> B | fn activate(AdminToken) -> Session<Active> |
| “Both A and B” | Tuple (A, B) or multi-param | fn op(a: &AdminToken, b: &LinkTrained) |
| “Either A or B” | enum { A(A), B(B) } or Result<A, B> | Result<Session<Active>, Error> |
| “Always true” | () (unit type) | Always constructible |
| “Impossible” | ! (never type) or enum Void {} | Can never be constructed |
Testing Type-Level Guarantees 🟡
What you’ll learn: How to test that invalid code fails to compile (trybuild), fuzz validated boundaries (proptest), verify RAII invariants, and prove zero-cost abstraction via
cargo-show-asm.Cross-references: ch03 (compile-fail for nonces), ch07 (proptest for boundaries), ch05 (RAII for sessions)
Testing Type-Level Guarantees
Correct-by-construction patterns shift bugs from runtime to compile time. But how do you test that invalid code actually fails to compile? And how do you ensure validated boundaries hold under fuzzing? This chapter covers the testing tools that complement type-level correctness.
Compile-Fail Tests with trybuild
The trybuild crate lets you assert that
certain code should not compile. This is essential for maintaining type-level
invariants across refactors — if someone accidentally adds Clone to your
single-use Nonce, the compile-fail test catches it.
Setup:
# Cargo.toml
[dev-dependencies]
trybuild = "1"
Test file (tests/compile_fail.rs):
#[test]
fn type_safety_tests() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}
Test case: Nonce reuse must not compile (tests/ui/nonce_reuse.rs):
// tests/ui/nonce_reuse.rs
use my_crate::Nonce;
fn main() {
let nonce = Nonce::new();
encrypt(nonce);
encrypt(nonce); // should fail: use of moved value
}
fn encrypt(_n: Nonce) {}
Expected error (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
More compile-fail test cases per chapter:
| Pattern (Chapter) | Test assertion | File |
|---|---|---|
| Single-Use Nonce (ch03) | Can’t use nonce twice | nonce_reuse.rs |
| Capability Token (ch04) | Can’t call admin_op() without token | missing_token.rs |
| Type-State (ch05) | Can’t send_command() on Session<Idle> | wrong_state.rs |
| Dimensional (ch06) | Can’t add Celsius + Rpm | unit_mismatch.rs |
| Sealed Trait (Trick 2) | External crate can’t impl sealed trait | unseal_attempt.rs |
| Non-Exhaustive (Trick 3) | External match without wildcard fails | missing_wildcard.rs |
CI integration:
# .github/workflows/ci.yml
- name: Run compile-fail tests
run: cargo test --test compile_fail
Property-Based Testing of Validated Boundaries
Validated boundaries (ch07) parse data once and reject invalid input. But
how do you know your validation catches all invalid inputs? Property-based
testing with proptest generates
thousands of random inputs to stress the boundary:
# Cargo.toml
[dev-dependencies]
proptest = "1"
use proptest::prelude::*;
/// From ch07: ValidFru wraps a spec-compliant FRU payload.
/// These tests use the full ch07 ValidFru with board_area(),
/// product_area(), and format_version() methods.
/// Note: ch07 defines TryFrom<RawFruData>, so we wrap raw bytes first.
proptest! {
/// Any byte sequence that passes validation must be usable without 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)) {
// These must never panic on a validated FRU
// (methods from ch07's ValidFru impl):
let _ = fru.format_version();
let _ = fru.board_area();
let _ = fru.product_area();
}
}
/// Round-trip: format_version is preserved through reparsing.
#[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();
// Re-parse the same bytes — version must be identical
let reparsed = ValidFru::try_from(RawFruData(data)).unwrap();
prop_assert_eq!(version, reparsed.format_version());
}
}
/// Custom strategy: generates byte vectors that satisfy the FRU spec header.
/// The header format matches ch07's `TryFrom<RawFruData>` validation:
/// - Byte 0: version = 0x01
/// - Bytes 1-6: area offsets (×8 = actual byte offset)
/// - Byte 7: checksum (sum of bytes 0-7 = 0 mod 256)
/// The body is random but large enough for the offsets to be in-bounds.
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
})
}
The testing pyramid for correct-by-construction code:
┌───────────────────────────────────┐
│ Compile-Fail Tests (trybuild) │ ← "Invalid code must not compile"
├───────────────────────────────────┤
│ Property Tests (proptest/quickcheck) │ ← "Valid inputs never panic"
├───────────────────────────────────┤
│ Unit Tests (#[test]) │ ← "Specific inputs produce expected outputs"
├───────────────────────────────────┤
│ Type System (patterns ch02–13) │ ← "Entire classes of bugs can't exist"
└───────────────────────────────────┘
RAII Verification
RAII (Trick 12) guarantees cleanup. To test this, verify that the Drop impl
actually fires:
use std::sync::atomic::{AtomicBool, Ordering};
// NOTE: These tests use a global AtomicBool, so they must not run in
// parallel with each other. Use `#[serial_test::serial]` or run with
// `cargo test -- --test-threads=1`. Alternatively, use a per-test
// `Arc<AtomicBool>` passed via closure to avoid the global entirely.
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("simulated failure")?;
Ok(())
})();
assert!(result.is_err());
assert!(DROPPED.load(Ordering::SeqCst), "Drop must fire on early return");
}
#[test]
fn session_drops_on_panic() {
DROPPED.store(false, Ordering::SeqCst);
let result = std::panic::catch_unwind(|| {
let _session = TestSession;
panic!("simulated panic");
});
assert!(result.is_err());
assert!(DROPPED.load(Ordering::SeqCst), "Drop must fire on panic");
}
Applying to Your Codebase
Here’s a prioritized plan for adding type-level tests to the workspace:
| Crate | Test type | What to test |
|---|---|---|
protocol_lib | Compile-fail | Session<Idle> can’t send_command() |
protocol_lib | Property | Any byte seq → TryFrom either succeeds or returns Err (no panic) |
thermal_diag | Compile-fail | Can’t construct FanReading without HasSpi mixin |
accel_diag | Property | GPU sensor parsing: random bytes → validated-or-rejected |
config_loader | Property | Random strings → FromStr for DiagLevel never panics |
pci_topology | Compile-fail | Register<Width16> can’t be passed where Width32 expected |
event_handler | Compile-fail | Audit token can’t be cloned |
diag_framework | Compile-fail | DerBuilder<Missing, _> can’t call finish() |
Zero-Cost Abstraction: Proof by Assembly
A common concern: “Do newtypes and phantom types add runtime overhead?” The answer is no — they compile to identical assembly as raw primitives. Here’s how to verify:
Setup:
cargo install cargo-show-asm
Example: Newtype vs raw u32:
// src/lib.rs
#[derive(Clone, Copy)]
pub struct Rpm(pub u32);
#[derive(Clone, Copy)]
pub struct Celsius(pub f64);
// Newtype arithmetic
#[inline(never)]
pub fn add_rpm(a: Rpm, b: Rpm) -> Rpm {
Rpm(a.0 + b.0)
}
// Raw arithmetic (for comparison)
#[inline(never)]
pub fn add_raw(a: u32, b: u32) -> u32 {
a + b
}
Run:
cargo asm my_crate::add_rpm
cargo asm my_crate::add_raw
Result — identical assembly:
; add_rpm (newtype) ; add_raw (raw u32)
my_crate::add_rpm: my_crate::add_raw:
lea eax, [rdi + rsi] lea eax, [rdi + rsi]
ret ret
The Rpm wrapper is completely erased at compile time. The same holds for
PhantomData<S> (zero bytes), ZST tokens (zero bytes), and all other
type-level markers used throughout this guide.
Verify for your own types:
# Show assembly for a specific function
cargo asm --lib ipmi_lib::session::execute
# Show that PhantomData adds zero bytes
cargo asm --lib --rust ipmi_lib::session::IpmiSession
Key takeaway: Every pattern in this guide has zero runtime cost. The type system does all the work and is erased completely during compilation. You get the safety of Haskell with the performance of C.
Key Takeaways
- trybuild tests that invalid code won’t compile — essential for maintaining type-level invariants across refactors.
- proptest fuzzes validation boundaries — generates thousands of random inputs to stress
TryFromimplementations. - RAII verification tests that Drop runs — Arc counters or mock flags prove cleanup happened.
- cargo-show-asm proves zero-cost — phantom types, ZSTs, and newtypes produce the same assembly as raw C.
- Add compile-fail tests for every “impossible” state — if someone accidentally derives
Cloneon a single-use type, the test catches it.
End of Type-Driven Correctness in Rust
Const Fn — Compile-Time Correctness Proofs 🟠
What you’ll learn: How
const fnandassert!turn the compiler into a proof engine — verifying SRAM memory maps, register layouts, protocol frames, bitfield masks, clock trees, and lookup tables at compile time with zero runtime cost.Cross-references: ch04 (capability tokens), ch06 (dimensional analysis), ch09 (phantom types)
The Problem: Memory Maps That Lie
In embedded and systems programming, memory maps are the foundation of everything — they define where bootloaders, firmware, data sections, and stacks live. Get a boundary wrong, and two subsystems silently corrupt each other. In C, these maps are typically #define constants with no structural relationship:
/* STM32F4 SRAM layout — 256 KB at 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) /* Someone bumped this from 64K to 80K */
#define STACK_BASE 0x20038000
#define STACK_SIZE (48 * 1024) /* 0x20038000 + 48K = 0x20044000 — past SRAM end! */
The bug: 16 + 128 + 80 + 48 = 272 KB, but SRAM is only 256 KB. The stack extends 16 KB past the end of physical memory. No compiler warning, no linker error, no runtime check — just silent corruption when the stack grows into unmapped space.
Every failure mode is discovered after deployment — potentially as a mysterious crash that only happens under heavy stack usage, weeks after the data section was resized.
Const Fn: Turning the Compiler into a Proof Engine
Rust’s const fn functions can run at compile time. When a const fn panics during compile-time evaluation, the panic becomes a compile error. Combined with assert!, this turns the compiler into a theorem prover for your invariants:
pub const fn checked_add(a: u32, b: u32) -> u32 {
let sum = a as u64 + b as u64;
assert!(sum <= u32::MAX as u64, "overflow");
sum as u32
}
// ✅ Compiles — 100 + 200 fits in u32
const X: u32 = checked_add(100, 200);
// ❌ Compile error: "overflow"
// const Y: u32 = checked_add(u32::MAX, 1);
fn main() {
println!("{X}");
}
The key insight:
const fn+assert!= a proof obligation. Each assertion is a theorem that the compiler must verify. If the proof fails, the program does not compile. No test suite needed, no code review catch — the compiler itself is the auditor.
Building a Verified SRAM Memory Map
The Region Type
A Region represents a contiguous block of memory. Its constructor is a const fn that enforces basic validity:
#[derive(Debug, Clone, Copy)]
pub struct Region {
pub base: u32,
pub size: u32,
}
impl Region {
/// Create a region. Panics at compile time if invariants fail.
pub const fn new(base: u32, size: u32) -> Self {
assert!(size > 0, "region size must be non-zero");
assert!(
base as u64 + size as u64 <= u32::MAX as u64,
"region overflows 32-bit address space"
);
Self { base, size }
}
pub const fn end(&self) -> u32 {
self.base + self.size
}
/// True if `inner` fits entirely within `self`.
pub const fn contains(&self, inner: &Region) -> bool {
inner.base >= self.base && inner.end() <= self.end()
}
/// True if two regions share any addresses.
pub const fn overlaps(&self, other: &Region) -> bool {
self.base < other.end() && other.base < self.end()
}
/// True if `addr` falls within this region.
pub const fn contains_addr(&self, addr: u32) -> bool {
addr >= self.base && addr < self.end()
}
}
// Every Region is born valid — you cannot construct an invalid one
const R: Region = Region::new(0x2000_0000, 1024);
fn main() {
println!("Region: {:#010X}..{:#010X}", R.base, R.end());
}
The Verified Memory Map
Now we compose regions into a full SRAM map. The constructor proves six overlap-freedom invariants and four containment invariants — all at compile time:
#[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, "region size must be non-zero");
assert!(base as u64 + size as u64 <= u32::MAX as u64, "overflow");
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 {
// ── Containment: every sub-region fits within total SRAM ──
assert!(total.contains(&bootloader), "bootloader exceeds SRAM");
assert!(total.contains(&firmware), "firmware exceeds SRAM");
assert!(total.contains(&data), "data section exceeds SRAM");
assert!(total.contains(&stack), "stack exceeds SRAM");
// ── Overlap freedom: no pair of sub-regions shares an address ──
assert!(!bootloader.overlaps(&firmware), "bootloader/firmware overlap");
assert!(!bootloader.overlaps(&data), "bootloader/data overlap");
assert!(!bootloader.overlaps(&stack), "bootloader/stack overlap");
assert!(!firmware.overlaps(&data), "firmware/data overlap");
assert!(!firmware.overlaps(&stack), "firmware/stack overlap");
assert!(!data.overlaps(&stack), "data/stack overlap");
Self { total, bootloader, firmware, data, stack }
}
}
// ✅ All 10 invariants verified at compile time — zero runtime cost
const SRAM: SramMap = SramMap::verified(
Region::new(0x2000_0000, 256 * 1024), // 256 KB total 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);
}
Ten compile-time checks, zero runtime instructions. The binary contains only the verified constants.
Breaking the Map
Suppose someone increases the data section from 64 KB to 80 KB without adjusting anything else:
// ❌ Does not compile
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 too large
Region::new(0x2003_8000, 48 * 1024), // stack pushed past SRAM end
);
The compiler reports:
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'
The bug that would have been a mysterious field failure is now a compile error. No unit test needed, no code review catch — the compiler proves it impossible. Compare this to C, where the same bug would ship silently and surface as a stack corruption months later in the field.
Layering Access Control with Phantom Types
Combine const fn verification with phantom-typed access permissions (ch09) to enforce read/write constraints at the type level:
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, "region size must be non-zero");
Self { base, size, _access: PhantomData }
}
}
// Read is available for any access level
fn read_word<A>(region: &TypedRegion<A>, offset: u32) -> u32 {
assert!(offset + 4 <= region.size, "read out of bounds");
// In real firmware: unsafe { core::ptr::read_volatile((region.base + offset) as *const u32) }
0 // stub
}
// Write requires ReadWrite — the function signature enforces it
fn write_word(region: &TypedRegion<ReadWrite>, offset: u32, value: u32) {
assert!(offset + 4 <= region.size, "write out of bounds");
// In real firmware: unsafe { core::ptr::write_volatile(...) }
let _ = value; // stub
}
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 from read-only region
read_word(&DATA, 0); // ✅ read from read-write region
write_word(&DATA, 0, 42); // ✅ write to read-write region
// write_word(&BOOTLOADER, 0, 42); // ❌ Compile error: expected ReadWrite, found ReadOnly
}
The bootloader region is physically writeable (it’s SRAM), but the type system prevents accidental writes. This distinction between hardware capability and software permission is exactly what correct-by-construction means.
Pointer Provenance: Proving Addresses Belong to Regions
Taking it further, we can create verified addresses — values that are statically proven to lie within a specific region:
#[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()
}
}
/// An address proven at compile time to lie within a Region.
pub struct VerifiedAddr {
addr: u32, // private — can only be created through the checked constructor
}
impl VerifiedAddr {
/// Panics at compile time if `addr` is outside `region`.
pub const fn new(region: &Region, addr: u32) -> Self {
assert!(region.contains_addr(addr), "address outside region");
Self { addr }
}
pub const fn raw(&self) -> u32 {
self.addr
}
}
const DATA: Region = Region::new(0x2002_4000, 64 * 1024);
// ✅ Proven at compile time to be inside the data region
const STATUS_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_4000);
const CONFIG_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_5000);
// ❌ Would not compile: address is in the bootloader region, not data
// const BAD_ADDR: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2000_0000);
fn main() {
println!("Status register at {:#010X}", STATUS_WORD.raw());
println!("Config register at {:#010X}", CONFIG_WORD.raw());
}
Provenance established at compile time — no runtime bounds check needed when accessing these addresses. The constructor is private, so a VerifiedAddr can only exist if the compiler has proven it valid.
Beyond Memory Maps
The const fn proof pattern applies wherever you have compile-time-known values with structural invariants. The SRAM map above proved inter-region properties (containment, non-overlap). The same technique scales to increasingly fine-grained domains:
flowchart TD
subgraph coarse["Coarse-Grained"]
MEM["Memory Maps<br/>regions don't overlap"]
REG["Register Maps<br/>offsets are aligned & disjoint"]
end
subgraph fine["Fine-Grained"]
BIT["Bitfield Layouts<br/>masks are disjoint within a register"]
FRAME["Protocol Frames<br/>fields are contiguous, total ≤ max"]
end
subgraph derived["Derived-Value Chains"]
PLL["Clock Trees / PLL<br/>each intermediate freq in range"]
LUT["Lookup Tables<br/>computed & verified at compile time"]
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
Each subsection below follows the same pattern: define a type with a const fn constructor that encodes the invariants, then use const _: () = { ... } or a const binding to trigger verification.
Register Maps
Hardware register blocks have fixed offsets and widths. A misaligned or overlapping register definition is always a 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,
"register width must be 1, 2, or 4 bytes"
);
assert!(offset % width == 0, "register must be naturally aligned");
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 peripheral registers
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);
// Compile-time proof: no register overlaps another
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);
}
Note the const _: () = { ... }; idiom — an unnamed constant whose only purpose is to run compile-time assertions. If any assertion fails, the constant can’t be evaluated and compilation stops.
Mini-Exercise: SPI Register Bank
Given these SPI controller registers, add const fn assertions proving:
- Every register is naturally aligned (offset % width == 0)
- No two registers overlap
- All registers fit within a 64-byte register block
Hint
Reuse the Register and disjoint functions from the UART example above. Define three or four const Register values (e.g., CTRL at offset 0x00 width 4, STATUS at 0x04 width 4, TX_DATA at 0x08 width 1, RX_DATA at 0x0C width 1) and assert the three properties.
Protocol Frame Layouts
Network or bus protocol frames have fields at specific offsets. The then() method makes contiguity structural — gaps and overlaps are impossible by construction:
#[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, "field size must be non-zero");
Self { offset, size }
}
pub const fn end(&self) -> usize {
self.offset + self.size
}
/// Create the next field immediately after this one.
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);
// Compile-time proof: frame fits within maximum size
const _: () = assert!(CRC.end() <= MAX_FRAME, "frame exceeds maximum size");
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: {}/{} bytes", CRC.end(), MAX_FRAME);
}
Fields are contiguous by construction — each starts exactly where the previous one ends. The final assertion proves the frame fits within the protocol’s maximum size.
Inline Const Blocks for Generic Validation
Since Rust 1.79, const { ... } blocks let you validate const generic parameters at the point of use — perfect for DMA buffer size constraints or alignment requirements:
fn dma_transfer<const N: usize>(buf: &[u8; N]) {
const { assert!(N % 4 == 0, "DMA buffer must be 4-byte aligned in size") };
const { assert!(N <= 65536, "DMA transfer exceeds maximum size") };
// ... initiate transfer ...
}
dma_transfer(&[0u8; 1024]); // ✅ 1024 is divisible by 4 and ≤ 65536
// dma_transfer(&[0u8; 1023]); // ❌ Compile error: not 4-byte aligned
The assertions are evaluated when the function is monomorphized — each call site with a different N gets its own compile-time check.
Bitfield Layouts Within a Register
Register maps prove that registers don’t overlap each other — but what about the bits within a single register? Control registers pack multiple fields into one word. If two fields share a bit position, reads and writes silently corrupt each other. In C, this is typically caught (or not) by manual review of mask constants.
A const fn can prove that every field’s mask/shift pair is disjoint from every other field in the same register:
#[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, "bit field width must be non-zero");
assert!(shift as u32 + width as u32 <= 32, "bit field exceeds 32-bit register");
// Build mask: `width` ones starting at bit `shift`
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 exceeds field width");
value << self.shift as u32
}
}
const fn fields_disjoint(a: &BitField, b: &BitField) -> bool {
a.positioned_mask() & b.positioned_mask() == 0
}
// SPI Control Register fields: enable[0], mode[1:2], clock_div[4:7], irq_en[8]
const SPI_EN: BitField = BitField::new(0, 1); // bit 0
const SPI_MODE: BitField = BitField::new(1, 2); // bits 1–2
const SPI_CLKDIV: BitField = BitField::new(4, 4); // bits 4–7
const SPI_IRQ: BitField = BitField::new(8, 1); // bit 8
// Compile-time proof: no field shares a bit position
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);
}
This complements the register map pattern above — register maps prove inter-register disjointness while bitfield layouts prove intra-register disjointness. Together they provide full coverage from the register block down to individual bits.
Clock Tree / PLL Configuration
Microcontrollers derive peripheral clocks through multiplier/divider chains. A PLL produces f_vco = f_in × N / M, and the VCO frequency must stay within a hardware-specified range. Get any parameter wrong for a specific board, and the chip outputs garbage clocks or refuses to lock. These constraints are perfect for const fn:
#[derive(Debug, Clone, Copy)]
pub struct PllConfig {
pub input_khz: u32, // external oscillator
pub m: u32, // input divider
pub n: u32, // VCO multiplier
pub p: u32, // system clock divider
}
impl PllConfig {
pub const fn verified(input_khz: u32, m: u32, n: u32, p: u32) -> Self {
// Input divider produces the PLL input frequency
let pll_input = input_khz / m;
assert!(pll_input >= 1_000 && pll_input <= 2_000,
"PLL input must be 1–2 MHz");
// VCO frequency must be within hardware limits
let vco = pll_input as u64 * n as u64;
assert!(vco >= 192_000 && vco <= 432_000,
"VCO must be 192–432 MHz");
// System clock divider must be even (hardware constraint)
assert!(p == 2 || p == 4 || p == 6 || p == 8,
"P must be 2, 4, 6, or 8");
// Final system clock
let sysclk = vco / p as u64;
assert!(sysclk <= 168_000,
"system clock exceeds 168 MHz maximum");
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 with 8 MHz HSE crystal → 168 MHz system clock
const PLL: PllConfig = PllConfig::verified(8_000, 8, 336, 2);
// ❌ Would not compile: VCO = 480 MHz exceeds 432 MHz limit
// 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);
}
Uncommenting the BAD constant produces a compile-time error that pinpoints the violated constraint:
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'
The compiler catches the constraint violation in the middle of the derivation chain — not at the end. If you had instead violated the system clock limit (sysclk > 168 MHz), the error message would point to that assertion instead.
Derived-value constraint chains turn a single
const fninto a multi-stage proof. Each intermediate value has its own hardware-mandated range. Changing one parameter (e.g., swapping to a 25 MHz crystal) immediately surfaces any downstream violation.
Derived-value constraint chains — the VCO frequency depends on input / m × n, and the system clock depends on vco / p. Each intermediate value has its own hardware-mandated range. A single const fn verifies the entire chain, so changing one parameter (e.g., swapping to a 25 MHz crystal) immediately surfaces any downstream violation.
Compile-Time Lookup Tables
const fn can compute entire lookup tables at compile time, placing them in .rodata with zero startup cost. This is especially valuable for CRC tables, trigonometry, encoding maps, and error-correction codes — anywhere you’d normally use a build script or code generation:
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; // standard CRC-32 polynomial
} else {
crc >>= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
}
/// Full CRC-32 table — computed at compile time, placed in .rodata
const CRC32_TABLE: [u32; 256] = crc32_table();
/// Compute CRC-32 over a byte slice at runtime using the precomputed table.
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
}
// Smoke-test: well-known CRC-32 of "123456789"
const _: () = {
// Verify a single table entry at compile time
assert!(CRC32_TABLE[0] == 0x0000_0000);
assert!(CRC32_TABLE[1] == 0x7707_3096);
};
fn main() {
let check = crc32(b"123456789");
// Known CRC-32 of "123456789" is 0xCBF43926
assert_eq!(check, 0xCBF4_3926);
println!("CRC-32 of '123456789' = {:#010X} ✓", check);
println!("Table size: {} entries × 4 bytes = {} bytes in .rodata",
CRC32_TABLE.len(), CRC32_TABLE.len() * 4);
}
The crc32_table() function runs entirely during compilation. The resulting 1 KB table is baked into the binary’s read-only data section — no allocator, no initialization code, no startup cost. Compare this with a C approach that either uses a code generator or computes the table at startup. The Rust version is provably correct (the const _ assertions verify known values) and provably complete (the compiler will reject the program if the function fails to produce a valid table).
When to Use Const Fn Proofs
| Scenario | Recommendation |
|---|---|
| Memory maps, register offsets, partition tables | ✅ Always |
| Protocol frame layouts with fixed fields | ✅ Always |
| Bitfield masks within a register | ✅ Always |
| Clock tree / PLL parameter chains | ✅ Always |
| Lookup tables (CRC, trig, encoding) | ✅ Always — zero startup cost |
| Constants with cross-value invariants (non-overlap, sum ≤ bound) | ✅ Always |
| Configuration values with domain constraints | ✅ When values are known at compile time |
| Values computed from user input or files | ❌ Use runtime validation |
| Highly dynamic structures (trees, graphs) | ❌ Use property-based testing |
| Single-value range checks | ⚠️ Consider newtype + From instead (ch07) |
Cost Summary
| What | Runtime cost |
|---|---|
const fn assertions (assert!, panic!) | Compile time only — 0 instructions |
const _: () = { ... } validation blocks | Compile time only — not in binary |
Region, Register, Field structs | Plain data — same layout as raw integers |
Inline const { } generic validation | Monomorphised at compile time — 0 cost |
Lookup tables (crc32_table()) | Computed at compile time — placed in .rodata |
Phantom-typed access markers (TypedRegion<RW>) | Zero-sized — optimised away |
Every row is zero runtime cost — the proofs exist only during compilation. The resulting binary contains only the verified constants and lookup tables, with no assertion-checking code.
Exercise: Flash Partition Map
Design a verified flash partition map for a 1 MB NOR flash starting at 0x0800_0000. Requirements:
- Four partitions: bootloader (64 KB), application (640 KB), config (64 KB), OTA staging (256 KB)
- Every partition must be 4 KB aligned (flash erase granularity): both base and size must be multiples of 4096
- No partition may overlap another
- All partitions must fit within flash
- Add a
const fn total_used()that returns the sum of all partition sizes and assert it equals 1 MB
Solution
#[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, "partition size must be non-zero");
assert!(base % 4096 == 0, "partition base must be 4 KB aligned");
assert!(size % 4096 == 0, "partition size must be 4 KB aligned");
assert!(
base as u64 + size as u64 <= u32::MAX as u64,
"partition overflows address space"
);
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 exceeds flash");
assert!(total.contains(&app), "application exceeds flash");
assert!(total.contains(&config), "config exceeds flash");
assert!(total.contains(&ota), "OTA staging exceeds flash");
assert!(!boot.overlaps(&app), "boot/app overlap");
assert!(!boot.overlaps(&config), "boot/config overlap");
assert!(!boot.overlaps(&ota), "boot/ota overlap");
assert!(!app.overlaps(&config), "app/config overlap");
assert!(!app.overlaps(&ota), "app/ota overlap");
assert!(!config.overlaps(&ota), "config/ota overlap");
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 total
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
);
// Every byte of flash is accounted for
const _: () = assert!(
FLASH.total_used() == 1024 * 1024,
"partitions must exactly fill flash"
);
fn main() {
println!("Flash map: {} KB used / {} KB total",
FLASH.total_used() / 1024,
FLASH.total.size / 1024);
}
flowchart LR
subgraph compile["Compile Time — zero runtime cost"]
direction TB
RGN["Region::new()<br/>✅ size > 0<br/>✅ no overflow"]
MAP["SramMap::verified()<br/>✅ containment<br/>✅ non-overlap"]
ACC["TypedRegion<RW><br/>✅ access control"]
PROV["VerifiedAddr::new()<br/>✅ provenance"]
end
subgraph runtime["Runtime"]
HW["Hardware access<br/>No bounds checks<br/>No permission checks"]
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
Key Takeaways
-
const fn+assert!= compile-time proof obligation — if the assertion fails during const evaluation, the program does not compile. No test needed, no code review catch — the compiler proves it. -
Memory maps are ideal candidates — sub-region containment, overlap freedom, total-size bounds, and alignment constraints are all expressible as const fn assertions. The C
#defineapproach offers none of these guarantees. -
Phantom types layer on top — combine const fn (value verification) with phantom-typed access markers (permission verification) for defense in depth at zero runtime cost.
-
Provenance can be established at compile time —
VerifiedAddrproves at compile time that an address belongs to a specific region, eliminating runtime bounds checks on every access. -
The pattern generalizes beyond memory — register maps, bitfield masks, protocol frames, clock trees, DMA parameters — anywhere you have compile-time-known values with structural invariants.
-
Bitfields and clock trees are ideal candidates — intra-register bit disjointness and derived-value constraint chains (VCO range, divider limits) are exactly the kind of invariant that
const fnproves effortlessly. -
const fnreplaces code generators and build scripts for lookup tables — CRC tables, trigonometry, encoding maps — computed at compile time, placed in.rodata, with zero startup cost and no external tooling. -
Inline
const { }blocks validate generic parameters — since Rust 1.79, you can enforce constraints on const generics at the call site, catching misuse before any code runs.
Send & Sync — Compile-Time Concurrency Proofs 🟠
What you’ll learn: How Rust’s
SendandSyncauto-traits turn the compiler into a concurrency auditor — proving at compile time which types can cross thread boundaries and which can be shared, with zero runtime cost.Cross-references: ch04 (capability tokens), ch09 (phantom types), ch15 (const fn proofs)
The Problem: Concurrent Access Without a Safety Net
In systems programming, peripherals, shared buffers, and global state are accessed from multiple contexts — main loops, interrupt handlers, DMA callbacks, and worker threads. In C, the compiler offers no enforcement whatsoever:
/* Shared sensor buffer — accessed from main loop and ISR */
volatile uint32_t sensor_buf[64];
volatile uint32_t buf_index = 0;
void SENSOR_IRQHandler(void) {
sensor_buf[buf_index++] = read_sensor(); /* Race: buf_index read + write */
}
void process_sensors(void) {
for (uint32_t i = 0; i < buf_index; i++) { /* buf_index changes mid-loop */
process(sensor_buf[i]); /* Data overwritten mid-read */
}
buf_index = 0; /* ISR fires between these lines */
}
The volatile keyword prevents the compiler from optimizing away the reads, but it does nothing about data races. Two contexts can read and write buf_index simultaneously, producing torn values, lost updates, or buffer overruns. The same problem appears with pthread_mutex_t — the compiler will happily let you forget to lock:
pthread_mutex_t lock;
int shared_counter;
void increment(void) {
shared_counter++; /* Oops — forgot pthread_mutex_lock(&lock) */
}
Every concurrent bug is discovered at runtime — typically under load, in production, and intermittently.
What Send and Sync Prove
Rust defines two marker traits that the compiler derives automatically:
| Trait | Proof | Informal meaning |
|---|---|---|
Send | A value of type T can be safely moved to another thread | “This can cross a thread boundary” |
Sync | A shared reference &T can be safely used by multiple threads | “This can be read from multiple threads” |
These are auto-traits — the compiler derives them by inspecting every field. A struct is Send if all its fields are Send. A struct is Sync if all its fields are Sync. If any field opts out, the entire struct opts out. No annotation needed, no runtime overhead — the proof is structural.
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
The compiler is the auditor. In C, thread-safety annotations live in comments and header documentation — advisory, never enforced. In Rust,
SendandSyncare derived from the structure of the type itself. Adding a singleCell<f32>field automatically makes the containing struct!Sync. No programmer action required, no way to forget.
The two traits are linked by a key identity:
TisSyncif and only if&TisSend.
This makes intuitive sense: if a shared reference can be safely sent to another thread, then the underlying type is safe for concurrent reads.
Types That Opt Out
Certain types are deliberately !Send or !Sync:
| Type | Send | Sync | Why |
|---|---|---|---|
u32, String, Vec<T> | ✅ | ✅ | No interior mutability, no raw pointers |
Cell<T>, RefCell<T> | ✅ | ❌ | Interior mutability without synchronization |
Rc<T> | ❌ | ❌ | Reference count is not atomic |
*const T, *mut T | ❌ | ❌ | Raw pointers have no safety guarantees |
Arc<T> (where T: Send + Sync) | ✅ | ✅ | Atomic reference count |
Mutex<T> (where T: Send) | ✅ | ✅ | Lock serializes all access |
Every ❌ in this table is a compile-time invariant. You cannot accidentally send an Rc to another thread — the compiler rejects it.
!Send Peripheral Handles
In embedded systems, a peripheral register block lives at a fixed memory address and should only be accessed from a single execution context. Raw pointers are inherently !Send and !Sync, so wrapping one automatically opts the containing type out of both traits:
/// A handle to a memory-mapped UART peripheral.
/// The raw pointer makes this automatically !Send and !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) {
// In real firmware: 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'); // ✅ Use on the creating thread
// ❌ Would not compile: Uart is !Send
// std::thread::spawn(move || {
// uart.write_byte(b'B');
// });
}
The commented-out thread::spawn would produce:
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`
No raw pointer? Use PhantomData. Sometimes a type has no raw pointer but should still be confined to one thread — for example, a file descriptor index or a handle obtained from a C library:
use std::marker::PhantomData;
/// An opaque handle from a C library. PhantomData<*const ()> makes it
/// !Send + !Sync even though the inner fd is just a plain integer.
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());
// ❌ Would not compile: LibHandle is !Send
// std::thread::spawn(move || { let _ = handle.fd(); });
}
This is the compile-time equivalent of C’s “please read the documentation that says this handle isn’t thread-safe.” In Rust, the compiler enforces it.
Mutex Transforms !Sync into Sync
Cell<T> and RefCell<T> provide interior mutability without any synchronization — so they’re !Sync. But sometimes you genuinely need to share mutable state across threads. Mutex<T> adds the missing synchronization, and the compiler recognizes this:
If
T: Send, thenMutex<T>: Send + Sync.
The lock serializes all access, so the !Sync inner type becomes safe to share. The compiler proves this structurally — no runtime check for “did the programmer remember to lock”:
use std::sync::{Arc, Mutex};
use std::cell::Cell;
/// A sensor cache using Cell for interior mutability.
/// Cell<u32> is !Sync — can't be shared across threads directly.
struct SensorCache {
last_reading: Cell<u32>,
reading_count: Cell<u32>,
}
fn main() {
// Mutex makes SensorCache safe to share — compiler proves it
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(); // Must lock before access
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());
}
Compare to the C version: pthread_mutex_lock is a runtime call that the programmer can forget. Here, the type system makes it impossible to access SensorCache without going through the Mutex. The proof is structural — the only runtime cost is the lock itself.
Mutexdoesn’t just synchronize — it proves synchronization.Mutex::lock()returns aMutexGuardthatDerefs to&T. There is no way to obtain a reference to the inner data without going through the lock. The API makes “forgot to lock” structurally unrepresentable.
Function Bounds as Theorems
std::thread::spawn has this signature:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
The Send + 'static bound isn’t just an implementation detail — it’s a theorem:
“Any closure and return value passed to
spawnis proven at compile time to be safe to run on another thread, with no dangling references.”
You can apply the same pattern to your own APIs:
use std::sync::mpsc;
/// Run a task on a background thread and return its result.
/// The bounds prove: the closure and its result are thread-safe.
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 is Send, closure captures nothing non-Send
let result = run_on_background(|| 6 * 7);
println!("Result: {result}");
// ✅ String is Send
let greeting = run_on_background(|| String::from("hello from background"));
println!("{greeting}");
// ❌ Would not compile: Rc is !Send
// use std::rc::Rc;
// let data = Rc::new(42);
// run_on_background(move || *data);
}
Uncommenting the Rc example produces a precise diagnostic:
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
The compiler traces the violation back to the exact bound — and tells the programmer why. Compare to C’s pthread_create:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
The void *arg accepts anything — thread-safe or not. The C compiler can’t distinguish a non-atomic refcount from a plain integer. Rust’s trait bounds make the distinction at the type level.
When to Use Send/Sync Proofs
| Scenario | Approach |
|---|---|
| Peripheral handle wrapping a raw pointer | Automatic !Send + !Sync — nothing to do |
| Handle from C library (integer fd/handle) | Add PhantomData<*const ()> for !Send + !Sync |
| Shared config behind a lock | Arc<Mutex<T>> — compiler proves access is safe |
| Cross-thread message passing | mpsc::channel — Send bound enforced automatically |
| Task spawner or thread pool API | Require F: Send + 'static in signature |
| Single-threaded resource (e.g., GPU context) | PhantomData<*const ()> to prevent sharing |
Type should be Send but contains a raw pointer | unsafe impl Send with documented safety justification |
Cost Summary
| What | Runtime cost |
|---|---|
Send / Sync auto-derivation | Compile time only — 0 bytes |
PhantomData<*const ()> field | Zero-sized — optimised away |
!Send / !Sync enforcement | Compile time only — no runtime check |
F: Send + 'static function bounds | Monomorphised — static dispatch, no boxing |
Mutex<T> lock | Runtime lock (unavoidable for shared mutation) |
Arc<T> reference counting | Atomic increment/decrement (unavoidable for shared ownership) |
The first four rows are zero-cost — they exist only in the type system and vanish after compilation. Mutex and Arc carry unavoidable runtime costs, but those costs are the minimum any correct concurrent program must pay — Rust just makes sure you pay them.
Exercise: DMA Transfer Guard
Design a DmaTransfer<T> that holds a buffer while a DMA transfer is in flight. Requirements:
DmaTransfermust be!Send— the DMA controller uses physical addresses tied to this core’s memory busDmaTransfermust be!Sync— concurrent reads while DMA is writing would see torn data- Provide a
wait()method that consumes the guard and returns the buffer — ownership proves the transfer is complete - The buffer type
Tmust implement aDmaSafemarker trait
Solution
use std::marker::PhantomData;
/// Marker trait for types that can be used as DMA buffers.
/// In real firmware: type must be repr(C) with no padding.
trait DmaSafe {}
impl DmaSafe for [u8; 64] {}
impl DmaSafe for [u8; 256] {}
/// A guard representing an in-flight DMA transfer.
/// !Send + !Sync: can't be sent to another thread or shared.
pub struct DmaTransfer<T: DmaSafe> {
buffer: T,
channel: u8,
_no_send_sync: PhantomData<*const ()>,
}
impl<T: DmaSafe> DmaTransfer<T> {
/// Start a DMA transfer. The buffer is consumed — no one else can touch it.
pub fn start(buffer: T, channel: u8) -> Self {
// In real firmware: configure DMA channel, set source/dest, start transfer
println!("DMA channel {} started", channel);
Self {
buffer,
channel,
_no_send_sync: PhantomData,
}
}
/// Wait for the transfer to complete and return the buffer.
/// Consumes self — the guard no longer exists after this.
pub fn wait(self) -> T {
// In real firmware: poll DMA status register until complete
println!("DMA channel {} complete", self.channel);
self.buffer
}
}
fn main() {
let buf = [0u8; 64];
// Start transfer — buf is moved into the guard
let transfer = DmaTransfer::start(buf, 2);
// ❌ buf is no longer accessible — ownership prevents use-during-DMA
// println!("{:?}", buf);
// ❌ Would not compile: DmaTransfer is !Send
// std::thread::spawn(move || { transfer.wait(); });
// ✅ Wait on the original thread, get the buffer back
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
Key Takeaways
-
SendandSyncare compile-time proofs about concurrency safety — the compiler derives them structurally by inspecting every field. No annotation, no runtime cost, no opt-in needed. -
Raw pointers automatically opt out — any type containing
*const Tor*mut Tbecomes!Send + !Sync. This makes peripheral handles naturally thread-confined. -
PhantomData<*const ()>is the explicit opt-out — when a type has no raw pointer but should still be thread-confined (C library handles, GPU contexts), a phantom field does the job. -
Mutex<T>restoresSyncwith proof — the compiler structurally proves that all access goes through the lock. Unlike C’spthread_mutex_t, you cannot forget to lock. -
Function bounds are theorems —
F: Send + 'staticin a spawner’s signature is a compile-time proof obligation: every call site must prove its closure is thread-safe. Compare to C’svoid *argwhich accepts anything. -
The pattern complements all other correctness techniques — typestate proves protocol sequencing, phantom types prove permissions,
const fnproves value invariants, andSend/Syncprove concurrency safety. Together they cover the full correctness surface.
Applied Walkthrough — Type-Safe Redfish Client 🟡
What you’ll learn: How to compose type-state sessions, capability tokens, phantom-typed resource navigation, dimensional analysis, validated boundaries, builder type-state, and single-use types into a complete, zero-overhead Redfish client — where every protocol violation is a compile error.
Cross-references: ch02 (typed commands), ch03 (single-use types), ch04 (capability tokens), ch05 (type-state), ch06 (dimensional types), ch07 (validated boundaries), ch09 (phantom types), ch10 (IPMI integration), ch11 (trick 4 — builder type-state)
Why Redfish Deserves Its Own Chapter
Chapter 10 composes the core patterns around IPMI — a byte-level protocol. But most BMC platforms now expose a Redfish REST API alongside (or instead of) IPMI, and Redfish introduces its own category of correctness hazards:
| Hazard | Example | Consequence |
|---|---|---|
| Malformed URI | GET /redfish/v1/Chassis/1/Processors (wrong parent) | 404 or wrong data silently returned |
| Action on wrong power state | Reset(ForceOff) on an already-off system | BMC returns error, or worse, races with another operation |
| Missing privilege | Operator-level code calls Manager.ResetToDefaults | 403 in production, security audit finding |
| Incomplete PATCH | Omit a required BIOS attribute from a PATCH body | Silent no-op or partial config corruption |
| Unverified firmware apply | SimpleUpdate invoked before image integrity check | Bricked BMC |
| Schema version mismatch | Access LastResetTime on a v1.5 BMC (added in v1.13) | null field → runtime panic |
| Unit confusion in telemetry | Compare inlet temperature (°C) to power draw (W) | Nonsensical threshold decisions |
In C, Python, or untyped Rust, every one of these is prevented by discipline and testing alone. This chapter makes them compile errors.
The Untyped Redfish Client
A typical Redfish client looks like this:
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!({})) // stub
}
fn patch(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
// ... HTTP PATCH ...
Ok(()) // stub
}
fn post_action(&self, path: &str, body: &serde_json::Value) -> Result<(), String> {
// ... HTTP POST ...
Ok(()) // stub
}
}
fn check_thermal(client: &RedfishClient) -> Result<(), String> {
let resp = client.get("/redfish/v1/Chassis/1/Thermal")?;
// 🐛 Is this field always present? What if the BMC returns null?
let cpu_temp = resp["Temperatures"][0]["ReadingCelsius"]
.as_f64().unwrap();
let fan_rpm = resp["Fans"][0]["Reading"]
.as_f64().unwrap();
// 🐛 Comparing °C to RPM — both are f64
if cpu_temp > fan_rpm {
println!("thermal issue");
}
// 🐛 Is this the right path? No compile-time check.
client.post_action(
"/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
&serde_json::json!({"ResetType": "ForceOff"})
)?;
Ok(())
}
This “works” — until it doesn’t. Every unwrap() is a potential panic, every
string path is an unchecked assumption, and unit confusion is invisible.
Section 1 — Session Lifecycle (Type-State, ch05)
A Redfish session has a strict lifecycle: connect → authenticate → use → close. Encode each state as a distinct type.
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 calls only exist here
note right of Connected : get() → compile error
use std::marker::PhantomData;
// ──── Session States ────
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,
}
}
/// Transition: Disconnected → Connected.
/// Verifies the service root is reachable.
pub fn connect(self) -> Result<RedfishSession<Connected>, RedfishError> {
// GET /redfish/v1 — verify service root
println!("Connecting to {}/redfish/v1", self.base_url);
Ok(RedfishSession {
base_url: self.base_url,
auth_token: None,
_state: PhantomData,
})
}
}
impl RedfishSession<Connected> {
/// Transition: Connected → Authenticated.
/// Creates a session via 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> {
/// Only available on Authenticated sessions.
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!({})) // stub
}
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!({})) // stub
}
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!({})) // stub
}
/// Transition: Authenticated → Closed (session consumed).
pub fn logout(self) {
// DELETE /redfish/v1/SessionService/Sessions/{id}
println!("Session closed");
// self is consumed — can't use the session after logout
}
}
// Attempting to call http_get on a non-Authenticated session:
//
// let session = RedfishSession::new("bmc01").connect()?;
// session.http_get("/redfish/v1/Systems");
// ❌ ERROR: 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 class eliminated: sending requests on a disconnected or unauthenticated session. The method simply doesn’t exist — no runtime check to forget.
Section 2 — Privilege Tokens (Capability Tokens, ch04)
Redfish defines four privilege levels: Login, ConfigureComponents,
ConfigureManager, ConfigureSelf. Rather than checking permissions at
runtime, encode them as zero-sized proof tokens.
// ──── Privilege Tokens (zero-sized) ────
/// Proof the caller has Login privilege.
/// Returned by successful login — the only way to obtain one.
pub struct LoginToken { _private: () }
/// Proof the caller has ConfigureComponents privilege.
/// Only obtainable by admin-level authentication.
pub struct ConfigureComponentsToken { _private: () }
/// Proof the caller has ConfigureManager privilege (firmware updates, etc.).
pub struct ConfigureManagerToken { _private: () }
// Extend login to return privilege tokens based on role:
impl RedfishSession<Connected> {
/// Admin login — returns all privilege tokens.
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: () },
))
}
/// Operator login — returns Login + ConfigureComponents only.
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: () },
))
}
/// Read-only login — returns Login token only.
pub fn login_readonly(
self,
user: &str,
pass: &str,
) -> Result<(RedfishSession<Authenticated>, LoginToken), RedfishError> {
self.login(user, pass)
}
}
Now privilege requirements are part of the function signature:
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 } }
/// Anyone with Login can read thermal data.
fn get_thermal(
session: &RedfishSession<Authenticated>,
_proof: &LoginToken,
) -> Result<serde_json::Value, RedfishError> {
// GET /redfish/v1/Chassis/1/Thermal
Ok(serde_json::json!({})) // stub
}
/// Changing boot order requires ConfigureComponents.
fn set_boot_order(
session: &RedfishSession<Authenticated>,
_proof: &ConfigureComponentsToken,
order: &[&str],
) -> Result<(), RedfishError> {
let _ = order;
// PATCH /redfish/v1/Systems/1
Ok(())
}
/// Factory reset requires ConfigureManager.
fn reset_to_defaults(
session: &RedfishSession<Authenticated>,
_proof: &ConfigureManagerToken,
) -> Result<(), RedfishError> {
// POST .../Actions/Manager.ResetToDefaults
Ok(())
}
// Operator code calling reset_to_defaults:
//
// let (session, login, configure) = session.login_operator("op", "pass")?;
// reset_to_defaults(&session, &???);
// ❌ ERROR: no ConfigureManagerToken available — operator can't do this
Bug class eliminated: privilege escalation. An operator-level login physically
cannot produce a ConfigureManagerToken — the compiler won’t let the code reference
one. Zero runtime cost: for the compiled binary, these tokens don’t exist.
Section 3 — Typed Resource Navigation (Phantom Types, ch09)
Redfish resources form a tree. Encoding the hierarchy as types prevents constructing illegal URIs:
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;
// ──── Resource Type Markers ────
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;
// ──── Typed Resource Path ────
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
}
}
// ── Usage ──
fn build_paths() {
let root = RedfishPath::root();
// ✅ Valid navigation
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");
// ❌ Compile error: ServiceRoot has no .thermal() method
// root.thermal();
// ❌ Compile error: SystemsCollection has no .bios() method
// root.systems().bios();
// ❌ Compile error: ChassisInstance has no .bios() method
// root.chassis().instance("1").bios();
}
Bug class eliminated: malformed URIs, navigating to a child resource that
doesn’t exist under the given parent. The hierarchy is enforced structurally —
you can only reach Thermal through Chassis → Instance → Thermal.
Section 4 — Typed Telemetry Reads (Typed Commands + Dimensional Analysis, ch02 + ch06)
Combine typed resource paths with dimensional return types so the compiler knows what unit every reading carries:
use std::marker::PhantomData;
// ──── Dimensional Types (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);
// ──── Typed Redfish GET (ch02 pattern applied to REST) ────
/// A Redfish resource type determines its parsed response.
pub trait RedfishResource {
type Response;
fn parse(json: &serde_json::Value) -> Result<Self::Response, RedfishError>;
}
// ──── Validated Thermal Response (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, // ← dimensional type, not f64
pub upper_critical: Celsius,
pub status: HealthStatus,
}
#[derive(Debug)]
pub struct FanReading {
pub name: String,
pub reading: Rpm, // ← dimensional type, not 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> {
// Parse and validate in one pass — boundary validation (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), // safe default for missing threshold
),
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,
}
}
// ──── Typed GET on Authenticated Session ────
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)
}
}
// ── Usage ──
fn read_thermal(
session: &RedfishSession<Authenticated>,
_proof: &LoginToken,
) -> Result<(), RedfishError> {
let path = RedfishPath::root().chassis().instance("1").thermal();
// Response type is inferred: ValidThermalResponse
let thermal = session.get_resource(&path)?;
for t in &thermal.temperatures {
// t.reading is Celsius — can only compare with Celsius
if t.reading > t.upper_critical {
println!("CRITICAL: {} at {:?}", t.name, t.reading);
}
// ❌ Compile error: cannot compare Celsius with Rpm
// if t.reading > thermal.fans[0].reading { }
// ❌ Compile error: cannot compare Celsius with Watts
// if t.reading > Watts(350.0) { }
}
Ok(())
}
Bug classes eliminated:
- Unit confusion:
Celsius≠Rpm≠Watts— the compiler rejects comparisons. - Missing field panics:
parse()validates at the boundary;ValidThermalResponseguarantees all fields are present. - Wrong response type:
get_resource(&thermal_path)returnsValidThermalResponse, not raw JSON. The resource type determines the response type at compile time.
Section 5 — PATCH with Builder Type-State (ch11, Trick 4)
Redfish PATCH payloads must contain specific fields. A builder that gates
.apply() on required fields being set prevents incomplete or empty patches:
use std::marker::PhantomData;
// ──── Type-level booleans for required fields ────
pub struct FieldUnset;
pub struct FieldSet;
// ──── BIOS Settings PATCH Builder ────
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> {
/// Set boot order — transitions the BootOrder marker to 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> {
/// Set TPM state — transitions the TpmState marker to 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() only exists when ALL required fields are set.
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(())
}
}
// ── Usage ──
fn configure_bios(
session: &RedfishSession<Authenticated>,
configure: &ConfigureComponentsToken,
) -> Result<(), RedfishError> {
let system = RedfishPath::root().systems().system("1");
// ✅ Both required fields set — .apply() is available
BiosPatchBuilder::new()
.boot_order(vec!["Pxe".into(), "Hdd".into()])
.tpm_enabled(true)
.apply(session, configure, &system)?;
// ❌ Compile error: .apply() not found on BiosPatchBuilder<FieldSet, FieldUnset>
// BiosPatchBuilder::new()
// .boot_order(vec!["Pxe".into()])
// .apply(session, configure, &system)?;
// ❌ Compile error: .apply() not found on BiosPatchBuilder<FieldUnset, FieldUnset>
// BiosPatchBuilder::new()
// .apply(session, configure, &system)?;
Ok(())
}
Bug classes eliminated:
- Empty PATCH: Can’t call
.apply()without setting every required field. - Missing privilege:
.apply()requires&ConfigureComponentsToken. - Wrong resource: Takes a
&RedfishPath<ComputerSystem>, not a raw string.
Section 6 — Firmware Update Lifecycle (Single-Use + Type-State, ch03 + ch05)
The Redfish UpdateService has a strict sequence: push image → verify →
apply → reboot. Each phase must happen exactly once, in order.
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() consumes this state —
note right of Verified : can't apply twice
use std::marker::PhantomData;
// ──── Firmware Update States ────
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
// or multipart push to /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> {
/// Verify image integrity. Returns FwVerified on success.
pub fn verify(self) -> Result<FirmwareUpdate<FwVerified>, RedfishError> {
// Poll task until verification complete
println!("Image verified: {}", self.image_hash);
Ok(FirmwareUpdate {
task_uri: self.task_uri,
image_hash: self.image_hash,
_phase: PhantomData,
})
}
}
impl FirmwareUpdate<FwVerified> {
/// Apply the update. Consumes self — can't apply twice.
/// This is the single-use pattern from ch03.
pub fn apply(self) -> Result<FirmwareUpdate<FwNeedsReboot>, RedfishError> {
// PATCH /redfish/v1/UpdateService — set ApplyTime
println!("Firmware applied from {}", self.task_uri);
// self is moved — calling apply() again is a compile error
Ok(FirmwareUpdate {
task_uri: self.task_uri,
image_hash: self.image_hash,
_phase: PhantomData,
})
}
}
impl FirmwareUpdate<FwNeedsReboot> {
/// Reboot to activate the new firmware.
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(())
}
}
// ── Usage ──
fn update_bmc_firmware(
session: &RedfishSession<Authenticated>,
manager_proof: &ConfigureManagerToken,
image: &[u8],
) -> Result<(), RedfishError> {
// Each step returns the next state — the old state is consumed
let uploaded = FirmwareUpdate::push_image(session, manager_proof, image)?;
let verified = uploaded.verify()?;
let needs_reboot = verified.apply()?;
needs_reboot.reboot(session, manager_proof)?;
// ❌ Compile error: use of moved value `verified`
// verified.apply()?;
// ❌ Compile error: FirmwareUpdate<FwUploaded> has no .apply() method
// uploaded.apply()?; // must verify first!
// ❌ Compile error: push_image requires &ConfigureManagerToken
// FirmwareUpdate::push_image(session, &login_token, image)?;
Ok(())
}
Bug classes eliminated:
- Applying unverified firmware:
.apply()only exists onFwVerified. - Double apply:
apply()consumesself— moved value can’t be reused. - Skipping reboot:
FwNeedsRebootis a distinct type; you can’t accidentally continue normal operations while firmware is staged. - Unauthorized update:
push_image()requires&ConfigureManagerToken.
Section 7 — Putting It All Together
Here’s the full diagnostic workflow composing all six sections:
fn full_redfish_diagnostic() -> Result<(), RedfishError> {
// ── 1. Session lifecycle (Section 1) ──
let session = RedfishSession::new("bmc01.lab.local");
let session = session.connect()?;
// ── 2. Privilege tokens (Section 2) ──
// Admin login — receives all capability tokens
let (session, _login, configure, manager) =
session.login_admin("admin", "p@ssw0rd")?;
// ── 3. Typed navigation (Section 3) ──
let thermal_path = RedfishPath::root()
.chassis()
.instance("1")
.thermal();
// ── 4. Typed telemetry read (Section 4) ──
let thermal: ValidThermalResponse = session.get_resource(&thermal_path)?;
for t in &thermal.temperatures {
// Celsius can only compare with Celsius — dimensional safety
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. Type-safe PATCH (Section 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. Firmware update lifecycle (Section 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. Clean shutdown ──
needs_reboot.reboot(&session, &manager)?;
session.logout();
Ok(())
}
What the Compiler Proves
| # | Bug class | How it’s prevented | Pattern (Section) |
|---|---|---|---|
| 1 | Request on unauthenticated session | http_get() only exists on Session<Authenticated> | Type-state (§1) |
| 2 | Privilege escalation | ConfigureManagerToken not returned by operator login | Capability tokens (§2) |
| 3 | Malformed Redfish URI | Navigation methods enforce parent→child hierarchy | Phantom types (§3) |
| 4 | Unit confusion (°C vs RPM vs W) | Celsius, Rpm, Watts are distinct types | Dimensional analysis (§4) |
| 5 | Missing JSON field → panic | ValidThermalResponse validates at parse boundary | Validated boundaries (§4) |
| 6 | Wrong response type | RedfishResource::Response is fixed per resource | Typed commands (§4) |
| 7 | Incomplete PATCH payload | .apply() only exists when all fields are FieldSet | Builder type-state (§5) |
| 8 | Missing privilege for PATCH | .apply() requires &ConfigureComponentsToken | Capability tokens (§5) |
| 9 | Applying unverified firmware | .apply() only exists on FwVerified | Type-state (§6) |
| 10 | Double firmware apply | apply() consumes self — value is moved | Single-use types (§6) |
| 11 | Firmware update without authority | push_image() requires &ConfigureManagerToken | Capability tokens (§6) |
| 12 | Use-after-logout | logout() consumes the session | Ownership (§1) |
Total runtime overhead of ALL twelve guarantees: zero.
The generated binary makes the same HTTP calls as the untyped version — but the untyped version can have 12 classes of bugs. This version can’t.
Comparison: IPMI Integration (ch10) vs. Redfish Integration
| Dimension | ch10 (IPMI) | This chapter (Redfish) |
|---|---|---|
| Transport | Raw bytes over KCS/LAN | JSON over HTTPS |
| Navigation | Flat command codes (NetFn/Cmd) | Hierarchical URI tree |
| Response binding | IpmiCmd::Response | RedfishResource::Response |
| Privilege model | Single AdminToken | Role-based multi-token |
| Payload construction | Byte arrays | Builder type-state for JSON |
| Update lifecycle | Not covered | Full type-state chain |
| Patterns exercised | 7 | 8 (adds builder type-state) |
The two chapters are complementary: ch10 shows the patterns work at the byte level, this chapter shows they work identically at the REST/JSON level. The type system doesn’t care about the transport — it proves correctness either way.
Key Takeaways
- Eight patterns compose into one Redfish client — session type-state, capability tokens, phantom-typed URIs, typed commands, dimensional analysis, validated boundaries, builder type-state, and single-use firmware apply.
- Twelve bug classes become compile errors — see the table above.
- Zero runtime overhead — every proof token, phantom type, and type-state marker compiles away. The binary is identical to hand-rolled untyped code.
- REST APIs benefit as much as byte protocols — the patterns from ch02–ch09 apply equally to JSON-over-HTTPS (Redfish) and bytes-over-KCS (IPMI).
- Privilege enforcement is structural, not procedural — the function signature declares what’s required; the compiler enforces it.
- This is a design template — adapt the resource type markers, capability tokens, and builder for your specific Redfish schema and organizational role hierarchy.
Applied Walkthrough — Type-Safe Redfish Server 🟡
What you’ll learn: How to compose response builder type-state, source-availability tokens, dimensional serialization, health rollup, schema versioning, and typed action dispatch into a Redfish server that cannot produce a schema-non-compliant response — the mirror of the client walkthrough in ch17.
Cross-references: ch02 (typed commands — inverted for action dispatch), ch04 (capability tokens — source availability), ch06 (dimensional types — serialization side), ch07 (validated boundaries — inverted: “construct, don’t serialize”), ch09 (phantom types — schema versioning), ch11 (trick 3 —
#[non_exhaustive], trick 4 — builder type-state), ch17 (client counterpart)
The Mirror Problem
Chapter 17 asks: “How do I consume Redfish correctly?” This chapter asks the mirror question: “How do I produce Redfish correctly?”
On the client side, the danger is trusting bad data. On the server side, the danger is emitting bad data — and every client in the fleet trusts what you send.
A single GET /redfish/v1/Systems/1 response must fuse data from many sources:
flowchart LR
subgraph Sources
SMBIOS["SMBIOS\nType 1, Type 17"]
SDR["IPMI Sensors\n(SDR + readings)"]
SEL["IPMI SEL\n(critical events)"]
PCIe["PCIe Config\nSpace"]
FW["Firmware\nVersion Table"]
PWR["Power State\nRegister"]
end
subgraph Server["Redfish Server"]
Handler["GET handler"]
Builder["ComputerSystem\nBuilder"]
end
SMBIOS -->|"Name, UUID, Serial"| Handler
SDR -->|"Temperatures, Fans"| Handler
SEL -->|"Health escalation"| Handler
PCIe -->|"Device links"| Handler
FW -->|"BIOS version"| Handler
PWR -->|"PowerState"| Handler
Handler --> Builder
Builder -->|".build()"| JSON["Schema-compliant\nJSON response"]
style JSON fill:#c8e6c9,color:#000
style Builder fill:#e1f5fe,color:#000
In C, this is a 500-line handler that calls into six subsystems, manually builds
a JSON tree with json_object_set(), and hopes every required field was populated.
Forget one? The response violates the Redfish schema. Get the unit wrong? Every
client sees corrupted telemetry.
// C — the assembly problem
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"));
// 🐛 Forgot to set "Name" — schema requires it
// 🐛 Forgot to set "UUID" — schema requires it
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())); // at least this one is always available
// 🐛 Reading is in raw ADC counts, not Celsius — no type to catch it
double cpu_temp = read_sensor(SENSOR_CPU_TEMP);
// This number ends up in a Thermal response somewhere else...
// but nothing ties it to "Celsius" at the type level
// 🐛 Health is manually computed — forgot to include PSU status
json_object_set_new(obj, "Status",
build_status("Enabled", "OK")); // should be "Critical" — PSU is failing
return obj; // missing 2 required fields, wrong health, raw units
}
Four bugs in one handler. On the client side, each bug affects one client. On the server side, each bug affects every client that queries this BMC.
Section 1 — Response Builder Type-State: “Construct, Don’t Serialize” (ch07 Inverted)
Chapter 7 teaches “parse, don’t validate” — validate inbound data once, carry the
proof in a type. The server-side mirror is “construct, don’t serialize” — build
the outbound response through a builder that gates .build() on all required fields
being present.
use std::marker::PhantomData;
// ──── Type-level field tracking ────
pub struct HasField;
pub struct MissingField;
// ──── Response Builder ────
/// Builder for a ComputerSystem Redfish resource.
/// Type parameters track which REQUIRED fields have been supplied.
/// Optional fields don't need type-level tracking.
pub struct ComputerSystemBuilder<Name, Uuid, PowerState, Status> {
// Required fields — tracked at the type level
name: Option<String>,
uuid: Option<String>,
power_state: Option<PowerStateValue>,
status: Option<ResourceStatus>,
// Optional fields — not tracked (always settable)
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,
}
// ──── Constructor: all fields start 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,
}
}
}
// ──── Required field setters — each transitions one type parameter ────
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,
}
}
}
// ──── Optional field setters — available in any state ────
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() ONLY exists when all required fields are 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 guarantees these are Some — .unwrap() is safe here.
// In production, prefer .expect("guaranteed by type state").
"Name": self.name.unwrap(),
"UUID": self.uuid.unwrap(),
"PowerState": self.power_state.unwrap(),
"Status": self.status.unwrap(),
});
// Optional fields — included only if present
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);
}
// NOTE: .unwrap() on to_value() is used for brevity.
// Production code should propagate serialization errors with `?`.
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
}
}
//
// ── The Compiler Enforces Completeness ──
//
// ✅ All required fields set — .build() is available:
// ComputerSystemBuilder::new()
// .name("PowerEdge R750".into())
// .uuid("4c4c4544-...".into())
// .power_state(PowerStateValue::On)
// .status(ResourceStatus { ... })
// .manufacturer("Dell".into()) // optional — fine to include
// .build("1")
//
// ❌ Missing "Name" — compile error:
// ComputerSystemBuilder::new()
// .uuid("4c4c4544-...".into())
// .power_state(PowerStateValue::On)
// .status(ResourceStatus { ... })
// .build("1")
// ERROR: method `build` not found for
// `ComputerSystemBuilder<MissingField, HasField, HasField, HasField>`
Bug class eliminated: schema-non-compliant responses. The handler physically
cannot serialize a ComputerSystem without supplying every required field. The
compiler error message even tells you which field is missing — it’s right there
in the type parameter: MissingField in the Name position.
Section 2 — Source-Availability Tokens (Capability Tokens, ch04 — New Twist)
In ch04 and ch17, capability tokens prove authorization — “the caller is allowed to do this.” On the server side, the same pattern proves availability — “this data source was successfully initialized.”
Each subsystem the BMC queries can fail independently. SMBIOS tables might be corrupt. The sensor subsystem might still be initializing. PCIe bus scan might have timed out. Encode each as a proof token:
/// Proof that SMBIOS tables were successfully parsed.
/// Only produced by the SMBIOS init function.
pub struct SmbiosReady {
_private: (),
}
/// Proof that IPMI sensor subsystem is responsive.
pub struct SensorsReady {
_private: (),
}
/// Proof that PCIe bus scan completed.
pub struct PcieReady {
_private: (),
}
/// Proof that the SEL was successfully read.
pub struct SelReady {
_private: (),
}
// ──── Data source initialization ────
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)>,
}
/// Rich SEL summary — per-subsystem health derived from typed events.
/// Built by the consumer pipeline in ch07's SEL section.
/// Replaces the lossy `has_critical_events: bool` with typed granularity.
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)> {
// Read SMBIOS entry point, parse tables...
// Returns None if tables are absent or corrupt
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)> {
// Initialize SDR repository, read all sensors...
// Returns None if IPMI subsystem is not responsive
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)> {
// In production: read SEL entries, parse via ch07's TryFrom,
// classify via classify_event_health(), aggregate via 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,
},
))
}
Now, functions that populate builder fields from a data source require the corresponding proof token:
/// Populate SMBIOS-sourced fields. Requires proof SMBIOS is available.
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())
}
/// Fallback when SMBIOS is unavailable — supplies required fields
/// with safe defaults.
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())
}
The handler chooses the path based on which tokens are available:
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),
};
// Both paths produce HasField for Name and UUID.
// .build() is available either way.
builder.build("1")
}
Bug class eliminated: calling into a subsystem that failed initialization.
If SMBIOS didn’t parse, you don’t have a SmbiosReady token — the compiler forces
you through the fallback path. No runtime if (smbios != NULL) to forget.
Combining Source Tokens with Capability Mixins (ch08)
With multiple Redfish resource types to serve (ComputerSystem, Chassis, Manager, Thermal, Power), source-population logic repeats across handlers. The mixin pattern from ch08 eliminates this duplication. Declare what sources a handler has, and blanket impls provide the population methods automatically:
/// ── Ingredient Traits (ch08) for data sources ──
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: any handler with SMBIOS + Sensors gets identity population ──
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())
}
}
/// Auto-implement for any type that has SMBIOS capability.
impl<T: HasSmbios> IdentityMixin for T {}
/// ── Mixin: any handler with Sensors + SEL gets health rollup ──
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 {}
/// ── Concrete handler owns available sources ──
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 automatically gets:
// IdentityMixin::populate_identity() (via HasSmbios)
// HealthMixin::compute_health() (via HasSensors + HasSel)
//
// A SensorsOnlyHandler that impls HasSensors but NOT HasSel
// would get IdentityMixin (if it has SMBIOS) but NOT HealthMixin.
// Calling .compute_health() on it → compile error.
This directly mirrors ch08’s BaseBoardController pattern: ingredient traits
declare what you have, mixin traits provide behavior via blanket impls, and
the compiler gates each mixin on its prerequisites. Adding a new data
source (e.g., HasNvme) plus a mixin (e.g., StorageMixin: HasNvme + HasSel)
gives health rollup for storage to every handler that has both — automatically.
Section 3 — Dimensional Types at the Serialization Boundary (ch06)
On the client side (ch17 §4), dimensional types prevent reading °C as RPM. On the server side, they prevent writing RPM into a Celsius JSON field. This is arguably more dangerous — a wrong value on the server propagates to every client.
use serde::Serialize;
// ──── Dimensional types from ch06, with 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 response members ────
// Field types enforce which unit belongs in which JSON property.
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TemperatureMember {
pub member_id: String,
pub name: String,
pub reading_celsius: Celsius, // ← must be 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, // ← must be Rpm
pub reading_units: &'static str, // always "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, // ← must be Watts
#[serde(skip_serializing_if = "Option::is_none")]
pub power_capacity_watts: Option<Watts>,
pub status: ResourceStatus,
}
// ──── Building a Thermal response from sensor cache ────
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,
},
},
// ❌ Compile error — can't put Rpm in a Celsius field:
// 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 class eliminated: unit confusion at serialization. The Redfish schema says
ReadingCelsius is in °C. The Rust type system says reading_celsius must be
Celsius. If a developer accidentally passes Rpm(8400) or Watts(285.0), the
compiler catches it before the value ever reaches JSON.
Section 4 — Health Rollup as a Typed Fold
Redfish Status.Health is a rollup — the worst health of all sub-components.
In C, this is typically a series of if checks that inevitably misses a source.
With typed enums and Ord, the rollup is a one-line fold — and the compiler
ensures every source contributes:
/// Roll up health from multiple sources.
/// Ord on HealthValue: OK < Warning < Critical.
/// Returns the worst (max) value.
fn rollup(sources: &[HealthValue]) -> HealthValue {
sources.iter().copied().max().unwrap_or(HealthValue::OK)
}
/// Compute system-level health from all sub-components.
/// Takes explicit references to every source — the caller must provide ALL of them.
fn compute_system_health(
sensors: Option<&(SensorsReady, SensorCache)>,
sel: Option<&(SelReady, TypedSelSummary)>,
) -> ResourceStatus {
let mut inputs = Vec::new();
// ── Live sensor readings ──
if let Some((_proof, cache)) = sensors {
// Temperature health (dimensional: Celsius comparison)
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);
}
// Fan health (dimensional: Rpm comparison)
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 health (dimensional: Watts comparison)
for (_name, watts) in &cache.psu_power {
if *watts > Watts(800.0) {
inputs.push(HealthValue::Critical);
} else {
inputs.push(HealthValue::OK);
}
}
}
// ── SEL per-subsystem health (from ch07's TypedSelSummary) ──
// Each subsystem's health was derived by exhaustive matching over
// every sensor type and event variant. No information was lost.
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 class eliminated: incomplete health rollup. In C, forgetting to include PSU
status in the health calculation is a silent bug — the system reports “OK” while a
PSU is failing. Here, compute_system_health takes explicit references to every
data source. The SEL contribution is no longer a lossy bool — it’s seven
per-subsystem HealthValue fields derived by exhaustive matching in ch07’s consumer
pipeline. Adding a new SEL sensor type forces the classifier to handle it; adding a
new subsystem field forces the rollup to include it.
Section 5 — Schema Versioning with Phantom Types (ch09)
If the BMC advertises ComputerSystem.v1_13_0, the response must include
properties introduced in that schema version (LastResetTime, BootProgress).
Advertising v1.13 without those fields is a Redfish Interop Validator failure.
Phantom version markers make this a compile-time contract:
use std::marker::PhantomData;
// ──── Schema Version Markers ────
pub struct V1_5;
pub struct V1_13;
// ──── Version-Aware Response ────
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>,
}
// Methods available on ALL versions:
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-specific fields ────
/// Date and time of the last system reset.
pub struct LastResetTime(pub String);
/// Boot progress information.
pub struct BootProgress {
pub last_state: String,
pub last_state_time: String,
}
impl ComputerSystemResponse<V1_13> {
/// LastResetTime — REQUIRED in v1.13+.
/// This method only exists on V1_13. If the BMC advertises v1.13
/// and the handler doesn't call this, the field is missing.
pub fn last_reset_time(&self) -> LastResetTime {
// Read from RTC or boot timestamp register
LastResetTime("2026-03-16T08:30:00Z".to_string())
}
/// BootProgress — REQUIRED in v1.13+.
pub fn boot_progress(&self) -> BootProgress {
BootProgress {
last_state: "OSRunning".to_string(),
last_state_time: "2026-03-16T08:32:00Z".to_string(),
}
}
/// Build the full v1.13 JSON response, including version-specific fields.
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 — no LastResetTime, no 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() doesn't exist here.
// Calling it → compile error:
// let resp: ComputerSystemResponse<V1_5> = ...;
// resp.last_reset_time();
// ❌ ERROR: method `last_reset_time` not found for
// `ComputerSystemResponse<V1_5>`
}
Bug class eliminated: schema version mismatch. If the BMC is configured to
advertise v1.13, use ComputerSystemResponse<V1_13> and the compiler ensures
every v1.13-required field is produced. Downgrade to v1.5? Change the type
parameter — the v1.13 methods vanish, and no dead fields leak into the response.
Section 6 — Typed Action Dispatch (ch02 Inverted)
In ch02, the typed command pattern binds Request → Response on the client
side. On the server side, the same pattern validates incoming action payloads
and dispatches them type-safely — the inverse direction.
use serde::Deserialize;
// ──── Action Trait (mirror of ch02's IpmiCmd trait) ────
/// A Redfish action: the framework deserializes Params from the POST body,
/// then calls execute(). If the JSON doesn't match Params, deserialization
/// fails — execute() is never called with bad input.
pub trait RedfishAction {
/// The expected JSON body structure.
type Params: serde::de::DeserializeOwned;
/// The result of executing the 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 => {
// Send ACPI shutdown to host
println!("Initiating ACPI shutdown");
Ok(())
}
ResetType::ForceOff => {
// Assert power-off to host
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(())
}
// Exhaustive — compiler catches missing variants
}
}
}
// ──── 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(())
}
}
}
}
// ──── Generic Action Dispatcher ────
fn dispatch_action<A: RedfishAction>(
action: &A,
raw_body: &str,
) -> Result<A::Result, RedfishError> {
// Deserialization validates the payload structure.
// If the JSON doesn't match A::Params, this fails
// and execute() is never called.
let params: A::Params = serde_json::from_str(raw_body)
.map_err(|e| RedfishError::InvalidPayload(e.to_string()))?;
action.execute(params)
}
// ── Usage ──
fn handle_reset_action(body: &str) -> Result<(), RedfishError> {
// Type-safe: ResetParams is validated by serde before execute()
dispatch_action(&ComputerSystemReset, body)?;
Ok(())
// Invalid JSON: {"ResetType": "Explode"}
// → serde error: "unknown variant `Explode`"
// → execute() never called
// Missing field: {}
// → serde error: "missing field `ResetType`"
// → execute() never called
}
Bug classes eliminated:
- Invalid action payload: serde rejects unknown enum variants and missing fields
before
execute()is called. No manualif (body["ResetType"] == ...)chains. - Missing variant handling:
match params.reset_typeis exhaustive — adding a newResetTypevariant forces every action handler to be updated. - Type confusion:
ComputerSystemResetexpectsResetParams;ManagerResetToDefaultsexpectsResetToDefaultsParams. The trait system prevents passing one action’s params to another action’s handler.
Section 7 — Putting It All Together: The GET Handler
Here’s the complete handler that composes all six sections into a single schema-compliant response:
/// Complete GET /redfish/v1/Systems/1 handler.
///
/// Every required field is enforced by the builder type-state.
/// Every data source is gated by availability tokens.
/// Every unit is locked to its dimensional type.
/// Every health input feeds the typed rollup.
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. Health rollup (Section 4) ──
// Folds health from sensors + SEL into a single typed status
let health = compute_system_health(
sensors.as_ref(),
sel.as_ref(),
);
// ── 2. Builder type-state (Section 1) ──
let builder = ComputerSystemBuilder::new()
.power_state(power_state)
.status(health);
// ── 3. Source-availability tokens (Section 2) ──
let builder = match smbios {
Some((proof, tables)) => {
// SMBIOS available — populate from hardware
populate_from_smbios(builder, proof, tables)
}
None => {
// SMBIOS unavailable — safe defaults
populate_smbios_fallback(builder)
}
};
// ── 4. Optional enrichment from sensors (Section 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. Build (Section 1) ──
// .build() is available because both paths (SMBIOS present / absent)
// produce HasField for Name and UUID. The compiler verified this.
builder.build("1")
}
// ──── Server Startup ────
fn main() {
// Initialize all data sources — each returns an availability token
let smbios = init_smbios();
let sensors = init_sensors();
let sel = init_sel();
// Simulate handler call
let response = handle_get_computer_system(
&smbios,
&sensors,
&sel,
PowerStateValue::On,
Some("2.10.1".into()),
);
// NOTE: .unwrap() is used for brevity — handle errors in production.
println!("{}", serde_json::to_string_pretty(&response).unwrap());
}
Expected output:
{
"@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"
}
}
}
What the Compiler Proves (Server Side)
| # | Bug class | How it’s prevented | Pattern (Section) |
|---|---|---|---|
| 1 | Missing required field in response | .build() requires all type-state markers to be HasField | Builder type-state (§1) |
| 2 | Calling into failed subsystem | Source-availability tokens gate data access | Capability tokens (§2) |
| 3 | No fallback for unavailable source | Both match arms (present/absent) must produce HasField | Type-state + exhaustive match (§2) |
| 4 | Wrong unit in JSON field | reading_celsius: Celsius ≠ Rpm ≠ Watts | Dimensional types (§3) |
| 5 | Incomplete health rollup | compute_system_health takes explicit source refs; SEL provides per-subsystem HealthValue via ch07’s TypedSelSummary | Typed function signature + exhaustive matching (§4) |
| 6 | Schema version mismatch | ComputerSystemResponse<V1_13> has last_reset_time(); V1_5 doesn’t | Phantom types (§5) |
| 7 | Invalid action payload accepted | serde rejects unknown/missing fields before execute() | Typed action dispatch (§6) |
| 8 | Missing action variant handling | match params.reset_type is exhaustive | Enum exhaustiveness (§6) |
| 9 | Wrong action params to wrong handler | RedfishAction::Params is an associated type | Typed commands inverted (§6) |
Total runtime overhead: zero. The builder markers, availability tokens, phantom version types, and dimensional newtypes all compile away. The JSON produced is identical to the hand-rolled C version — minus nine classes of bugs.
The Mirror: Client vs. Server Pattern Map
| Concern | Client (ch17) | Server (this chapter) |
|---|---|---|
| Boundary direction | Inbound: JSON → typed values | Outbound: typed values → JSON |
| Core principle | “Parse, don’t validate” | “Construct, don’t serialize” |
| Field completeness | TryFrom validates required fields are present | Builder type-state gates .build() on required fields |
| Unit safety | Celsius ≠ Rpm when reading | Celsius ≠ Rpm when writing |
| Privilege / availability | Capability tokens gate requests | Availability tokens gate data source access |
| Data sources | Single source (BMC) | Multiple sources (SMBIOS, sensors, SEL, PCIe, …) |
| Schema version | Phantom types prevent accessing unsupported fields | Phantom types enforce providing version-required fields |
| Actions | Client sends typed action POST | Server validates + dispatches via RedfishAction trait |
| Health | Read and trust Status.Health | Compute Status.Health via typed rollup |
| Failure propagation | One bad parse → one client error | One bad serialization → every client sees wrong data |
The two chapters form a complete story. Ch17: “Every response I consume is type-checked.” This chapter: “Every response I produce is type-checked.” The same patterns flow in both directions — the type system doesn’t know or care which end of the wire you’re on.
Key Takeaways
- “Construct, don’t serialize” is the server-side mirror of “parse, don’t
validate” — use builder type-state so
.build()only exists when all required fields are present. - Source-availability tokens prove initialization — the same capability token pattern from ch04, repurposed to prove a data source is ready.
- Dimensional types protect producers and consumers — putting
Rpmin aReadingCelsiusfield is a compile error, not a customer-reported bug. - Health rollup is a typed fold —
OrdonHealthValueplus explicit source references mean the compiler catches “forgot to include PSU status.” - Schema versioning at the type level — phantom type parameters make version-specific fields appear and disappear at compile time.
- Action dispatch inverts ch02 —
serdedeserializes the payload into a typedParamsstruct, and exhaustive matching on enum variants means adding a newResetTypeforces every handler to be updated. - Server-side bugs propagate to every client — that’s why compile-time correctness on the producer side is even more critical than on the consumer side.