レッスン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 | { ... } ブロック |
ソース読みには expr・ident・tt でほとんど足りる。
繰り返し: $( ... ),*
マクロは リスト を繰り返し構文でマッチできる:
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 はタイトなコードでは時々省略)
魔法を読んでいるわけではない。すでに知っているパターンを 読んでいるだけである。
読み物リスト
- Rust Book 章 19.5 (Macros) — 簡潔。1 度読んで、必要時に戻る。
- 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 つ、片方が
?Sizedでdyn 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 で深掘り、次は最後のレッスン(ソース読解コースの読み方)。