cover_image

Rust 强大的静态分析

技术源泉
2023年10月18日 22:00
在编译时强制要求引脚和外设配置,以确保资源不会被您应用程序中非预期的部分使用。
Rust 的类型系统在编译时防止数据竞争(参见Send和Sync特征)。类型系统在编译时还可用于检查其他属性,在某些情况下减少对运行时检查的需求。
这些静态检查应用于嵌入式程序时,可确保正确的完成 I/O 接口的配置。可以设计一个只能初始化串行的 API,首次使用时需要先配置引脚。
还可以静态检查操作(例如将引脚设置为低电平),只能 在正确配置的外围设备上执行。例如,在浮动输入模式下配置的引脚,如果修改它的输出状态将引发编译错误。
而且,所有权的概念可以应用到外围设备,以确保只有程序的某些部分可以修改外设。与将外围设备视为全局可变状态的替代方案相比,这种访问控制使软件更容易理解。
Typestate概念描述了将关于对象当前状态的信息编码为类型。虽然这有点晦涩,但如果你在rust中使用builder模式,那么你已经在使用typestate编程了。
pub mod foo_module {    #[derive(Debug)]    pub struct Foo {        inner: u32,    }
pub struct FooBuilder { a: u32, b: u32, }
impl FooBuilder { pub fn new(starter: u32) -> Self { Self { a: starter, b: starter, } }
pub fn double_a(self) -> Self { Self { a: self.a * 2, b: self.b, } }
pub fn into_foo(self) -> Foo { Foo { inner: self.a + self.b, } } }}
fn main() { let x = foo_module::FooBuilder::new(10) .double_a() .into_foo();
println!("{:#?}", x);}
在本例中,没有直接的方法创建Foo对象。我们必须创建一个FooBuilder,并正确初始化它,然后才能获取我们想要的Foo对象。
这个小例子中,编码了两种状态:
  • FooBuilder,表示“未配置”或“配置过程中”状态
  • Foo,表示“已配置”或“准备使用”状态
强类型
因为Rust是一个强类型系统语言,它没有简单的方法可以神奇的创建Foo实例,或者在不调用into_foo方法的情况下将FooBuilder转换为Foo。另外,调用into_foo方法将消费原始的FooBuilder结构,这意味着如果不创建新实例,就无法重用它。
这使我们能够将系统的状态表示为类型,并将状态转换的必要操作包含在将一种类型变换为另一种类型的方法中。通过创建FooBuilder,并将其转换为Foo对象,我们已经完成了基本状态机的步骤。
这种状态机可以应用于嵌入式外围设备,例如,简化的GPIO引脚的配置可以表示为以下状态树:

图片

如果外围设备在一种模式下启动,想要切换到另一种模式下,例如,将禁用状态转换为输入:高电阻状态,则需要如下操作:
禁用->启用->配置为输入->输入:高电阻
硬件表示,通常将上面的状态树通过在寄存器中写入值映射到GPIO外围设备。我们定义的一个GPIO配置寄存器,描述如下:

图片

