グローバルシングルトン
この節では、グローバルで共有されるシングルトンをどのように実装するかを 扱います。embedded Rust book では、ほぼ Rust 特有と言ってよい、ローカルで 所有されるシングルトンを扱いました。グローバルシングルトンは本質的には C や C++ で見られるシングルトンパターンそのものです。これは組み込み開発に 固有のものではありませんが、シンボルが関わるので Embedonomicon によく合う 題材だと思われました。embedded Rust book には singletons の章もあります。
この節を説明するために、前の節で開発したロガーを拡張して、グローバル
ロギングをサポートするようにします。結果は、embedded Rust book の
collections 章で扱った #[global_allocator] 機能によく似たものに
なります。
ここでやりたいことを要約すると次のとおりです。
前の節では、特定のロガー、つまり Log トレイトを実装する値を通して
メッセージを記録する log! マクロを作成しました。log! マクロの構文は
log!(logger, "String") です。これを拡張して、log!("String") も動作
するようにしたいのです。logger を指定しない版を使った場合、その
メッセージはグローバルロガーを通して記録されるようにします。これは
std::println! の動作と同じです。また、どれをグローバルロガーとするかを
宣言する仕組みも必要です。ここが #[global_allocator] と似ている部分です。
グローバルロガーが最上位クレートで宣言されることもあれば、グローバル ロガーの型自体が最上位クレートで定義されることもあります。このシナリオ では、依存クレートはグローバルロガーの正確な型を 知ることができません。 このシナリオをサポートするには、何らかの間接化が必要になります。
そこで、log クレートの中にグローバルロガーの型をハードコードする代わり
に、そのクレートではグローバルロガーの インターフェース だけを宣言
します。つまり、log クレートに新しいトレイト GlobalLog を追加します。
log! マクロもそのトレイトを利用しなければなりません。
$ cat ../log/src/lib.rs
#![allow(unused)]
#![no_std]
fn main() {
// NEW!
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 {
// NEW!
($string:expr) => {
unsafe {
unsafe extern "Rust" {
static LOGGER: &'static dyn $crate::GlobalLog;
}
#[unsafe(export_name = $string)]
#[unsafe(link_section = ".log")]
static SYMBOL: u8 = 0;
$crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
}
};
($logger:expr, $string:expr) => {{
#[unsafe(export_name = $string)]
#[unsafe(link_section = ".log")]
static SYMBOL: u8 = 0;
$crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
}};
}
// NEW!
#[macro_export]
macro_rules! global_logger {
($logger:expr) => {
#[unsafe(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 {
unsafe extern "Rust" {
static LOGGER: &'static dyn $crate::GlobalLog;
}
#[unsafe(export_name = $string)]
#[unsafe(link_section = ".log")]
static SYMBOL: u8 = 0;
$crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
}
};
}
特定の $logger を指定せずに呼び出されると、このマクロは LOGGER という
名前の extern static 変数を使ってメッセージを記録します。この変数
こそが 別の場所で定義されているグローバルロガーであり、そのために
extern ブロックを使います。このパターンは main interface 章で見ました。
LOGGER の型を宣言する必要があります。そうしないとコードが型チェックを
通りません。この時点では LOGGER の具体的な型は分かりませんが、
GlobalLog トレイトを実装していることは分かっている、というより要求して
いるので、ここではトレイトオブジェクトを使えます。
マクロ展開の残りの部分は log! マクロのローカル版の展開と非常によく似て
いるので、previous 章で説明していることもあり、ここでは説明しません。
LOGGER がトレイトオブジェクトでなければならないと分かった今、なぜ
GlobalLog で関連型 Error を省いたのかがより明確になります。もし
省かなければ、LOGGER の型シグネチャにおいて Error の型を選ぶ必要が
あったでしょう。これが先ほど「log! のすべての利用者がエラー型について
合意する必要がある」と述べた意味です。
さて、最後のピースは global_logger! マクロです。proc macro 属性にする
こともできましたが、macro_rules! マクロとして書くほうが簡単です。
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! global_logger {
($logger:expr) => {
#[unsafe(no_mangle)]
pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
};
}
}
このマクロは log! が使う LOGGER 変数を作成します。安定した ABI
インターフェースが必要なので、no_mangle 属性を使います。こうすることで
LOGGER のシンボル名は “LOGGER” となり、これは log! マクロが期待する
ものです。
もう 1 つ重要なのは、この static 変数の型が log! マクロの展開で使われる
型と完全に一致していなければならないことです。一致しないと、ABI の不一致
によって深刻な問題が発生します。
この新しいグローバルロガー機能を使う例を書いてみましょう。
$ cat src/main.rs
#![no_main]
#![no_std]
use core::cell::RefCell;
use cortex_m::interrupt;
use cortex_m::interrupt::Mutex;
use cortex_m_semihosting::{
debug,
hio::{self, HostStream},
};
use log::{GlobalLog, global_logger, log};
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) {
// we use a critical section (`interrupt::free`) to make the access to the
// `HSTDOUT` variable interrupt-safe which is required for memory safety
interrupt::free(|cs| {
static HSTDOUT: Mutex<RefCell<Option<HostStream>>> = Mutex::new(RefCell::new(None));
let mut hstdout = HSTDOUT.borrow(cs).borrow_mut();
// lazy initialization
if hstdout.is_none() {
hstdout.replace(hio::hstdout()?);
}
let hstdout = hstdout.as_mut().unwrap();
hstdout.write_all(&[address])
})
.ok(); // `.ok()` = ignore errors
}
}
依存関係に cortex-m を追加する必要がありました。
$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.7.7"
cortex-m-semihosting = "0.5.0"
log = { path = "../log" }
rt = { path = "../rt" }
これは previous 節で書いた例の 1 つを移植したものです。出力はそこで 得たものと同じです。
$ 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 呼び出しに変換できたことを
意味します。