Day 060 — マクロ① macro_rules! ⑤ 「どこから見えるか」を予想する:テキスト順序・#[macro_use]・#[macro_export]・$crate

2026-09-29(バックフィル) 🟣 上級者 / Phase 3 概念理解 マクロ①(宣言的マクロ macro_rules!)
※ 9/29 のバックフィル分です。番号は出題済みの Day 059 の続きで 060 を使っています(日付と番号の順序が逆になるのは仕様)。Day 059 の fsm! とは独立した1本として読めます。

📚 背景知識(読んでから問題へ)

これまでの Day 056〜059 は「マクロの中身」(パターン・フラグメント・再帰)の話でした。今日は「そのマクロがどこから見えるか」、つまり名前解決の話です。macro_rules! は関数や型と違い、名前の見え方が2種類の別のルールで決まります。

ルールどのマクロに効く見え方並べる順序
テキスト順序スコープ(textual scope)#[macro_export] の無い普通の macro_rules!ファイルの上から下へ。定義の後ろで、定義を含むブロック・モジュールの終わりまで効く(定義より前の行からは見えない)
パスベースのスコープ(path-based scope)#[macro_export] を付けた、または pub(crate) use m; で名前を公開したマクロ他の項目と同じく crate::m!・use で解決効かない

この2つが混ざる場所が、実務で一番はまる所です。さらに次の4点が絡みます。

  1. #[macro_use] を mod に付けると、そのモジュールの中のマクロのテキスト順序スコープが、モジュールの終わりを越えて親へ伸びる。ただし伸びるのは「mod の行より後ろ」だけ。
  2. #[macro_export] は定義がモジュールの奥にあっても、クレートのルートに置く。名前空間がクレート全体で1つになる(衝突しうる)。
  3. macro_rules! は、変数(let の束縛)にだけ衛生的。展開後のコードに出てくる関数名・型名・モジュールのパスは、呼び出した場所で解決される。だから別クレートから呼ばれると、マクロ内の helper() は呼び出し側の helper を探しに行く。
  4. その対策が $crate。「このマクロを定義したクレートのルート」を指す特別な変数で、依存を package = "..." で改名されても正しく解決される。

macro_rules! の名前解決は、関数のように定義時に確定するのではなく展開された場所の名前で行われます(この挙動自体は実測で確認。設計意図の一次資料は未照合)。その代わり、公開するときは自分で絶対パスにする責任が作者に移ります。これは C のプリプロセッサの「テキスト置換で何でも通る」と、関数の「定義時に名前が確定する」の中間にあたります。

実務では、tokio::select!・anyhow::bail!・tracing::info! のようなライブラリが公開するマクロはすべてこの規則の上に作られています。$crate:: を書き忘れたマクロは、利用者が use を足すか別名にした瞬間に壊れます。

📝 問題

Part 1: 予想する(8問)

次の各スニペットは、cargo new s --edition 2024 の src/main.rs(Q7 だけは2クレートの workspace)に貼ったものとします。実行する前に、(a) コンパイルが通るか、(b) 通るなら標準出力、(c) 通らないならなぜかを、規則(テキスト順序/パスベース/衛生性/$crate)の名前で答えてください。

// Q1
fn main() {
    println!("{}", m!());
}
macro_rules! m { () => { 1 }; }
// Q2
mod a {
    macro_rules! m { () => { 2 }; }
}
fn main() {
    println!("{}", m!());
}
// Q3
fn main() {
    println!("{}", m!());
}
#[macro_use]
mod a {
    macro_rules! m { () => { 3 }; }
}
// Q4(2つの別ファイルとして試す。mod の位置が違うだけ)
// Q4-a: crate ルートの main から、定義より前の行で呼ぶ
fn main() { println!("{}", m!()); }
mod a {
    #[macro_export]
    macro_rules! m { () => { 4 }; }
}

