設計契約

前回、設計契約を強制しないインタフェースを書きました。架空のGPIO設定レジスタをもう一度見てみましょう。

名前 ビットフィールド 意味 説明
有効 0 0 無効 GPIOを無効にする
1 有効 GPIOを有効にする
方向 1 0 入力 方向を入力に設定する
1 出力 方向を出力に設定する
入力モード 2..3 00 hi-z 入力を高抵抗に設定する
01 プルダウン 入力ピンはプルダウンになる
10 プルアップ 入力ピンはプルアップになる
11 n/a 無効な状態。設定しないこと。
出力モード 4 0 ロー 出力ピンをローにする
1 ハイ 出力ピンをハイにする
入力状態 5 x 入力値 入力が1.5Vより低ければ0、1.5V以上であれば1

ハードウェアを使う前に状態をチェックし、実行時に設計契約を強制すると、コードは次のように書くことができます。

# /// GPIO interface
/// GPIOインタフェース
struct GpioConfig {
#     /// GPIO Configuration structure generated by svd2rust
    /// svd2rustによって生成されたGPIO設定構造体
    periph: GPIO_CONFIG,
}

impl Gpio {
    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
/// GPIOインタフェース
struct GpioConfig<ENABLED, DIRECTION, MODE> {
#     /// GPIO Configuration structure generated by svd2rust
    /// svd2rustによって生成されたGPIO設定構造体
    periph: GPIO_CONFIG,
    enabled: ENABLED,
    direction: DIRECTION,
    mode: MODE,
}

# // Type states for MODE in GpioConfig
// GpioConfigのMODEのための型状態
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
/// これらの関数はどのGPIOピンにも使えます
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
/// この関数はOutputピンに使用できます
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
/// これらのメソッドは、有効化された入力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
 * 例1:未設定から高抵抗入力
 */
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
// 今度は、未設定から高抵抗入力に変えます
// 訳注:into_enabled_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
 * 例2:高抵抗入力からプルダウン入力
 */
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
 * 例3:プルダウン入力から出力、ハイを設定
 */
let output_pin = pulled_low.into_enabled_output();
output_pin.set_bit(false);

# // Can't do this, output pins don't have this interface!
// これはできません、出力ピンはこのインタフェースを持っていません!
// output_pin.into_input_pull_down();

これは間違いなく、ピンの状態を保存するのに便利な方法ですが、なぜこのようにするのでしょうか? なぜ、GpioConfig構造体の中で、状態をenumとして保存するより良い方法なのでしょうか?

コンパイル時の機能の安全性

コンパイル時に、設計契約を完全に強制しているため、実行時コストはかかりません。入力方向のピンに対して、出力モードを設定することは不可能です。 代わりに、そのピンを出力ピンに変換してから、出力モードを設定することで、状態を辿る必要があります。 このおかげで、関数実行前に現在の状態をチェックすることによる実行時ペナルティは、ありません。

型システムによってこれらの状態が強制されるため、このインタフェースの利用者によるエラーの余地はもはや残っていません。 もし利用者が不正な状態遷移をしようとすると、そのコードはコンパイルできません!