シングルトン
ソフトウェア工学において、シングルトン・パターンはクラスのインスタンス化を1つのオブジェクトに制限するデザインパターンです。
Wikipedia: Singleton Pattern
なぜグローバル変数は使えないのか?
このように、全てをパブリックかつスタティックにすることができます。
static mut THE_SERIAL_PORT: SerialPort = SerialPort; fn main() { let _ = unsafe { THE_SERIAL_PORT.read_speed(); } }
しかし、これにはいくつか問題があります。 これはミュータブルなグローバル変数であり、Rustにおいては、これらとやり取りするのは常にアンセーフです。 これらの変数はプログラムの全体を通して見えることになり、つまりそれは借用チェッカがこれらの変数の参照や所有権を追跡するのに役立たなくなることを意味します。
Rustではどうするか?
単にペリフェラルをグローバル変数にする代わりに、各ペリフェラル毎にOption<T>
を含むPERIPHERALS
と呼ばれるグローバル変数を作ることにします。
struct Peripherals {
serial: Option<SerialPort>,
}
impl Peripherals {
fn take_serial(&mut self) -> SerialPort {
let p = replace(&mut self.serial, None);
p.unwrap()
}
}
static mut PERIPHERALS: Peripherals = Peripherals {
serial: Some(SerialPort),
};
この構造体によって、ペリフェラルの唯一のインスタンスが取得できるようになります。
もしもtake_serial()
を複数回呼び出そうとすれば、コードはパニックするでしょう。
fn main() { let serial_1 = unsafe { PERIPHERALS.take_serial() }; // This panics! // これはパニックします! // let serial_2 = unsafe { PERIPHERALS.take_serial() }; }
この構造体とのやり取りはunsafe
にはなりますが、一度この構造体に含まれるSerialPort
を取得してしまえばもうunsafe
やPERIPHERALS
構造体を使う必要は全くありません。
この方法では、オプション型の中にSerialPort
構造体をラップし、take_serial()
を一度コールする必要があるため、実行時に小さなオーバーヘッドとなります。
しかし、この少々のコストを前払いすることで、残りのプログラムで借用チェッカを利用できるようになるのです。
既存のライブラリによるサポート
上記のコードではPeripherals
構造体を作りましたが、あなたのコードでも同じようにする必要はありません。
cortex_m
クレートはこれと同様のことをしてくれるsingleton!()
と呼ばれるマクロを含んでいます。
#[macro_use(singleton)] extern crate cortex_m; fn main() { // OK if `main` is executed only once, // `main`が一度だけしか実行されなければOKです let x: &'static mut bool = singleton!(: bool = false).unwrap(); }
加えて、あなたがcortex-m-rtfm
クレートを使うのなら、これらのペリフェラルを定義・取得するプロセス全体は抽象化され、代わりにあなたが定義した全てのアイテムが、Option<T>
なしで含まれているPeripherals
構造体を手渡されます。
// cortex-m-rtfm v0.3.x
app! {
resources: {
static RX: Rx<USART1>;
static TX: Tx<USART1>;
}
}
fn init(p: init::Peripherals) -> init::LateResources {
// Note that this is now an owned value, not a reference
// これは所有された値であり、参照ではないことに注意してください
let usart1: USART1 = p.device.USART1;
}
しかしなぜ?
しかし、これらのシングルトンがRustのコードの動作にどのような顕著な違いをもたらすのでしょうか?
impl SerialPort {
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
fn read_speed(
&self // <------ This is really, really important
&self // <------ これは本当に、本当に重要です。
) -> u32 {
unsafe {
ptr::read_volatile(Self::SER_PORT_SPEED_REG)
}
}
}
ここには2つの重要な要素があります。
- シングルトンを使用しているため、
SerialPort
構造体の取得手段や場所は1つだけになります。 read_speed()
メソッドを呼ぶためには、SerialPort
構造体の所有権もしくは参照を持つ必要があります。
これらの2つの要素をまとめると、借用チェッカを適切に満たしている場合のみハードウェアにアクセスできることを意味します。つまり、同じハードウェアに対して複数のミュータブルな参照を持つことはありません。
fn main() { // missing reference to `self`! Won't work. // `self`への参照が見つかりません。これはうまく動きません。 // SerialPort::read_speed(); let serial_1 = unsafe { PERIPHERALS.take_serial() }; // you can only read what you have access to // アクセスできるものだけ読み出すことができます。 let _ = serial_1.read_speed(); }
ハードウェアをデータのように扱う
加えて、いくつかの参照はミュータブルで、またいくつかはイミュータブルなため、関数またはメソッドがハードウェアの状態を変更できるかどうかを確認することが可能になります。
この関数はハードウェアの設定を変更できます。
fn setup_spi_port(
spi: &mut SpiPort,
cs_pin: &mut GpioPin
) -> Result<()> {
// ...
}
このメソッドは変更できません。
fn read_button(gpio: &GpioPin) -> bool {
// ...
}
これにより、実行時ではなくコンパイル時にコードがハードウェアを変更するかどうかを強制できます。 注意点としては、通常この強制はひとつのアプリケーション内でのみ機能します。ベアメタルシステムにおいては、ソフトウェアはひとつのアプリケーションにコンパイルされるため、これは一般的には制約にはなりません。