在Rust中,我们定义下面的结构体来控制GPIO:
/// GPIO interfacestruct GpioConfig {    /// GPIO Configuration structure generated by svd2rust    periph: GPIO_CONFIG,}
impl GpioConfig { pub fn set_enable(&mut self, is_enabled: bool) { self.periph.modify(|_r, w| { w.enable().set_bit(is_enabled) }); }
pub fn set_direction(&mut self, is_output: bool) { self.periph.modify(|_r, w| { w.direction().set_bit(is_output) }); }
pub fn set_input_mode(&mut self, variant: InputMode) { self.periph.modify(|_r, w| { w.input_mode().variant(variant) }); }
pub fn set_output_mode(&mut self, is_high: bool) { self.periph.modify(|_r, w| { w.output_mode.set_bit(is_high) }); }
pub fn get_input_status(&self) -> bool { self.periph.read().input_status().bit_is_set() }}
上面的结构,允许我们在寄存器中设置没有意义的数据,例如,我们在GPIO被配置为输入时,设置字段,会发生什么?再比如,设置被拉低的输出,或被设置为高的输入,对于某些硬件来说,这可能无关紧要,但在其它硬件上,可能导致意外或未定义的行为。尽管这个接口编写起来很方便,但它并没有强制执行我们的硬件实现所规定的设计契约。
所以,为了能够落地,我们必须在使用底层硬件前检查状态,在运行时强制执行我们的设计契约。我们实现的代码如下:
/// GPIO interfacestruct GpioConfig {    /// GPIO Configuration structure generated by svd2rust    periph: GPIO_CONFIG,}
impl GpioConfig { pub fn set_enable(&mut self, is_enabled: bool) { self.periph.modify(|_r, w| { w.enable().set_bit(is_enabled) }); }
pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> { if self.periph.read().enable().bit_is_clear() { // Must be enabled to set direction return Err(()); }
self.periph.modify(|r, w| { w.direction().set_bit(is_output) });
Ok(()) }
pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> { if self.periph.read().enable().bit_is_clear() { // Must be enabled to set input mode return Err(()); }
if self.periph.read().direction().bit_is_set() { // Direction must be input return Err(()); }
self.periph.modify(|_r, w| { w.input_mode().variant(variant) });
Ok(()) }
pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> { if self.periph.read().enable().bit_is_clear() { // Must be enabled to set output status return Err(()); }
if self.periph.read().direction().bit_is_clear() { // Direction must be output return Err(()); }
self.periph.modify(|_r, w| { w.output_mode.set_bit(is_high) });
Ok(()) }
pub fn get_input_status(&self) -> Result<bool, ()> { if self.periph.read().enable().bit_is_clear() { // Must be enabled to get status return Err(()); }
if self.periph.read().direction().bit_is_set() { // Direction must be input return Err(()); }
Ok(self.periph.read().input_status().bit_is_set()) }}
因为我们需要对硬件实施限制,所以我们最终会做大量的运行时检查,这会浪费时间和资源。
我们使用另一种实现模式,使用Rust系统的类型系统来使用状态转换规则,例如:
/// GPIO interfacestruct GpioConfig<ENABLED, DIRECTION, MODE> {    /// GPIO Configuration structure generated by svd2rust    periph: GPIO_CONFIG,    enabled: ENABLED,    direction: DIRECTION,    mode: MODE,}
// Type states for MODE in GpioConfigstruct Disabled;struct Enabled;struct Output;struct Input;struct PulledLow;struct PulledHigh;struct HighZ;struct DontCare;
/// These functions may be used on any GPIO Pinimpl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> { pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> { self.periph.modify(|_r, w| w.enable.disabled()); GpioConfig { periph: self.periph, enabled: Disabled, direction: DontCare, mode: DontCare, } }
pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input, HighZ> { self.periph.modify(|_r, w| { w.enable.enabled() .direction.input() .input_mode.high_z() }); GpioConfig { periph: self.periph, enabled: Enabled, direction: Input, mode: HighZ, } }
pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> { self.periph.modify(|_r, w| { w.enable.enabled() .direction.output() .input_mode.set_high() }); GpioConfig { periph: self.periph, enabled: Enabled, direction: Output, mode: DontCare, } }}
/// This function may be used on an Output Pinimpl GpioConfig<Enabled, Output, DontCare> { pub fn set_bit(&mut self, set_high: bool) { self.periph.modify(|_r, w| w.output_mode.set_bit(set_high)); }}
/// These methods may be used on any enabled input GPIOimpl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> { pub fn bit_is_set(&self) -> bool { self.periph.read().input_status.bit_is_set() }
pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> { self.periph.modify(|_r, w| w.input_mode().high_z()); GpioConfig { periph: self.periph, enabled: Enabled, direction: Input, mode: HighZ, } }
pub fn into_input_pull_down(self) -> GpioConfig<Enabled, Input, PulledLow> { self.periph.modify(|_r, w| w.input_mode().pull_low()); GpioConfig { periph: self.periph, enabled: Enabled, direction: Input, mode: PulledLow, } }
pub fn into_input_pull_up(self) -> GpioConfig<Enabled, Input, PulledHigh> { self.periph.modify(|_r, w| w.input_mode().pull_high()); GpioConfig { periph: self.periph, enabled: Enabled, direction: Input, mode: PulledHigh, } }}
让我们看看,怎么使用它:
/* * Example 1: Unconfigured to High-Z input */let pin: GpioConfig<Disabled, _, _> = get_gpio();
// Can't do this, pin isn't enabled!// pin.into_input_pull_down();
// Now turn the pin from unconfigured to a high-z inputlet input_pin = pin.into_enabled_input();
// Read from the pinlet pin_state = input_pin.bit_is_set();
// Can't do this, input pins don't have this interface!// input_pin.set_bit(true);
/* * Example 2: High-Z input to Pulled Low input */let pulled_low = input_pin.into_input_pull_down();let pin_state = pulled_low.bit_is_set();
/* * Example 3: Pulled Low input to Output, set high */let output_pin = pulled_low.into_enabled_output();output_pin.set_bit(true);
// Can't do this, output pins don't have this interface!// output_pin.into_input_pull_down();
这绝对是存储GPIO引脚的便捷方法,但是为什么这么做呢?
由于我们完全在编译时强制实施设计约束,因此不产生运行时成本。当引脚处于输入模式时,无法设置输出模式。相反,你必须通过将其转换为输出引脚,然后设置输出模式来演练状态。因此,在执行函数之前检查当前状态不会受到运行时损失。此外,由于这些状态有类型系统强制实施,因此此接口的使用者不再有出错的余地。如果他们尝试执行非法的状态转换,代码将无法编译。
类型状态也是零成本抽象的一个很好的例子,零成本抽象是将某些行为移动到编译时执行或分析的能力。这些类型状态不包含实际数据,而是用作标记。由于它们不包含数据,因此它们在运行时在内存中没有实际表示形式:
use core::mem::size_of;
let _ = size_of::<Enabled>(); // == 0let _ = size_of::<Input>(); // == 0let _ = size_of::<PulledHigh>(); // == 0let _ = size_of::<GpioConfig<Enabled, Input, PulledHigh>>(); // == 0
零大小类型
struct Enabled;
像这样定义的结构称为零大小类型,因为它们不包含实际数据。尽管这些类型在编译时表现为“真实”,你可以复制它们,移动它们,引用它们等,但是优化器会完全剥离它们。
在此代码片段中:
pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {    self.periph.modify(|_r, w| w.input_mode().high_z());    GpioConfig {        periph: self.periph,        enabled: Enabled,        direction: Input,        mode: HighZ,    }}
我们返回的GpioConfig在运行时永远不会存在。调用此函数通常归结为单个汇编指令,将常量寄存器值存储到寄存器位置。这意味着我们开发的类型状态接口是零成本抽象,它不再使用CPU、RAM或代码空间来跟踪状态,并呈现为与直接寄存器访问相同的机器代码。
通常,这些抽象可以根据需要嵌套得尽可能深。只要使用的所有组件都是零大小的类型,整个结构在运行时就不存在。
对于复杂或深度嵌套得结构,定义所有可能得状态组合可能很乏味。在这些情况下,宏可用于生成所有实现。



Rust · 目录
上一篇Rust 灵活的内存管理下一篇Rust 与C进行互操作
继续滑动看下一个
技术源泉
向上滑动看下一个