グローバルシングルトン
このセクションでは、グローバルに共有されるシングルトンの実装方法を説明します。 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::Mutex
instead of astatic mut
variable whenconst fn
is 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
の呼び出しに変換できたことを意味します。