FABRKNT
スタックを読む — 中級への橋渡し
ソース読みのための Rust
レッスン 9 / 10·CONTENT10 分20 XP
コース
スタックを読む — 中級への橋渡し
レッスンの役割
CONTENT
順序
9 / 10

レッスン8 — macro_rules! 基礎

問い

Rust の マクロprintln! / vec! / format! 等の ! 付き呼び出し。コンパイル時にコード展開、関数より柔軟。Reth / Revm のソースで popn_top! / gas! 等のマクロが頻出する。macro_rules! の読み方

原理(最小モデル)

  • マクロ = コンパイル時コード展開. 関数の呼び出し時に AST に展開、関数より柔軟(可変アリティ / 任意の構文)、トレードオフはデバッグの難しさ。
  • macro_rules! の構文. macro_rules! name { ($x:expr) => { ... } }、パターン → 展開のルール、expr / ident / ty / pat / stmt 等の fragment specifier。
  • 可変アリティ. $($x:expr),* でカンマ区切りの式リスト、$( ... )* で繰り返し展開、println!("{} {}", a, b) のような可変引数を実現。
  • ヒジエン(hygiene). マクロ内の変数名がスコープ汚染しない、外側の同名変数と衝突しない、安全な抽象化。
  • Revm の popn_top!. スタック pop + アンダーフロー事前チェック + unwrap_unchecked を 1 マクロに集約、add / mul / sub 等 30+ opcode で再利用。
  • gas! マクロ. ガス課金 + cold_path ヒント + 早期 return を集約、5 行版、ホットパスのボイラープレート削減。
  • 手続きマクロ(proc-macro). #[derive(...)] / #[tokio::main] / sol! 等、macro_rules! より強力、Rust 関数として実装、Expert で深掘り。
  • 読むときの注意. マクロ展開を cargo expand で見る、展開後コードを読めば理解できる、macro_rules! 自体の構文は慣れ。

具体例 + ステップで組み立てる

macro_rules! 基礎

標準 Rust 教科書が 薄い 2 領域のうちのもう 1 つ。前のレッスンが unsafe をカバー、これが macro_rules!。両方合わせて Revm のホットパスソース — popn_top!gas! 等 — を読むのに必要なもの。

Revm のインタープリタは マクロで埋め尽くされているpopn_top!gas!push!as_usize_or_fail! — これらは関数呼び出しではなく、コンパイル時のテキスト展開。読むには構文を知る必要がある。

基本の形

macro_rules! マクロは パターン → 展開。パターンが呼び出し構文にマッチ、展開がコードを生成:

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

let n = square!(3 + 4);    // 展開: (3 + 4) * (3 + 4) → 49

$x:expr 部分が メタ変数 $x を宣言、任意の式にマッチ。exprフラグメント specifier、パーサに何の構文を期待するか教える。

一般的なフラグメント specifier

Specifierマッチ対象
expr任意の Rust 式
ident識別子 (変数名、関数名)
tt単一の token tree (最も柔軟)
stmt
patパターン (match arm、let 束縛で)
ty
block{ ... } ブロック

ソース読みには expridenttt でほとんど足りる。

繰り返し: $( ... ),*

マクロは リスト を繰り返し構文でマッチできる:

macro_rules! print_all {
    ( $( $x:expr ),* ) => {
        $(
            println!("{}", $x);
        )*
    };
}

print_all!(1, "hello", 3.14);
// 展開:
// println!("{}", 1);
// println!("{}", "hello");
// println!("{}", 3.14);

構文を読む:

  • $( ... ) が繰り返しグループを宣言
  • ,* が「カンマ区切り、ゼロ回以上」(,+ で 1 回以上)
  • 展開内の $( ... )* が本体をマッチ 1 つにつき 1 回繰り返す

Revm の popn_top! を読む

基礎を装備して:

