S-JIS[2024-10-08] 変更履歴

Rust 宣言的マクロ定義方法メモ

Rustマクロのmacro_rules!マクロのメモ。


概要

宣言的マクロはmacro_rules!マクロで定義する。

macro_rules! マクロ名 {
    (引数のパターン) => {
        生成する値
    };
    …
}

なお、宣言的マクロはグローバルな位置(モジュール直下)で定義する。(implブロック内では定義できない)


macro_rules!の例

列挙型のインスタンスを生成する宣言的マクロを作ってみる。

pub enum MyEnum {
    Message(String),
}

この列挙子ではメッセージをStringで保持するが、文字列リテラルを渡そうとすると一手間かかる。(直接&strを渡せないから)

fn main() {
   let _e = MyEnum::Message("test".to_string());	// いちいちto_string()するのがウザい
}

そこで、&strを渡せるマクロを作ってみる。


macro_rules! my_enum_message {
    ($message:expr) => {
        MyEnum::Message($message.to_string())
    };
}

宣言的マクロはmacro_rules!マクロで定義する。
ここで定義するマクロ名の後ろには「!」を付けない。

マクロ定義本体はmatch式のような感じで、引数をパターンマッチさせる。
このマクロ内で使用する変数には「$」を付ける。
($message:expr)」は引数が1個の場合にマッチする。
引数2つだったら「($message1:expr, $message2:expr)」のようになる。

引数名の後ろのexprは、式を受け取るという意味。
識別子(変数名や関数名等)を受け取るidentとか、型名を受け取るtyとか、他にも色々ある。

そして、「=>」の後に、生成したい値を構築する。


fn main() {
    let _e = my_enum_message!("test");
    let _e = my_enum_message!(123);
}

(残念ながら)マクロでは渡された式の型をチェックできないので、マクロ展開後のソースコードにエラーが無ければ通る。
この例だと、"test".to_string()123.to_string()もコンパイルが通るので、マクロとしてもエラーにならない。


末尾カンマの例

Rustでは、関数やメソッド呼び出しの引数の末尾にカンマがあっても良い。(末尾カンマは無視される)

fn main() {
    my_func(1);
    my_func(2,);	// 末尾カンマOK
}

fn my_func(n: i32) {
    println!("{}", n);
}

しかし上記で作ったマクロの呼び出しでは、末尾カンマがあるとエラーになる。

    let _e = my_enum_message!("test",);	// error: no rules expected the token `,`

macro_rules!マクロによる引数のパターンマッチでは、カンマがあるかどうかも判断対象になる。
したがって、末尾カンマを許容するには、引数パターンにカンマを含めてやればいい。

以下の2通りの方法が考えられる。

macro_rules! my_enum_message {
    ($message:expr) => {		// 末尾カンマが無いパターン
        MyEnum::Message($message.to_string())
    };
    ($message:expr,) => {		// 末尾カンマが有るパターン
        MyEnum::Message($message.to_string())
    };
}
macro_rules! my_enum_message {
    ($message:expr $(,)?) => {	// カンマが0個または1個
        MyEnum::Message($message.to_string())
    };
}

オーバーロードの例

Rustでは、関数やメソッドのオーバーロード(同名の引数違い)を定義することは出来ない。

しかしmacro_rules!マクロでは引数のパターンマッチが出来るので、異なる引数が受け取れる。
つまり、宣言的マクロではオーバーロードを定義するのと同じ状態に出来る。

#[derive(Debug)]
struct MyStruct {
    value1: i32,
    value2: Option<i32>,
}
macro_rules! new_my_struct {
    ($value:expr) => {               	// 引数が1つのパターン
        MyStruct {
            value1: $value,
            value2: None,
        }
    };
    ($value1:expr, $value2:expr) => {	// 引数が2つのパターン
        MyStruct {
            value1: $value1,
            value2: Some($value2),
        }
    };
}
fn main() {
    let s1 = new_my_struct!(123);
    println!("s1={:?}", s1);	// MyStruct { value1: 123, value2: None }

    let s2 = new_my_struct!(123, 456);
    println!("s2={:?}", s2);	// MyStruct { value1: 123, value2: Some(456) }
}

宣言的マクロのエクスポート

macro_rules!マクロで定義された宣言的マクロは、デフォルトでは同一モジュール内(マクロを定義した場所より下)でしか使用できない。

他モジュールで使用できるようにする為には、マクロをエクスポートする必要がある。
(通常の構造体や列挙型等はuse文によってモジュールを指定すれば使えるようになるが、宣言的マクロはそういう仕組みではない)


エクスポートの例

src/my_struct.rs

// my_structモジュール

#[derive(Debug)]
pub struct MyStruct1 {
    value: i32,
}

impl MyStruct1 {
    pub fn new(value: i32) -> MyStruct1 {
        MyStruct1 { value }
    }
}

src/my_macro.rs

// my_macroモジュール

#[macro_export]
macro_rules! new_my_struct1 {
    ($value:expr) => {
        $crate::my_struct::MyStruct1::new($value)
    };
}

マクロ定義に#[macro_export]属性を付けることによってエクスポートする。
これにより、ルートモジュールのマクロとして、他モジュールから使用できるようになる。
(このマクロを定義している場所はmy_macroモジュールなのだが、エクスポートされたマクロは、そのモジュールに属している扱いにならないようだ)

エクスポートされたマクロはどのモジュールで展開されるか分からない。
内部で使用する構造体等をモジュールの相対パスで書いてしまうと、展開された場所からは異なるものを指してしまう(あるいは見つからない)ことになってしまう。
そこで、$crate(自分のクレートのルートを表す)を使って、絶対パスでモジュールを記述しておく。


src/main.rs:

mod my_macro;
mod my_struct;
mod sub;

fn main() {
    let s = new_my_struct1!(123);
    println!("main={:?}", s);

    sub::print_my_struct1();
}

main.rsはルートモジュールなので、(use文でマクロを宣言しなくても)エクスポートされたマクロを使用できる。


src/sub.rs

use crate::new_my_struct1;

pub(crate) fn print_my_struct1() {
    let s = new_my_struct1!(456);
    println!("sub={:?}", s);
}

ルートモジュールでないモジュールでは、use文でインポートしないとエクスポートされたマクロを使えない。
use文で宣言する際は、(マクロが定義されているモジュールではなく)ルートモジュール直下にあるものとして宣言する。


マクロへ戻る / Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま