宣言マクロ: macro_rules!

16 分で読了
このページの目次

宣言マクロは、反復的な構文構造をパターンセットに変換するのに適しています。例えば、複数の実装を生成したり、可変長の式を受け取ったり、簡潔なドメイン固有言語を提供したりする場合です。すでに型が確定されている値に対してアルゴリズムを実行するだけであれば、通常の関数とジェネリクスの方が理解しやすく、デバッグや検査も容易です。

マクロはコンパイル時に展開されますが、生成されたコードは実行時に割り当て、ループを実行、副作用を発生させる可能性があります。const fn もコンパイル時に評価できるため、「コンパイル時対実行時」という区別だけでマクロと関数を分類することはできません。

呼び出し中のトークンツリー括弧によるグループ化と構文断片であり、任意の文字列置換ではありません1ルール順にマッチ最初にマッチしたルールを選択し、反復と置換を実行2呼び出し位置に展開式、項目、パターンなどの有効な構文を形成3名前、型、借用チェッカーマクロが生成したコードも通常のRustルールに従う必要がある4宣言マクロ: まず構文にマッチし、次に展開後のコードを検査するマクロ展開は所有権ルールを免除せず、引数が1回だけ評価されることも自動的に保証されません。

評価回数を示す小さなマクロ

macro_rules! list {
    () => { ::std::vec::Vec::new() };
    ($($value:expr),+ $(,)?) => {{
        let mut output = ::std::vec::Vec::new();
        $(output.push($value);)*
        output
    }};
}

fn main() {
    let mut n = 0;
    let values = list![{ n += 1; n }, { n += 1; n },];
    assert_eq!(values, [1, 2]);
    assert_eq!(n, 2);
    let empty: Vec<u8> = list![];
    assert!(empty.is_empty());
}

反復部分により、各式は生成された push の中に1回ずつ現れるため、各項目は1回だけ評価され、記述順に実行されます。もし $value を2箇所に書くと、副作用が重複したり、ムーブ後の使用エラーを引き起こしたりする可能性があります。マクロシステムは結果をキャッシュしてくれません。

二重中括弧内の内部ブロックにより、全体が1つの式として展開され、一時変数のスコープがブロック内に制限されます。ここでは ::std パスを使用しており、例が std に依存していることを明確にしています。no_std ライブラリからエクスポートされるマクロでは、alloc パスを適切に処理する必要があり、そのままコピーすることはできません。

1 回の展開でも式は 2 回実行され得る

副作用のある引数で展開と実行を区別できます。next() は通常の実行時操作で、マクロが決めるのは生成構文での登場回数です。先に束縛すると評価回数は制御できますが、その後の再利用は Copy・借用・ムーブの規則に従います。

図を準備しています
1 回の展開でも式は 2 回実行され得る

$e の繰り返しは式も繰り返し評価し、先に let value=$e と束縛すると 1 回になります。

フラグメントは構文カテゴリであり、Rustの型ではない

フラグメントマッチする内容混同しやすい境界
expr式推論される型が u32 なのか String なのか不明
ty型構文エイリアスを自動的に展開しない、トレイト実装を判断しない
ident識別子の形状任意の複数セグメントパスではない
pathパス限定名などの対応する構文に使用可能
pat / pat_paramパターントップレベルまたはパターンルールは edition に依存
tt1つのトークンツリー1つのトークン、またはバランスの取れた区切り文字で囲まれたツリーであり、任意の長さの入力ではない
item項目関数、構造体、impl など
meta / vis属性内容 / 可視性vis は空の可視性にマッチ可能

任意の数のトークンツリーが必要ない場合は、通常 $($tokens:tt)* と書きます。expr などの既に解析されたフラグメントを別のマクロに渡す際、それは対応するカテゴリの不透明なフラグメントとなり、任意のリテラルトークンとして分解してマッチすることはできません。ident、lifetime、tt には特定の例外があります。マクロマッチルール

expr と tt を渡した後、次層で見るものが異なる

macro_rules! inspect {
    (3) => { "リテラルトークン" };
    ($value:expr) => { "式フラグメント" };
}
macro_rules! pass_expr { ($value:expr) => { inspect!($value) }; }
macro_rules! pass_tt { ($value:tt) => { inspect!($value) }; }
fn main() {
    assert_eq!(inspect!(3), "リテラルトークン");
    assert_eq!(pass_tt!(3), "リテラルトークン");
    assert_eq!(pass_expr!(3), "式フラグメント");
}

pass_expr は 3 を式フラグメントとして既に解析しているため、後続の inspect はこの不透明なフラグメントをリテラルの 3 として再マッチさせることができず、expr ルールに進みます。tt はマッチ可能なトークンツリーの形状を保持します。次にトークンレベルの構文解析を行う必要がある場合は、インターフェースに対応するトークンを保持します。式を使用するコードを生成するだけであれば、expr の方が構文制約が明確です。

pass_tt!(1 + 2) は「式も1つのトークンツリーである」という意味ではありません。これはグループ化されていない複数のトークンを含んでおり、単一の $value:tt にはマッチしません。明示的にグループ化するか、必要に応じて繰り返されるトークンを受け取る必要があります。フラグメントの選択は後続の処理に影響し、単に入力を受け付けるプレースホルダーを適当に選ぶものではありません。

反復、ルール順序、曖昧さ

$()* は0回以上の繰り返し、$()+ は1回以上の繰り返し、$()? はオプションの1回を意味します。カンマ区切りとオプションの末尾カンマは通常 $($x:expr),* $(,)? のように組み合わされます。反復出力のネスト構造はキャプチャと一致する必要があり、複数の変数が同時に反復する場合、その数も一致しなければなりません。

ルールは定義順に試行され、最初にマッチしたルールが選択されます。展開コードが失敗した場合、別のルールを試すために自動的にバックトラックすることはありません。より具体的なルールを、それらをキャプチャできる広範なルールの前に配置してください。

マッチングは任意のバックトラックを行う汎用パーサではありません。例えば、反復された ident の直後に別の ident が続くと、現在のトークンではそれがどの部分に属するかが判断できず、局所的な曖昧さが発生する可能性があります。区切り文字、明示的なキーワード、またはグループ化を使用して境界を明確にすることが、再帰を増やすよりも安全です。

対データを同じ反復層でキャプチャする

macro_rules! pairs {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut output = ::std::vec::Vec::new();
        $(output.push(($key, $value));)*
        output
    }};
}
fn main() {
    let mut calls = 0;
    let entries = pairs!(
        "first" => { calls += 1; calls },
        "second" => { calls += 1; calls },
    );
    assert_eq!(entries, [("first", 1), ("second", 2)]);
    assert_eq!(calls, 2);
}

各反復ユニットは key と value の両方を同時にキャプチャするため、1組のデータが自然に対応します。2つの独立した反復として設計し、出力でそれらを同じ反復層に配置する場合、2つのキャプチャ数が一致することを保証する必要があります。マクロ展開は、欠落している項目を呼び出し側に補完したり、自動的にデカルト積を行ったりしません。

この pairs は Vec を生成するだけで、キーの重複をチェックしたり、Map を作成したりしません。構文糖は、提供される能力を正確に命名すべきです。重複キーの戦略、型変換、リソース上限などの要件がある場合は、対応するコードを生成するか、通常の関数に任せる必要があります。

区切り文字も構文の長期互換性の境界

マクロフラグメントの後に許可されるトークンは、follow-set ルールによって制限されます。例えば、expr の後に =>、,、または ; が続くのは一般的な有効な境界ですが、式に直後に続く任意の記号シーケンスを独自に発明し、パーサが常に明確に区別することを期待することはできません。明示的なグループ化とキーワードは、エラー位置をユーザー入力の近くに保つことができます。また、複数の再帰層を経た後にマッチしないというエラーを報告するよりも優れています。

境界が明確な再帰的トークン処理の例

以下では、text 式を順序通りに String に書き込み、セミコロンで1つのコマンドを区切ります。これは小型の TT muncher であり、完全な HTML/XML パーサを装うものではありません。

macro_rules! text_script {
    (@step $out:ident;) => {};
    (@step $out:ident; text $value:expr; $($rest:tt)*) => {
        {
            use ::std::fmt::Write as _;
            ::std::write!(&mut $out, "{}", $value).expect("write String");
        }
        text_script!(@step $out; $($rest)*);
    };
    (@step $out:ident; $($invalid:tt)+) => {
        compile_error!("expected: text <expression>;");
    };
    () => { ::std::string::String::new() };
    ($($input:tt)+) => {{
        let mut output = ::std::string::String::new();
        text_script!(@step output; $($input)*);
        output
    }};
}

fn main() {
    assert_eq!(text_script!(text "value="; text 42;), "value=42");
    assert_eq!(text_script!(), "");
}

各ステップは1つの text コマンドを消費し、残りのトークンを転送します。終了ルールは必ず存在する必要があります。専用の @step エラールールを広範なエントリ前に配置することで、無効な入力がエントリに繰り返し入って再帰深度エラーのみを報告するのを防ぎます。長い入力で末尾全体に繰り返しマッチすると、高い、場合によっては二次的なマクロ展開コストを引き起こす可能性があります。

<$tag:ident> ... </$tag:ident> は「同じ名前のタグにマッチする」テクニックとして有効ではありません。同じマッチャー内で同じ名前の変数を反復バインドするとエラーになり、無制限の children 反復で終了タグに遭遇すると曖昧さが生じやすくなります。複雑なネスト構文は明示的なグループ化に変更するか、プロシージャルマクロを使用して実際に解析してください。

衛生性と跨 crate 参照