// Q4-b: 同じ定義を、submodule の中の関数から bare name で呼ぶ
mod user {
    pub fn f() -> i32 { m!() }
}
mod a {
    #[macro_export]
    macro_rules! m { () => { 4 }; }
}
fn main() { println!("{}", user::f()); }
// Q5
macro_rules! m { () => { "first" }; }
fn a() -> &'static str { m!() }
macro_rules! m { () => { "second" }; }
fn b() -> &'static str { m!() }
fn main() { println!("{} {}", a(), b()); }
// Q6
macro_rules! hyg { ($e:expr) => {{ let x = 100; $e + x }}; }
macro_rules! mk_fn { () => { fn made() -> i32 { 9 } }; }
mk_fn!();
fn main() {
    let x = 1;
    println!("{} {}", hyg!(x), made());
}
// Q7: workspace = { util (lib), app (bin) }
// util/src/lib.rs
pub fn helper() -> &'static str { "util::helper" }
#[macro_export]
macro_rules! bad_call { () => { helper() }; }
#[macro_export]
macro_rules! good_call { () => { $crate::helper() }; }
// app/src/main.rs
fn main() {
    println!("{}", util::good_call!());   // (a)
    println!("{}", util::bad_call!());    // (b)
}
// Q8
mod a {
    macro_rules! m { () => { 11 }; }
    // (ここに `pub(crate) use m;` が (i) 無い/(ii) ある の2通りを予想する)
}
fn main() { println!("{}", a::m!()); }

Part 2: 小さなライブラリを直す(実装)

