シングルトン
ソフトウェアエンジニアリングにおいて、singleton パターンは、クラスのインスタンス化を 1 つのオブジェクトに制限するソフトウェア設計パターンです。
Wikipedia: Singleton Pattern
しかし、なぜ単にグローバル変数を使えないのでしょうか?
次のように、すべてを public static にすることもできます
static mut THE_SERIAL_PORT: SerialPort = SerialPort;
fn main() {
let _ = unsafe {
THE_SERIAL_PORT.read_speed();
};
}
しかし、これにはいくつか問題があります。これは可変なグローバル変数であり、Rust では、これらを扱うことは常に unsafe です。さらに、これらの変数はプログラム全体から見えるため、borrow checker はこれらの変数への参照や所有権の追跡を支援できません。
Rust ではどのように行うのでしょうか?
ペリフェラルを単なるグローバル変数にする代わりに、PERIPHERALS という構造体を作成し、その中に各ペリフェラル用の Option<T> を含めることを考えるかもしれません。
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() を複数回呼び出そうとすると、コードは panic します!
fn main() {
let serial_1 = unsafe { PERIPHERALS.take_serial() };
// これは panic します!
// let serial_2 = unsafe { PERIPHERALS.take_serial() };
}
この構造体とのやり取りは unsafe ですが、いったんその中に含まれていた SerialPort を取得してしまえば、もはや unsafe も PERIPHERALS 構造体自体も使う必要はありません。
これには小さな実行時オーバーヘッドがあります。というのも、SerialPort 構造体を option でラップし、さらに一度 take_serial() を呼び出す必要があるからです。しかし、この小さな初期コストによって、プログラムの残り全体で borrow checker を活用できるようになります。
既存ライブラリのサポート
上では独自の Peripherals 構造体を作成しましたが、あなたのコードでこれを行う必要はありません。cortex_m クレートには singleton!() というマクロがあり、これを代わりに実行してくれます。
use cortex_m::singleton;
fn main() {
// `main` が一度だけ実行される場合は OK
let x: &'static mut bool =
singleton!(: bool = false).unwrap();
}
さらに、cortex-m-rtic を使用している場合、これらのペリフェラルを定義して取得する一連の処理全体は抽象化されており、代わりに、定義したすべての項目について非 Option<T> 版を含む Peripherals 構造体が渡されます。
// 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 ペリフェラル
let core: cortex_m::Peripherals = cx.core;
// デバイス固有のペリフェラル
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 つの要素を組み合わせると、borrow checker の要件を適切に満たしている場合にのみハードウェアへアクセスできることになり、つまり、同じハードウェアに対する複数の可変参照が同時に存在することはありません!
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 {
// ...
}
これにより、コードがハードウェアに変更を加えるべきかどうかを、実行時ではなく コンパイル時 に強制できます。補足すると、これは通常 1 つのアプリケーション内でのみ機能しますが、ベアメタルシステムではソフトウェアは単一のアプリケーションにコンパイルされるため、通常これは制約にはなりません。