macro_rules は混合位置衛生性を持ちます。マクロ内部で生成されたローカル変数などは、テキスト的に呼び出し側の変数と単純に混同されることはありませんが、項目パスなどの名前解決はルールに従って理解する必要があります。「完全に衛生なので、すべての名前に関する問題は消える」と言うことはできません。

エクスポートされたマクロ内で定義元 crate の補助項目を参照する場合、呼び出し側が依存関係を特定の固定文字列に命名していることを想定せず、$crate::helper を使用してください。$crate は可視性ルールを迂回せず、外部展開でアクセスする必要がある補助項目は、適切な可視性を持っている必要があります。

通常の macro_rules は、任意の安定した文字列連結による識別子の生成機能を提供しません。呼び出し側から提供された ident を受け取る、固定名を参照する、2つのトークンから新しい名前を連結する、これらは異なる要件です。後者の場合、プロシージャルマクロまたは特別なサポートが必要です。

マクロ定義内の valueマクロ内の局所束縛呼び出しで渡された $input呼び出し側の文脈で解決生成結果同名でも異なる束縛衛生性は名前を処理し、繰り返し評価やビジネス副作用は処理しないローカル一時名と呼び出し側の入力は、異なる名前ソースを保持する入力式が何回書かれ、いつ実行されるかは、依然として展開コードによって決定されるmacro_rules は混合位置衛生性である。項目パスと跨 crate の補助関数は明示的な参照が必要。

同名のローカル変数は単純なテキスト置換ではない

macro_rules! offset {
    ($input:expr) => {{
        let value = 10;
        $input + value
    }};
}
fn main() {
    let value = 7;
    assert_eq!(offset!(value), 17);
    assert_eq!(value, 7);
}

渡された value は依然として呼び出し場所の 7 を指しており、マクロが導入した一時 value は 10 です。これは、マクロが呼び出し側の任意の同名変数を読み取れることを意味しません。呼び出し側が後続で使用するためのバインディングを生成したい場合は、対応する ident を受け取り、展開位置とスコープを明確に規定する必要があります。

エクスポートされたマクロの補助項目は定義元 crate から位置指定する

#[doc(hidden)]
pub fn __decorate(text: &str) -> String { format!("[{text}]") }
#[macro_export]
macro_rules! decorated {
    ($text:expr) => { $crate::__decorate($text) };
}
fn main() {
    assert_eq!(decorated!("ready"), "[ready]");
}

$crate はマクロを定義した crate を指し、使用側が依存関係をリネームしても位置指定できます。これはプライベート関数をパブリック関数に変えるものではありません。ここでは補助関数が外部に可視である必要があり、doc(hidden) は生成されたドキュメントでの通常の表示を隠すだけで、アクセス権限ではありません。パブリックマクロが生成するコードに必要な補助インターフェースも、互換性の責任を負う必要があり、安易に削除することはできません。

edition と検査戦略

マクロ定義の edition は、expr、pat などのフラグメントのマッチ範囲に影響します。Rust 2024 では expr が拡張され、マッチ可能な式形状が増えました。expr_2021 は対応する旧ルールを保持します。edition をアップグレードする際は、以前複数のブランチを選択していた部分がどうなるか確認してください。

マクロを検証するには、成功例だけでなく、空の入力、末尾区切り文字、副作用のある引数、無効な構文、および跨モジュール/跨 crate での使用をカバーする必要があります。展開出力は構造を理解するのに適していますが、最終的には呼び出しコードのコンパイル動作が基準となります。

2024 の expr はルール選択を変更する

以下の完全な例は edition 2024 でコンパイルされます:

macro_rules! broad_first {
    ($value:expr) => { "expression" };
    (const $value:expr) => { "special const" };
}
macro_rules! legacy_first {
    ($value:expr_2021) => { "expression" };
    (const $value:expr) => { "special const" };
}
fn main() {
    assert_eq!(broad_first!(const { 1 }), "expression");
    assert_eq!(legacy_first!(const { 1 }), "special const");
}

2024 の expr はトップレベルの const ブロックを受け取ることができるため、broad_first の最初のルールですでにマッチします。expr_2021 は旧フラグメント範囲を保持し、2番目の const ルールが選択される機会を維持します。移行時は、旧動作を保持するか、ルール順序を調整して新構文を受け入れるかを決める必要があります。最終的にコンパイルできるかどうかだけでなく、マッチ範囲を決定するのはマクロ定義時の edition であり、マクロ呼び出し側の edition ではありません。2024 フラグメント移行

マクロの失敗ケースもインターフェース説明の一部です。無効な入力でどのカスタムエラーをトリガーするかを記録し、反復回数が一致しない場合に拒否されることを確認し、外部 crate でエクスポートされたマクロをテストしてください。同じソースファイル内の成功呼び出しだけでは、依存関係のリネーム、可視性、パスの衛生性をカバーすることはできません。