Rust 強大的靜態分析
在編譯時強制要求引腳和外設配置,以確保資源不會被您應用程序中非預期的部分使用。
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 interface
struct 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 interface
struct 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 interface
struct GpioConfig<ENABLED, DIRECTION, MODE> {
/// GPIO Configuration structure generated by svd2rust
periph: GPIO_CONFIG,
enabled: ENABLED,
direction: DIRECTION,
mode: MODE,
}
// Type states for MODE in GpioConfig
struct Disabled;
struct Enabled;
struct Output;
struct Input;
struct PulledLow;
struct PulledHigh;
struct HighZ;
struct DontCare;
/// These functions may be used on any GPIO Pin
impl<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 Pin
impl 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 GPIO
impl<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 input
let input_pin = pin.into_enabled_input();
// Read from the pin
let 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>(); // == 0
let _ = size_of::<Input>(); // == 0
let _ = size_of::<PulledHigh>(); // == 0
let _ = 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 或代碼空間來跟蹤狀態,並呈現爲與直接寄存器訪問相同的機器代碼。
通常,這些抽象可以根據需要嵌套得儘可能深。只要使用的所有組件都是零大小的類型,整個結構在運行時就不存在。
對於複雜或深度嵌套得結構,定義所有可能得狀態組合可能很乏味。在這些情況下,宏可用於生成所有實現。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pxixLSm8u-qqtw_kuzv1eQ