3-3. print!マクロ
ベアメタル環境でデバッグする上で、自在にテキストを表示できることは、非常に重要です。Rustでは、print!
やprintln!
マクロを使うことで、数値や文字列、構造体までフォーマットして、テキストで表示することができます。
fn main() { println!("{}, {:?}", 1, vec!(1, 2, 3)); }
マイクロコントローラでは、文字出力はUARTで行うことが多いです。例えば、今、次のような関数を使って、1文字のASCII文字を表示できるとします。土台としては、これだけあればRustの文字列フォーマッタを利用可能です。(UARTペリフェラルの初期化がが必要な点や、TXバッファに空きがあるかどうか調べなければならない点は、一旦目を瞑って下さい)
fn write_byte(c: u8) {
unsafe {
*UART0_TX = c;
}
}
この状態で数値や文字列、構造体をテキストで表示しようとすると、まず文字列に変換しなければなりません。これを、自前で実装するのは、容易ではありません。読者の中には、C言語でprintf()
関数を (部分的に) 自作した経験がある方が、多数いらっしゃるかと思います。あれはあれで貴重な経験ではありますが、Rustではより簡単に、型安全なテキスト表示マクロを実装できます。
では、上記関数を使って、std
環境と同じように利用できるprint!
/ println!
マクロを実装しましょう。
まず、全貌をお見せします。
use core::fmt::{self, Write};
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
($fmt:expr) => (print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
}
pub fn _print(args: fmt::Arguments) {
let mut writer = UartWriter {};
writer.write_fmt(args).unwrap();
}
struct UartWriter;
impl Write for UartWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.bytes() {
write_byte(c);
}
Ok(())
}
}
これで全てです。この30行にも満たないコードを追加するだけで、std
環境と同じようにprintln!
が使えます。順番に解説していきます。
まず、print!
マクロの実装です。
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}
最も重要な部分は、format_args!
マクロの呼び出しです。format_args!
マクロは、コンパイラ組込みの手続きマクロで、文字列フォーマットの中心を担うAPIです。このマクロは、与えられたフォーマット文字列と引数群から、core::fmt::Arguments
を構築するコードを生成します。この辺りの話については、Rustの文字列フォーマット回り (改訂版)で非常に詳しく解説されています。ここでは詳細を割愛します。
$crate::_print()
は、format_args!
マクロの出力であるcore::fmt::Arguments
を引数に取るラッパー関数です。
pub fn _print(args: fmt::Arguments) {
let mut writer = UartWriter {};
writer.write_fmt(args).unwrap();
}
フォーマット文字列をUARTに出力するUartWriter
構造体のオブジェクトを作成し、core::fmt::Write
トレイトのwrite_fmt
メソッドを呼び出します。UartWriter
は、ここではかなり実装を簡略化しており、中身のない空の構造体です。ハードウェアの排他制御などは、今回は考慮に入れていません。
struct UartWriter;
impl Write for UartWriter {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.bytes() {
write_byte(c);
}
Ok(())
}
}
フォーマット文字列を取り扱うために、UartWriter
はcore::fmt::Write
トレイトを実装します。write_fmt
メソッドは、デフォルトメソッドなので、write_str
だけ実装すれば良いです。write_str
メソッドの関数シグネチャは、fn (&mut self, &str) -> fmt::Result
となっており、&str
の形で渡されるフォーマット済み文字列をどのように出力するか、を実装します。上記コードでは、イテレータで1バイトずつ取得し、write_byte
関数でUARTに1バイトずつ送信します。
それでは、実行してみましょう。03-bare-metal/print
ディレクトリに、QEMUで動作するサンプルがあります。リセットベクタ内で、println!
マクロを呼び出します。
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
println!("Hello {}", "Rust");
// 中略
}
次のコマンドで実行できます (thumbv7m-none-eabi
のクロスコンパイラとqemu-system-armが必要です) 。
$ cargo run
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/debug/print`
Hello Rust
注意:このサンプルはQEMUでしか動作しません。QEMUのUARTは初期設定不要で雑に使えるため、非常に便利です。
また、panicで紹介した通り、panic時の情報を表示する際も便利です。
pub unsafe extern "C" fn Reset() -> ! {
panic!("explicit panic!");
}
#[panic_handler]
fn panic(panic: &PanicInfo) -> ! {
println!("{}", panic);
loop {}
}
実行すると、panicを発生させたソースコードの位置と、メッセージを表示します。
panicked at 'explicit panic!', src/main.rs:10:5
出典
- Discovery: urpintln!