Capability Mixins — Compile-Time Hardware Contracts 🟡
Capability Mixins:编译期硬件契约 🟡
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.
本章将学到什么: ingredient trait,也就是总线能力声明,怎样和 mixin trait、blanket impl 配合起来,一边消除诊断代码复制,一边保证所有硬件依赖都在编译期被满足。Cross-references: ch04 (capability tokens), ch09 (phantom types), ch10 (integration)
交叉阅读: ch04 讲 capability token,ch09 讲 phantom type,ch10 讲整体集成。
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.
阈值比较那部分明明是同一套逻辑,但因为周围依赖的总线类型不同,抽起来总觉得别扭。capability mixin 的做法是:把每条硬件总线拆成一个ingredient trait,只要一个类型具备了对应 ingredient,相关诊断行为就能自动挂上去。
Ingredient Traits (Hardware Capabilities)
Ingredient Traits,也就是硬件能力声明
Each bus or peripheral is an associated type on a trait. A diagnostic controller declares which buses it has:
每条总线、每种外设能力,都通过 trait 上的关联类型来表达。诊断控制器要做的事,只是把自己“拥有哪些总线”这件事声明出来。
/// 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)
Mixin Traits,也就是诊断行为
A mixin provides behavior automatically to any type that has the required capabilities:
mixin trait 的意思就是:只要一个类型满足所需能力,这些行为就会自动附着上去。
pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec<u8>; }
pub trait I2cBus {
fn read_register(&self, addr: u8, reg: u8) -> u8;
fn write_register(&self, addr: u8, reg: u8, value: u8);
}
pub trait GpioController { fn read_pin(&self, pin: u32) -> bool; }
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:
一个具体的诊断控制器只要把自己具备的能力声明出来,就会自动继承所有匹配的 mixin。
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:
这种 mixin 模式之所以符合“构造即正确”,原因就在这里:
- You can’t call
read_fan_speed()without SPI — the method only exists on types that implementHasSpi + HasI2c
没有 SPI 就调不了read_fan_speed():这个方法只会出现在实现了HasSpi + HasI2c的类型上 - You can’t forget a bus — if you remove
HasSpifromBaseBoardController,FanDiagMixinmethods disappear at compile time
总线能力不可能忘记补:如果从BaseBoardController里删掉HasSpi,FanDiagMixin的方法会在编译期整体消失 - Mock testing is automatic — replace
LinuxSpiwithMockSpiand all mixin logic works with the mock
Mock 测试天然成立:把LinuxSpi换成MockSpi,所有 mixin 逻辑都能直接复用 - New platforms just declare capabilities — a GPU daughter card with only I2C gets
TempMonitorMixin(if it also has GPIO) but notFanDiagMixin(no SPI)
新平台只需要声明能力:如果某块 GPU 子板只有 I2C,再加上 GPIO,它就能拿到TempMonitorMixin,但因为没有 SPI,自然拿不到FanDiagMixin
When to Use Capability Mixins
什么时候适合用 Capability Mixins
| Scenario 场景 | Use mixins? 适不适合用 mixin |
|---|---|
| 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 ✅ 适合,能力可以直接 mock |
| Single-bus simple peripherals 只有一条总线的简单外设 | ⚠️ Overhead may not be worth it ⚠️ 未必划算,抽象成本可能比收益大 |
| Pure business logic (no hardware) 纯业务逻辑,没有硬件依赖 | ❌ Simpler patterns suffice ❌ 没必要,普通抽象就够了 |
Mixin Trait Architecture
Mixin Trait 架构图
flowchart TD
subgraph "Ingredient Traits / 能力声明"
SPI["HasSpi"]
I2C["HasI2c"]
GPIO["HasGpio"]
end
subgraph "Mixin Traits / 行为混入"
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
练习:网络诊断 Mixins
Design a mixin system for network diagnostics:
设计一套用于网络诊断的 mixin 系统:
- Ingredient traits:
HasEthernet,HasIpmi
ingredient traits 包括:HasEthernet、HasIpmi - Mixin:
LinkHealthMixin(requiresHasEthernet) withcheck_link_status(&self)
mixin 一:LinkHealthMixin,要求HasEthernet,并提供check_link_status(&self) - Mixin:
RemoteDiagMixin(requiresHasEthernet + HasIpmi) withremote_health_check(&self)
mixin 二:RemoteDiagMixin,要求HasEthernet + HasIpmi,并提供remote_health_check(&self) - Concrete type:
NicControllerthat implements both ingredients.
具体类型为NicController,它要同时实现这两个 ingredient。
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.
ingredient trait 用来声明硬件能力:像HasSpi、HasI2c、HasGpio这些,都是基于关联类型的能力 trait。 - Mixin traits provide behaviour via blanket impls —
impl<T: HasSpi + HasI2c> FanDiagMixin for T {}.
mixin trait 通过 blanket impl 提供行为:比如impl<T: HasSpi + HasI2c> FanDiagMixin for T {}。 - Adding a new platform = listing its capabilities — the compiler provides all matching mixin methods.
新增平台,本质上就是列出它具备的能力:剩下匹配到的 mixin 方法由编译器自动补齐。 - 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.
Mock 测试几乎是白送的:把LinuxSpi换成MockSpi,mixin 逻辑基本一行都不用改。