macro_rules! popn_top {
    ([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
        // ... 本体
    };
}

翻訳:

  • パターンは [ で始まり、$($x:ident),* (識別子のカンマ区切りリスト)、それから ]
  • それからカンマ、それから $top:ident (1 つの識別子)
  • それからカンマ、それから $interpreter:expr (式)

なので popn_top!([op1], op2, ctx.interpreter) を呼ぶとマッチは:

  • $x = 識別子 1 つのリスト: op1
  • $top = op2
  • $interpreter = ctx.interpreter

popn_top!([a, b, c], top, ctx.interpreter) を呼ぶと $x は 3 つのリスト。

展開は $($x),* を使って同じ識別子リストを destructuring パターンに展開する。

マクロの hygiene — マクロがあなたのスコープを汚染できない理由

macro_rules! マクロは hygienic: マクロ内で導入された変数名は呼び出し元のスコープの変数と衝突しない。

macro_rules! double {
    ($e:expr) => {{
        let temp = $e;       // マクロ内の 'temp'
        temp * 2
    }};
}

let temp = "important";      // 呼び出し元の 'temp'
let n = double!(5);          // 呼び出し元の 'temp' を壊さず展開
println!("{}", temp);        // まだ "important"

Hygiene は macro_rules! の特徴 (そして C スタイルのマクロより好まれる主な理由)。

macro_rules! vs proc_macro

両方に出会う。簡単な区別:

macro_rules!proc-macro
定義パターンマッチルール別の proc-macro クレート内の Rust 関数
操作対象Token tree (リテラル構文)TokenStream (コンパイラのパース後表現)
限定的だがほぼ十分任意のコード生成
vec!println!popn_top!gas!#[derive(Serialize)]sol!address! の裏の hex!

macro_rules! が Revm のインタープリタが使うものをカバー。sol! と重い proc-macro は Expert ティアのトピック。

全部まとめる

中級レッスン 1 で revm/crates/interpreter/src/instructions/macros.rs を開くと:

macro_rules! popn_top {
    ([ $($x:ident),* ], $top:ident, $interpreter:expr) => {
        if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
            $crate::primitives::hints_util::cold_path();
            return Err($crate::InstructionResult::StackUnderflow);
        }
        let ([$( $x ),*], $top) = unsafe {
            $crate::interpreter_types::StackTr::popn_top(&mut $interpreter.stack)
                .unwrap_unchecked()
        };
    };
}

これを単語ずつ読める:

  • macro_rules! — 宣言的マクロ定義
  • パターン ([ $($x:ident),* ], $top:ident, $interpreter:expr) — 呼び出し引数を destructure
  • 本体: 長さチェック、それから unsafe { ... } ブロックで unwrap_unchecked() を呼ぶ (長さが直前で検証済みだから)
  • SAFETY の根拠は長さチェック — コメントは暗黙 (Revm はタイトなコードでは時々省略)

魔法を読んでいるわけではない。すでに知っているパターンを 読んでいるだけである。

読み物リスト

  1. Rust Book 章 19.5 (Macros) — 簡潔。1 度読んで、必要時に戻る。
  2. The Little Book of Rust Macros (danielkeep.github.io) — 無料、最高のマクロ-by-example リファレンス。

このレッスンで持ち帰るもの

  • macro_rules! は token パターンにマッチしてコンパイル時にコードに展開する
  • フラグメント specifier ($x:expr$x:ident$x:tt) が各メタ変数がマッチする構文を宣言
  • 繰り返し構文 $( ... ),* で 1 つのパターンが引数のリストにマッチできる
  • Hygiene がマクロが呼び出し元のスコープを汚染するのを防ぐ
  • proc-macros は重い兄弟 — 別クレート、TokenStream で動く — Expert で扱う

技術的前提知識は完了

これで スタックを読む — 中級への橋渡し技術的 前提は終了:

  • ✓ EVM をバイト単位で (dispatch loop、ストア、ガス、コールフレーム)
  • ✓ ブロックレベルの Ethereum (ブロック、レシート、reorg)
  • ✓ ソース読みのための Rust (generics、dyn、Arc、unsafe、マクロ)

進む前に、簡単なゲートチェック。最初の中級レッスンを開くとこれが見えるはず:

pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
    popn_top!([op1], op2, context.interpreter);
    *op2 = op1.wrapping_add(*op2);
    Ok(())
}

各部品が、今や 1 つの繋がった文として読めるはず:

  • シグネチャ: 型パラメータ 2 つ、片方が ?Sizeddyn Host になれる
  • マクロ呼び出し: 1 pop、新トップへの可変参照、内側の長さチェックで正当化された unsafe unwrap_unchecked
  • 算術: wrapping_add は EVM の ADD が mod 2²⁵⁶ だから

この 3 文がしっくり来るなら、技術的前提は身についている。

あと 1 レッスン — 中級コースの読み方

中級ティアは 3 つの独立コース(Revm・Reth・Alloy)で、いずれも同じ編集スタイル(Predict、クイズゲート、積み上げ→ウォークスルー→クイズ→ドリル)を共有する。次は 「中級コースの読み方」 を読む。3 コース共通で 1 度読めば済むメタオリエンテーションである。

それから、コースを 1 つ選んで始める。

まとめ(3行)

  • macro_rules! = コンパイル時コード展開、可変アリティ + ヒジエン + fragment specifier(expr / ident / ty 等)、関数より柔軟。
  • Revm の popn_top! / gas! 等は opcode 実装のボイラープレート削減、30+ opcode で再利用。
  • cargo expand で展開後コード確認、手続きマクロ(#[derive] / sol! 等)は Expert で深掘り、次は最後のレッスン(ソース読解コースの読み方)。