シングルトン
ソフトウェア工学において、シングルトン・パターンはクラスのインスタンス化を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() };
// これはパニックします!
// let serial_2 = unsafe { PERIPHERALS.take_serial() };
}
この構造体とのやり取りはunsafeにはなりますが、一度この構造体に含まれるSerialPortを取得してしまえばもうunsafeやPERIPHERALS構造体を使う必要は全くありません。
この方法では、オプション型の中にSerialPort構造体をラップし、take_serial()を一度コールする必要があるため、実行時に小さなオーバーヘッドとなります。 しかし、この少々のコストを前払いすることで、残りのプログラムで借用チェッカを利用できるようになるのです。
既存のライブラリによるサポート
上記のコードではPeripherals構造体を作りましたが、あなたのコードでも同じようにする必要はありません。 cortex_mクレートはこれと同様のことをしてくれるsingleton!()と呼ばれるマクロを含んでいます。
use cortex_m::singleton;
fn main() {
// `main`が一度だけしか実行されなければOKです
let x: &'static mut bool =
singleton!(: bool = false).unwrap();
}
Additionally, if you use cortex-m-rtic, the entire process of defining and obtaining these peripherals are abstracted for you, and you are instead handed a Peripherals structure that contains a non-Option<T> version of all of the items you define.
// cortex-m-rtic v0.5.x
#[rtic::app(device = lm3s6965, peripherals = true)]
const APP: () = {
#[init]
fn init(cx: init::Context) {
static mut X: u32 = 0;
// Cortex-M peripherals
let core: cortex_m::Peripherals = cx.core;
// Device specific peripherals
let device: lm3s6965::Peripherals = cx.device;
}
}
しかしなぜ?
しかし、これらのシングルトンがRustのコードの動作にどのような顕著な違いをもたらすのでしょうか?
impl SerialPort {
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
fn read_speed(
&self // <------ これは本当に、本当に重要です。
) -> u32 {
unsafe {
ptr::read_volatile(Self::SER_PORT_SPEED_REG)
}
}
}
ここには2つの重要な要素があります。
- シングルトンを使用しているため、
SerialPort構造体の取得手段や場所は1つだけになります。 read_speed()メソッドを呼ぶためには、SerialPort構造体の所有権もしくは参照を持つ必要があります。
これらの2つの要素をまとめると、借用チェッカを適切に満たしている場合のみハードウェアにアクセスできることを意味します。つまり、同じハードウェアに対して複数のミュータブルな参照を持つことはありません。
fn main() {
// `self`への参照が見つかりません。これはうまく動きません。
// SerialPort::read_speed();
let serial_1 = unsafe { PERIPHERALS.take_serial() };
// アクセスできるものだけ読み出すことができます。
let _ = serial_1.read_speed();
}
ハードウェアをデータのように扱う
加えて、いくつかの参照はミュータブルで、またいくつかはイミュータブルなため、関数またはメソッドがハードウェアの状態を変更できるかどうかを確認することが可能になります。
この関数はハードウェアの設定を変更できます。
fn setup_spi_port(
spi: &mut SpiPort,
cs_pin: &mut GpioPin
) -> Result<()> {
// ...
}このメソッドは変更できません。
fn read_button(gpio: &GpioPin) -> bool {
// ...
}これにより、実行時ではなくコンパイル時にコードがハードウェアを変更するかどうかを強制できます。 注意点としては、通常この強制はひとつのアプリケーション内でのみ機能します。ベアメタルシステムにおいては、ソフトウェアはひとつのアプリケーションにコンパイルされるため、これは一般的には制約にはなりません。