グローバルシングルトン
このセクションでは、グローバルに共有されるシングルトンの実装方法を説明します。 The embedded Rust bookは、Rust特有のローカルで所有されるシングルトンを説明しました。 グローバルシングルトンは、本質的にCやC++で見かけるシングルトンパターンです。 これは、組込み開発固有のものではありませんが、シンボルに関係するため、embedonomiconに相応しい内容のように思えます。
TODO(resources team) link "the embedded Rust book" to the singletons section when it's up
グローバルシングルトンを説明するために、このセクションでは、前のセクションで開発したロガーを、
グローバルにログ出力できるように拡張します。
結果は、the embedded Rust bookで説明した#[global_allocator]フィーチャと非常に似たものになります。
TODO(resources team) link
#[global_allocator]to the collections chapter of the book when it's in a more stable location.
やりたいことを、下記にまとめます。
前のセクションでは、Logトレイトを実装している特定のロガーを通してログメッセージを出力するために、log!マクロを作りました。
log!マクロのシンタックスは、log!(logger, "String")です。
このマクロを、log!("String")でも動くように拡張します。
loggerなしのバージョンを使うと、グローバルロガーを通してメッセージをログ出力しなければなりません。
これは、std::println!が動作する方法と同じです。
また、何がグローバルロガーか、を宣言するための機構が必要です。
これは、#[global_allocator]と似ている部分です。
グローバルロガーが最上位クレートで宣言される可能性があり、 グローバルロガーの型もまた最上位クレートで定義される可能性があります。 この場合、依存関係から正確なグローバルロガーの型を知ることはできません。 この場合をサポートするために、いくらか間接的な方法が必要になります。
logクレートにグローバルロガーの型をハードコーディングする代わりに、logクレート内で、
グローバルロガーのインタフェースだけを宣言します。
そのインタフェースは、logクレートに新しく追加するGlobalLogというトレイトです。
log!マクロもそのトレイトを使うようにします。
$ cat ../log/src/lib.rs
#![allow(unused)] #![no_std] fn main() { // 追加! pub trait GlobalLog: Sync { fn log(&self, address: u8); } pub trait Log { type Error; fn log(&mut self, address: u8) -> Result<(), Self::Error>; } #[macro_export] macro_rules! log { // 追加! ($string:expr) => { unsafe { extern "Rust" { static LOGGER: &'static dyn $crate::GlobalLog; } #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8) } }; ($logger:expr, $string:expr) => {{ #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8) }}; } // 追加! #[macro_export] macro_rules! global_logger { ($logger:expr) => { #[no_mangle] pub static LOGGER: &dyn $crate::GlobalLog = &$logger; }; } }
解説することがたくさんあります。
トレイトから始めましょう。
#![allow(unused)] fn main() { pub trait GlobalLog: Sync { fn log(&self, address: u8); } }
GlobalLogとLogとは、logメソッドを持っています。違いは、GlobalLog.logがレシーバの共有参照(&self)を取ることです。
グローバルロガーはstatic変数なので、これが必要です。後ほど、詳しく見ます。
もう1つの違う点は、GlobalLog.logはResultを返さないことです。
これは、呼び出し側にエラーを報告できないことを意味します。
これはグローバルシングルトンを実装するトレイトを使うための必要条件ではありません。
グローバルシングルトンでエラー処理をすることは良いことですが、グローバルバージョンのlog!マクロの全てのユーザーが、
エラー型に同意する必要があります。
ここでは、GlobalLog実装者がエラーを処理するようにして、インタフェースを少し簡略化します。
さらに別の違いは、GlobalLogが実装者に、スレッド間で共有できるようにするためのSyncを要求する点です。
これは、static変数内の値への要求です。それらの値の型は、Syncを実装しなければなりません。
現時点では、インタフェースがこのようになっていなければならない理由は、完全には明らかではないかもしれません。 クレートの他の部分を見ることで、より明らかになっていきますので、読み進めて下さい。
次はlog!マクロです。
#![allow(unused)] fn main() { ($string:expr) => { unsafe { extern "Rust" { static LOGGER: &'static dyn $crate::GlobalLog; } #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8) } }; }
特定の$loggerなしでマクロを呼び出すと、マクロはメッセージをログ出力するためにLOGGERと呼ばれるextern static変数を使います。
この変数はどこかで定義されたグローバルロガーです。そのため、externブロックを使っています。
このパターンはメインインタフェースの章で見ました。
LOGGERの型を宣言する必要があります。そうでなければ、コードは型チェックを行いません。
LOGGERの具体的な型はここではわかりませんが、
その型がGlobalLogトレイトを実装していることを知っています(むしろ必要としています)。
そこで、トレイトオブジェクトを使うことができます。
残りのマクロ拡張は、log!マクロのローカルバージョンの拡張ととてもよく似ています。
そのため、前の章で説明したことは、ここでは説明しません。
ここで、LOGGERがトレイトオブジェクトでなければならないことを知っているので、
GlobalLogで関連型のErrorを除去する理由はより明白です。もし除去しなければ、
LOGGERの型シグネチャの中でErrorの型を1つ選ばなければなりません。
これが先程、「log!マクロの全てのユーザーが、エラー型に同意する必要があります。」と書いた意味です。
そして、最後のピースのglobal_logger!マクロです。
これは、手続きマクロアトリビュートにもできますが、macro_rules!でマクロを書くほうが簡単です。
#![allow(unused)] fn main() { #[macro_export] macro_rules! global_logger { ($logger:expr) => { #[no_mangle] pub static LOGGER: &dyn $crate::GlobalLog = &$logger; }; } }
このマクロは、log!が使用するLOGGER変数を作ります。安定したABIインタフェースが必要なので、
no_mangleアトリビュートを使用します。
この方法により、LOGGERのシンボル名はlog!マクロが期待する「LOGGER」になります。
他の重要な点は、このstatic変数の型は、log!マクロの展開で使用される型と正確に一致しなければなりません。
もし一致しない場合、ABIの不一致により、良くないことが起こるでしょう。
新しいグローバルロガーの機能を使う例を書いてみましょう。
$ cat src/main.rs
#![no_main] #![no_std] use cortex_m::interrupt; use cortex_m_semihosting::{ debug, hio::{self, HStdout}, }; use log::{global_logger, log, GlobalLog}; use rt::entry; struct Logger; global_logger!(Logger); entry!(main); fn main() -> ! { log!("Hello, world!"); log!("Goodbye"); debug::exit(debug::EXIT_SUCCESS); loop {} } impl GlobalLog for Logger { fn log(&self, address: u8) { // `static mut`変数へのアクセスを割り込み安全にするため(これはメモリ安全のために要求されます)、 // クリティカルセクション(`interrupt::free`)を使います。 interrupt::free(|_| unsafe { static mut HSTDOUT: Option<HStdout> = None; // 遅延初期化 if HSTDOUT.is_none() { HSTDOUT = Some(hio::hstdout()?); } let hstdout = HSTDOUT.as_mut().unwrap(); hstdout.write_all(&[address]) }).ok(); // `.ok()` = エラーを無視します } }
TODO(resources team) use
cortex_m::Mutexinstead of astatic mutvariable whenconst fnis stabilized.
依存関係にcortex-mを追加する必要があります。
$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
これは、前のセクションで書いた例を移植したものです。 出力は、以前のものと同じです。
$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g O .log 00000001 Goodbye
00000000 g O .log 00000001 Hello, world!
このグローバルシングルトンの実装がゼロコストでないことが気になる読者も居るかと思います。 なぜなら、トレイトオブジェクトを使用しており、vtableを参照してメソッド呼び出しを行う動的ディスパッチになるためです。
しかし、LLVMは十分に賢く、この動的ディスパッチをコンパイラの最適化 / LTOで消去してくれます。
このことは、シンボルテーブル内のLOGGERを探すことで確認できます。
$ cargo objdump --bin app --release -- -t | grep LOGGER
もしstaticが見つからない場合、vtableがないことと、
LLVMがLOGGER.logの呼び出しをLogger.logの呼び出しに変換できたことを意味します。