Q7 の util を、利用者がどう呼んでも壊れない形に作り直してください(cargo test が通ること)。

  • shout!("hi") が "<HI>"(String)を返す公開マクロ。内部で補助関数 decorate と補助マクロ __shout_inner! を使う
  • 補助関数・補助マクロは、利用者から見えるが「内部用」と分かるように置く(#[doc(hidden)])
  • クレート内だけで使う twice!(($e, $e) を返す)を作る。#[macro_export] は付けない。さらに、twice! の定義より前に書いたモジュールからも呼べること
  • app 側で依存を renamed_util = { path = "../util", package = "util" } と改名しても shout! が動くこと(テストを app/tests/ に書く)
  • 利用者が renamed_util::twice!(1) と書いたらコンパイルエラーになること(手で確認でよい)

Part 3: 設計の設問(コードの後に答える)

  • D1: クレート内のマクロを共有する方法は3つある。(A) #[macro_use] を mod に付ける、(B) #[macro_export] を付ける、(C) macro_rules! m {..} の直後に pub(crate) use m;。それぞれの代償を1行ずつ挙げ、新しいクレートで使うならどれか理由つきで選べ。
  • D2: $crate::__private::decorate(..) のように補助を pub にして #[doc(hidden)] で隠す設計は、semver 上どういう約束になるか。何が起きたら「破壊的変更」になるか。
  • D3: Q6 の made() が外から見えるのは「衛生性が弱い」のではなく仕様です。もし macro_rules! が関数名にも衛生的だったら、何が書けなくなるか(Day 057 の hashmap! や Day 059 の fsm! を思い出して答える。hashmap! の中身は今日読み返していない)。

💡 ヒント

ヒント1(方向性)
  • Part 1 は規則を2つに分けて考える。「#[macro_export] が付いているか」で、テキスト順序とパスベースのどちらの規則が効くかが変わる
  • Q6 では「どの識別子が呼び出し側の名前空間に属するか」を考える。let の束縛と、fn の項目名は扱いが違う
ヒント2(アプローチ)
  • Q4 は、#[macro_export] マクロがクレートのルートの項目になることと、submodule からの bare name 解決がどこまで遡るかを分けて考える
  • Part 2 は、$crate を付ける場所と、mod を並べる順序の2つが論点。use crate::twice; の有無を切り替えて実験すると、どちらの規則で解決されているか分かる
  • D1 は「どこを見れば定義元が分かるか(grep 可能性)」「並べ替えで壊れるか」「外に漏れるか」の3軸で比べる
ヒント3(コード骨格)
// util/src/lib.rs の骨格
#[doc(hidden)]
pub mod __private { pub fn decorate(s: &str) -> String { /* ... */ } }

#[doc(hidden)]
#[macro_export]
macro_rules! __shout_inner { ($s:expr) => { /* $crate:: で補助を呼ぶ */ }; }

#[macro_export]
macro_rules! shout { ($s:expr) => { /* $crate::__shout_inner!(...) */ }; }

pub mod inner { /* use crate::twice; して twice!(21) */ }

macro_rules! twice { ($e:expr) => { ($e, $e) }; }
pub(crate) use twice;

✅ 模範解答

Part 1 の答え(出題者の環境で実際にコンパイルして確認した結果)

Q結果使われた規則
Q1エラー cannot find macro 'm' in this scope(consider moving the definition of 'm' before this call)テキスト順序。定義は呼び出しより後ろ
Q2エラー cannot find macro 'm' in this scope(have you added the #[macro_use] on the module/import?)テキスト順序のスコープは mod a の終わりで切れる。ブロックでも同じ(関数内の macro_rules! は関数の外で見えない。確認済み)
Q3エラー(Q2 と同じ文言)#[macro_use] で伸びるのは mod の行より後ろだけ。呼び出しが前なら届かない
Q4-a通る。出力 4#[macro_export] でクレートルートの項目になり、ルートの main からはパスベースで見える。順序に依らない
Q4-bエラー cannot find macro 'm' in this scope(use crate::m; を提案される)bare name のパスベース解決は今いるモジュールの中を探す。ルートにあっても submodule からは crate::m! か use crate::m; が要る
Q5通る。出力 first second同名の再定義は許される。呼び出しはその時点で最後に見えている定義を使う(シャドーイング)
Q6通る。出力 101 9hyg!(x) の x は呼び出し側の 1、マクロ内の let x = 100 とは別物なので 1+100。fn made は項目名なので呼び出し側から見える
Q7(a) util::helper/(b) エラー E0425 cannot find function 'helper' in this scopehelper() は呼び出し側(app)で解決される。$crate::helper() なら util のルートに固定される
Q8(i) エラー E0433 cannot find 'm' in 'a'/(ii) 通る。出力 11macro_rules! はパスで指せない。pub(crate) use m; で初めてパスベースの名前になる

Part 2 の答え

// util/src/lib.rs(実際に cargo test が通った全文)
//! 別クレートから安全に呼べる宣言的マクロの最小構成。

/// マクロの展開結果から呼ばれる「公開だが利用者には見せない」部品の置き場。
#[doc(hidden)]
pub mod __private {
    pub fn decorate(s: &str) -> String {
        format!("<{s}>")
    }
}

/// 内部用マクロ。crate ルートに出てしまうので名前に `__` を付けて目立たせ、
/// `$crate::` 経由でだけ呼ぶ。
#[doc(hidden)]
#[macro_export]
macro_rules! __shout_inner {
    ($s:expr) => {
        $crate::__private::decorate(&$s.to_uppercase())
    };
}

/// 公開マクロ。名前解決はすべて `$crate::`(と `::core::`)で絶対パスにする。
#[macro_export]
macro_rules! shout {
    ($s:expr) => {
        $crate::__shout_inner!($s)
    };
}

pub mod inner {
    use crate::twice; // 定義より前に書いたモジュールでも、パスで import すれば通る
    pub fn pair() -> (i32, i32) {
        twice!(21)
    }
}

// クレート内だけで使うマクロ: #[macro_export] を付けず、`pub(crate) use` で
// パスとして import できるようにする(#[macro_use] より、どこから来たかが見える)。
macro_rules! twice {
    ($e:expr) => {
        ($e, $e)
    };
}
pub(crate) use twice;

#[cfg(test)]
mod tests {
    #[test]
    fn pair_works() {
        assert_eq!(super::inner::pair(), (21, 21));
    }
}
# Cargo.toml(workspace ルート)
[workspace]
members = ["util", "app"]
resolver = "3"
# app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2024"

[dependencies]
renamed_util = { path = "../util", package = "util" }
// app/tests/rename.rs
// パッケージ名を renamed_util に変えても、$crate は正しい crate を指す。
#[test]
fn shout_survives_rename() {
    assert_eq!(renamed_util::shout!("hi"), "<HI>");
}

実測(出題者の環境: rustc 1.96.0 / cargo 1.96.0 / edition 2024 / resolver 3 / macOS、2026-10-02 に実行)

確認結果(実測)
cargo test(workspace 全体)util の単体1件・app の統合1件が成功、失敗 0
renamed_util::twice!(1)E0603 macro 'twice' is private(期待どおりエラー)
use crate::twice; を消すcannot find macro 'twice' in this scope(定義より前の mod inner はテキスト順序では届かない)
改名した依存で hardcoded!(中身が util::helper() と直書き)cannot find module or crate 'util' in this scope。$crate は成功
同名の #[macro_export] を別 mod に2つ置くE0428 the name 'm' is defined multiple times(ルートが1つの名前空間のため)
マクロが生成した #[macro_export] マクロを crate::inner_m! で呼ぶmacro-expanded 'macro_export' macros from the current crate cannot be referred to by absolute paths(将来ハードエラー化予告つきの lint)
同じクレート内でも、submodule から h::helper()(マクロ内に直書き)を呼ぶE0433 cannot find module or crate 'h'。別クレートでなくても、呼び出し側の名前空間で解決される

D1 の回答: 新しいクレートでは (C) pub(crate) use を選ぶ

案代償判定
(A) #[macro_use] を mod に付ける順序に依存する。mod を並べ替えると壊れる。呼び出し側からどのモジュール由来か分からない(grep しにくい)却下。既存コードを読むために知っておく
(B) #[macro_export]外部にも公開される(API になり semver の対象)。クレートルートの名前空間に出るので同名衝突する(実測 E0428)外部に出すマクロだけ
(C) macro_rules! の直後に pub(crate) use m;1行増える。定義と同じ場所に use を書く癖が要る採用。クレート内限定で、パスで指せて、順序に依存しない(実測: mod inner を定義より前に置いても use crate::twice; で通る)

前提が変わったら判断も変わる。マクロを外部にも出すなら (B)+$crate。edition 2015 のコードを保守するなら (A) の知識がそのまま要る(今日は 2015 を実行していない)。

D2 の回答: __private は「見えるが約束しない」領域

#[doc(hidden)] はドキュメントに出さないだけで、pub なので外から呼べる。semver 上の約束は「公開 API の一部ではないので、いつ変えても非破壊」と README/crate docs に書いて初めて成立する。実際にマクロの展開結果が $crate::__private::decorate を呼んでいるので、利用者の手元の古いバージョンの展開結果が、新しい util と混ざることは無い(マクロはコンパイル時に util の同一バージョンから展開される)。壊れるのは、利用者が __private を直接呼んでいたときだけ。

D3 の回答: 項目名が衛生的だと、DSL が書けなくなる

Day 059 の fsm! は JobState のような名前を呼び出し側が決め、マクロが enum と impl を生成する。項目名(fn・enum・struct)まで衛生的にすると、マクロが作った made() を外から呼べない。呼び出し側の名前を引数で渡す $n:ident 方式は動くが、引数に取らない名前(今日の made のような固定名)が使えなくなり、補助関数の自動生成ができなくなる。

🔍 Step-by-Step 解説

Step 1: 「#[macro_export] が付いているか」で規則が分かれる

付いていない macro_rules! はテキスト順序(上から下・ブロック内)、付いているとクレートルートの項目になりパスで指せる。Q1〜Q3 は前者、Q4 と Q8(ii) は後者。

Step 2: テキスト順序のスコープは「定義の次の行から、含むブロックの終わりまで」

Q1 は定義が後ろなので届かない。Q2 は mod a が閉じた時点でスコープが終わる。Q3 の #[macro_use] は、その終わりを親の mod の行より後ろまで延ばすだけで、前には戻らない。

Step 3: 同名なら後勝ち(Q5)

テキスト順序の規則では、同名の定義を重ねると、呼び出し時点で一番新しい定義が使われる。a() は first、b() は second。ライブラリ側で意図せず別のマクロを隠していないか、macro_rules! の名前は短くしすぎない。

Step 4: 衛生性は「変数」だけ(Q6)

let x = 100 はマクロ側の x、$e は呼び出し側の x。混ざらず 101。一方 fn made は項目で、呼び出し側の名前空間に入る。

Step 5: 展開結果のパスは、呼び出し側で解決される(Q7)

helper() と書くと「呼び出し側のスコープの helper」。別クレートだけでなく、同じクレートの別モジュールでも壊れる(実測: h::helper())。$crate:: を付けて util のルートに固定する。

Step 6: 内部用の補助も $crate 経由にする

補助マクロ __shout_inner! は #[macro_export] が要る($crate::__shout_inner! のようにパスで呼ぶため)。ルートの名前空間が汚れるので __ で始め #[doc(hidden)] を付ける。補助関数は __private モジュールにまとめる。

Step 7: クレート内専用は pub(crate) use で名前を与える

macro_rules! 自体はパスで指せない。直後に pub(crate) use twice; を置くと、crate::twice というパスベースの名前ができる。mod inner を定義より前に置いても、use crate::twice; で解決できる(実測)。ただし定義より後ろのモジュールでは use が不要で、use crate::twice; を書くと unused import 警告が出る(今日の最初の版で実際に出た)。

🧭 設計思想・なぜこう書くのか

名前解決を「展開時にその場で」にした代償を、作者が払う

macro_rules! は名前を展開された場所で解決するので、DSL(fsm! のように呼び出し側の名前を使うもの)が自然に書けます。その代わり、公開マクロの作者は絶対パスを自分で書く責任を負います。$crate:: を使い、外部クレートは ::core:: / ::std:: のように先頭 :: を付ける(呼び出し側に core という名前のモジュールがあっても衝突しないため)。

判断の記録: 今日の問題の作り方

候補内容採否
採用8問の予想+小さな修正課題規則が「2種類のスコープ」で、言葉で覚えるより実際に打ち分けて確かめた結果と照らす方が定着する。Day 059 の宿題(別クレートからの $crate/#[macro_export] の実測)を回収できる
却下展開結果を cargo expand で見る問題今日は名前解決が主題。cargo expand は未導入のまま(宿題として残す)
却下5問目として予告した設計問題(マクロにするか関数・型にするか)9/29 のバックフィルは独立した1本として作る方針。設計問題は state の次回予告のまま次に回す

🛠 コンパイルエラーが出た場合

エラー1: Q1・Q2・Q3 の cannot find macro

error: cannot find macro `m` in this scope --> src/main.rs:2:20 | 2 | println!("{}", m!()); | ^ consider moving the definition of `m` before this call

読み方: 「同名のマクロが後ろにある」と教えてくれている(Q1)。have you added the #[macro_use] on the module/import? が出たら Q2・Q3 の型。定義を呼び出しより前へ移すか、#[macro_export]+パスで指すか、pub(crate) use を使う。

エラー2: Q7(b) の E0425

error[E0425]: cannot find function `helper` in this scope --> app/src/main.rs:2:20 | 2 | println!("{}", util::bad_call!()); | ^^^^^^^^^^^^^^^^^ not found in this scope

読み方: エラーの場所が呼び出し側の行を指している。マクロの中身(util 側)ではなく、展開後のコードが呼び出し側で解決されたことが分かる。$crate::helper() に直す。

エラー3: Q8(i) の E0433

error[E0433]: cannot find `m` in `a`

macro_rules! はそのままではパスで指せない。pub(crate) use m; を足す。

警告: マクロ生成の #[macro_export] を絶対パスで呼んだ

macro-expanded 'macro_export' macros from the current crate cannot be referred to by absolute paths は将来ハードエラーになる予告。マクロがマクロを #[macro_export] 付きで生成する設計を避ける。

🔁 他言語との比較

観点Rust macro_rules!C プリプロセッサGoTypeScript
名前の見え方2種類の規則(テキスト順序/パス)。$crate で定義元に固定できるファイルの上から下のテキスト置換のみ(#include した順)マクロ自体が無い(go generate で外部生成)マクロ無し(型レベルの再利用のみ。コンパイラ拡張は別)
衛生性ローカル変数のみ衛生的。項目名・パスは呼び出し側なし(#define の x が呼び出し側の x を食う)——
公開時の落とし穴$crate の書き忘れ。ルート名前空間の衝突ヘッダガード・名前の接頭辞(MYLIB_ )で運用する——

C でライブラリが全マクロに接頭辞を付けるのと、Rust で内部補助に __ と #[doc(hidden)] を付けるのは、同じ問題への別の解です。

🏭 実務での使いどころ

  • anyhow::bail!・tokio::join!・tracing::info! など、公開マクロはすべて $crate:: で補助を指す。依存を改名しても動くのはこのため
  • 社内の共通クレートで macro_rules! を出すときのレビュー項目: (1) $crate:: で全パスを固定したか、(2) 内部補助は __ + #[doc(hidden)] か、(3) 先頭に ::core::/::std:: を付けたか、(4) 外部に出さないマクロに #[macro_export] が付いていないか
  • 既存コードで #[macro_use] を見たら「古い書き方。mod の並べ替えで壊れる」と読む

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「マクロも関数と同じで、後ろに定義してもよい」他の項目は順序に依らないから#[macro_export] の無いマクロは上から下(Q1)
「#[macro_use] を付ければどこからでも見える」名前が「use」だから伸びるのは mod の行より後ろだけ(Q3)
「#[macro_export] を付けたらモジュールの中に置かれる」定義した場所に置かれると思うからクレートルートに置かれる。同名は衝突(実測 E0428)
「同じクレートの中なら $crate は要らない」別クレート用の話だと思うから呼び出す場所が別モジュールなら、直書きのパスは失敗する(実測 h::helper())
「衛生的だから、マクロ内の名前は全部安全」let は確かに安全だからfn・struct・パスは呼び出し側(Q6・Q7)
「pub(crate) use m; は不要」テキスト順序で届くことが多いから定義より前のモジュールから呼ぶ・a::m! と書くときは必要(Q8)

📖 一次資料(参照日: 2026-10-02。記述は各資料の要旨で、原文の引用ではありません。今回は取得し直して照合していません)

  • The Rust Reference「Macros By Example」の Scoping 節(textual scope と path-based scope)— doc.rust-lang.org/reference/macros-by-example.html
  • The Rust Reference「Attributes」のうち macro_use・macro_export — doc.rust-lang.org/reference/macros-by-example.html#the-macro_use-attribute ほか
  • The Little Book of Rust Macros「Hygiene and ${crate}」— veykril.github.io/tlborm
  • 上記の記述と実測が食い違った場合は実測(上の表)を優先。コンパイラのバージョンで文言は変わりうる

🚧 今日扱わなかった範囲(次回以降)

扱わなかったこと理由・次の予定
edition 2015/2018 での #[macro_use] extern crate今日は edition 2024 のみ実行。古い書き方は読む用の知識として触れただけ
手続き的マクロ(proc-macro)の衛生性(Span::call_site / mixed_site)Phase 3 #13 マクロ②で扱う
macro_rules! の $crate と pub use の再エクスポート(reexport した crate 経由の呼び出し)今日は直接依存と改名のみ実測
trybuild による「コンパイルが通らないこと」の自動テスト今日のエラー再現は手動。外部クレートなしの方針で未導入のまま(宿題)
設計問題(マクロにするか、関数・型にするか)state の次回予告どおり、次の出題で扱う
#[macro_export(local_inner_macros)]2018 以降は $crate:: 方式が標準で、今日は扱わない

3 問の自己点検(/ai-smell-audit 相当)

  1. 同じ指示を AI に出せば数時間で出るか — 規則の一覧自体は出る。ただし「use crate::twice; が定義より後ろだと unused import になる」「#[macro_export] 同名で E0428」など実機で当たって初めて分かった点を結果表に残してある
  2. AI に指示するだけでは出ない部分 — 出題者の環境での実測表(9 件)と、3案比較(D1)の却下理由
  3. 世界の実物と比べて何割か — macro_rules! の名前解決に関する中核概念(テキスト順序・パス・#[macro_use]・#[macro_export]・$crate・衛生性)6 つのうち 6 つを扱ったが、local_inner_macros・2015 edition・proc-macro の衛生性・再エクスポートは扱っていない(上の表)

➡️ 次のステップ

  • 発展: 公開クレートを cargo public-api や cargo semver-checks にかけ、__private の変更が検出されるか確かめる(今日は未実施)
  • 次回予告: (state の次回予告のまま)マクロ① の設計問題 — 「マクロにするか、関数・型にするか」を既出の hashmap!・count!・fsm! で判断する

🎯 自己評価(解いた後に記入)

自分の回答

気づき・メモ