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(())
    }
}

フォーマット文字列を取り扱うために、UartWritercore::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

出典