---
title: "Rustのマクロの書き方｜macro_rules!と手続き型マクロ（proc macro）をsyn 3で実装"
url: "https://www.issoh.co.jp/tech/details/9463/"
published: 2025-10-24
updated: 2026-11-08
categories: ["Rust"]
publisher: "株式会社一創"
---

# Rustのマクロの書き方｜macro\_rules!と手続き型マクロ（proc macro）をsyn 3で実装

Rustのマクロは、コンパイル時にコードを生成する仕組みです。`println!`や`vec!`のように`macro_rules!`で定義する宣言的マクロと、`#[derive(Serialize)]`や`#[tokio::main]`の裏で動く手続き型マクロ（procedural macro、proc macro）の2系統があります。

この記事では両方を実装し、rustc 1.99.1（2026年10月15日公開）とsyn 3.0.9でビルドした出力を載せます。`macro_rules!`は再帰・繰り返し・他クレートへの公開まで、手続き型マクロは派生・属性・関数風の3種類と、2026年7月公開のsyn 3で壊れる箇所を扱います。Rustそのものが初めての方は、先に[Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)で所有権と基本文法を押さえてください。

## まとめ：Rustのマクロの要点

- 選ぶ順番は、関数・ジェネリクス → `macro_rules!` → 手続き型マクロです。前の手段で書けるなら次へ進みません。
- `macro_rules!`は「パターン => 展開内容」の対応表です。引数の個数を変えたい、構造体や`impl`を並べて生成したいときに使い、追加の依存はありません。
- 他のクレートへ公開するには`#[macro_export]`を付け、マクロの中から自分のクレートの項目を指すときは`$crate`を使います。同じクレート内で名前だけで呼ぶ場合は、定義より前の行では見えません。
- edition 2024では`expr`指定子が`const { ... }`と`_`にも一致するようになりました。2021までの挙動は`expr_2021`で残せます。
- 手続き型マクロは`proc-macro = true`の専用クレートに書き、入力の解析をsyn、出力の組み立てをquoteに任せます。2026年11月8日時点の最新はsyn 3.0.9・quote 1.0.47です。
- syn 3では`ItemFn`に`modifiers`フィールドが増え、フィールドを列挙して分解するコードはE0027で止まります。
- エラーは`panic!`でなく`syn::Error`で返し、trybuildでエラー文面を固定し、cargo expandで展開結果を確かめます。proc-macro-errorとそのforkのproc-macro-error2は、どちらも保守終了の勧告が出ています。

## Rustのマクロの種類と関数との違い

宣言的マクロはパターンとテンプレートの対応表で、コンパイラがパターン照合して展開します。手続き型マクロは普通のRustコードとしてコンパイルされたプログラムで、コンパイラがそれを呼び出し、戻り値のトークン列を展開結果として使います。

| 観点             | 関数     | macro\_rules! | 手続き型マクロ                 |
| -------------- | ------ | ------------- | ----------------------- |
| 呼び出し形式         | f(x)   | 名前!(…)        | derive・#\[名前\]・名前!(…)   |
| 引数の個数          | 固定     | 可変            | 可変                      |
| 受け取るもの         | 型の付いた値 | トークン列（パターン照合） | トークン列（任意のコードで解析）        |
| structやimplの生成 | 不可     | 可             | 可                       |
| フィールド名の走査      | 不可     | 困難            | synで可                   |
| 定義場所           | どこでも   | 同じクレート内で可     | proc-macro専用クレート        |
| 追加の依存          | なし     | なし            | 通常syn・quote・proc-macro2 |
| 衛生性            | －      | ローカル変数は衛生的    | 非衛生的                    |

### 関数・ジェネリクスとマクロの選択基準

関数で書けないのは、引数の個数を呼び出しごとに変えたい場合、構造体・`impl`・関数といった項目そのものを生成したい場合、構造体のフィールド名など構文の中身を読みたい場合の3つです。前の2つは`macro_rules!`で足り、3つ目で初めて手続き型マクロが要ります。

次の場合はマクロ以外を選びます。

- 型ごとに同じ処理を書きたいだけ：ジェネリクスとトレイトの既定実装で済みます。エラーも通常の型エラーとして出ます。ジェネリクスの実行コストは[ゼロコスト抽象化とは？Rustの単相化とdynの差をアセンブリと実測で解説](/tech/details/4440/)で扱っています。
- 生成元が外部ファイル（.protoやSQLスキーマ）：ビルドスクリプト（build.rs）で生成して`include!`で取り込む方が、生成物をそのまま読めます。

C言語の`#define`は字句をそのまま置き換えますが、Rustのマクロはトークン木を受け取り、展開結果も構文として検査されます。C側の置き換えの性質は[C言語のインライン関数とは？inline・static inlineの使い方とマクロとの違い](/tech/details/5422/)、言語ごとのコード生成の比較は[メタプログラミングとは？仕組みとRuby・Python・JavaScript・C++・Rustの実装例](/tech/details/3510/)にまとめています。

## macro\_rules!の基本構文と実装例

### max!の実装：複数アームと再帰

`macro_rules!`の本体は「`(パターン) => { 展開内容 }`」を`;`で並べたものです。呼び出しは上のアームから順に照合され、最初に一致したアームが展開されます。次の`max!`は、引数が1つならそれを返し、2つ以上なら先頭と残りの最大値を比べます。残りの最大値は`max!`自身を呼び直して求めます。

```
macro_rules! max {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {{
        let a = $x;
        let b = max!($($rest),+);
        if a > b { a } else { b }
    }};
}

fn main() {
    println!("{}", max!(3, 9, 4)); // 9
}
```

`$x:expr`の`$x`をメタ変数、`expr`をフラグメント指定子と呼びます。展開内容を`{{ ... }}`と二重の波括弧にしているのは、外側がマクロの区切り、内側が式としてのブロックだからです。再帰の深さには上限があり、[Rust Referenceのlimits属性の章](https://doc.rust-lang.org/reference/attributes/limits.html)によるとrustcの既定は128です。超える場合はクレートの先頭に`#![recursion_limit = "256"]`のように書いて引き上げます。

### フラグメント指定子の一覧とedition 2024での変更

[Rust Referenceのmacros by exampleの章](https://doc.rust-lang.org/reference/macros-by-example.html)は、次の15種類の指定子を定めています。

| 指定子        | 一致するもの                    | 例                   |
| ---------- | ------------------------- | ------------------- |
| block      | 波括弧のブロック                  | { a + 1 }           |
| expr       | 式                         | a + 1               |
| expr\_2021 | トップレベルのconstブロック式・\_式を除く式 | a + 1               |
| ident      | 識別子                       | port                |
| item       | 関数・構造体などの項目               | struct S;           |
| lifetime   | ライフタイム                    | ‘a                  |
| literal    | リテラル                      | 42、”a”              |
| meta       | 属性の中身                     | cfg(test)           |
| pat        | パターン（A \| B を含む）          | Some(x) \| None     |
| pat\_param | トップレベルの \| を含まないパターン      | Some(x)             |
| path       | パス                        | std::io::Result     |
| stmt       | 文（原則として末尾の ; を除く）         | let x = 1、struct S; |
| tt         | トークン木1つ                   | x、(a, b)            |
| ty         | 型                         | Vec<u8>             |
| vis        | 可視性（空も可）                  | pub(crate)          |

指定子の後ろに置けるトークンには制限があります。`expr`と`stmt`の直後に置けるのは`=>`・`,`・`;`だけです。`$a:expr - $b:expr`のように書くとマクロの定義時点でエラーになるため、区切りにはカンマを使います。

`expr`の範囲はeditionで変わりました。[Edition Guideのmacro fragment specifiersの節](https://doc.rust-lang.org/edition-guide/rust-2024/macro-fragment-specifiers.html)によると、edition 2024の`expr`は`const { ... }`と`_`にも一致し、従来の挙動のために`expr_2021`が追加されています。次のコードで差が出ます。

```
macro_rules! which {
    ($e:expr) => { "expr" };
    (const $b:block) => { "const block" };
}
macro_rules! which21 {
    ($e:expr_2021) => { "expr" };
    (const $b:block) => { "const block" };
}
fn main() {
    println!("{} {}", which!(const { 1 }), which21!(const { 1 }));
}
```

`rustc --edition 2024`では`expr const block`、`--edition 2021`では`const block const block`と表示されます。edition 2024では1つ目のアームが`const { 1 }`を先に取ってしまい、2つ目のアームに届きません。`const`で始まる入力を別扱いするアームを持つマクロは、edition移行時に`expr_2021`へ書き換えるか、アームの順番を入れ替えます。

### 繰り返し$(…),\*による構造体とゲッターの生成

可変個の入力は`$( ... ),*`で受けます。区切り文字（ここでは`,`）の後ろの`*`は0回以上、`+`は1回以上、`?`は0回か1回で、区切り文字は指定できません。展開内容の側でも同じ`$( ... )*`を書くと、受け取った回数だけ繰り返して出力します。

```
macro_rules! getters {
    ($name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
        struct $name { $($field: $ty),* }
        impl $name {
            $( fn $field(&self) -> &$ty { &self.$field } )*
        }
    };
}

getters!(Config { host: String, port: u16 });

fn main() {
    let c = Config { host: "localhost".into(), port: 8080 };
    println!("{}:{}", c.host(), c.port()); // localhost:8080
}
```

パターン末尾の`$(,)?`は、最後の要素の後ろのカンマを許すための書き方です。これがないと`getters!(Config { host: String, })`は一致しません。1つのマクロ呼び出しから構造体と`impl`の2つの項目を生成している点が、関数では書けない部分です。

### macro\_rules!の衛生性とローカル変数の名前解決

Rust Referenceは`macro_rules!`の衛生性を「mixed-site hygiene」と呼び、ローカル変数とループ・ブロックのラベルはマクロの定義側で、それ以外の名前は呼び出し側で解決すると定めています。

```
macro_rules! add_ten {
    ($e:expr) => {{ let base = 10; base + $e }};
}

fn main() {
    let base = 1;
    println!("{}", add_ten!(base)); // 11
}
```

表示は11です。字句を置き換えるだけのマクロなら`let base = 10; base + base`となって20になるところを、マクロ内の`base`と呼び出し側の`base`は別の変数として扱われます。一方、関数名や型名は呼び出し側で解決されるため、マクロ内で`HashMap::new()`と書くと、呼び出し側で`use`していなければ見つかりません。標準ライブラリの項目は`::std::collections::HashMap`のように先頭からの完全なパスで書き、自分のクレートの項目は次に述べる`$crate`で指します。

### #\[macro\_export\]による公開と$crateによる定義元参照

ライブラリから配るマクロには`#[macro_export]`を付けます。マクロはクレートのルートに公開され、利用者は`use decl_lib::hashmap;`のようにパスで取り込めます。マクロの中で自分のクレートの関数を呼ぶときは、クレート名を直書きせず`$crate`を使います。利用者が依存の名前を変えていても正しいクレートを指します。

```
// decl_lib/src/lib.rs
#[doc(hidden)]
pub fn __new_map<K, V>() -> std::collections::HashMap<K, V> {
    std::collections::HashMap::new()
}

/// キーと値の組から HashMap を作る（末尾カンマ可）
#[macro_export]
macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut map = $crate::__new_map();
        $( map.insert($key, $value); )*
        map
    }};
}
```

Rust Referenceは、`$crate`は定義元のクレートを指すだけで、参照先の可視性の制限は外さないと明記しています。マクロから呼ぶ補助関数は`pub`にする必要があり、利用者に見せたくなければ`#[doc(hidden)]`で文書から隠します。別のクレートから`hashmap! { "http" => 80, "https" => 443, }`と呼ぶと、展開後は`::decl_lib::__new_map()`になります。

同じクレートの中では順番に注意が要ります。`macro_rules!`を名前だけで呼ぶと、定義した行より後ろの範囲（テキストスコープ）から探されるため、定義の前で呼ぶと次のエラーになります。

```
error: cannot find macro `sq` in this scope
 --> order.rs:1:28
  |
1 | fn main() { println!("{}", sq!(3)); }
  |                            ^^ consider moving the definition of `sq` before this call
```

`#[macro_export]`を付けたマクロはクレートのルートにも置かれるため、`crate::sq!(3)`のようにパスで書けば定義より前の行からでも呼べます。モジュールの中で定義したマクロを名前だけで外のモジュールから使うには、`mod`宣言に`#[macro_use]`を付けて見える範囲を広げます。

### 安定版ではまだ使えない宣言マクロの機能

`macro_rules!`で属性マクロや派生マクロを書けるようにするRFC 3697・3698と、繰り返し回数を数える`${count(...)}`などのメタ変数式は、rustc 1.99.1ではまだ使えません。[rustcの未安定機能の一覧](https://github.com/rust-lang/rust/blob/main/compiler/rustc%5Ffeature/src/unstable.rs)で、それぞれ`macro_attr`・`macro_derive`・`macro_metavar_expr`という機能ゲートのまま残っています。安定版で`attr()`のアームを書くと次のエラーになります。

```
error[E0658]: `macro_rules!` attributes are unstable
 --> attr.rs:1:1
  |
1 | / macro_rules! noop_attr {
2 | |     attr() ($($item:tt)*) => { $($item)* };
3 | | }
  | |_^
  |
  = note: see issue #143547 <https://github.com/rust-lang/rust/issues/143547> for more information
```

派生マクロの`derive()`は#143549、`${count(...)}`は#83527で同じE0658になります。安定版で`#[名前]`や`#[derive(名前)]`の形のマクロを作るなら、今も手続き型マクロを書くしかありません。

## 手続き型マクロ3種類の定義方法と使い分け

3種類は付ける属性と関数の引数の数が違います。戻り値はどれも`proc_macro::TokenStream`です。

| 種類     | 定義側の属性                       | 引数           | 呼び出し例                   | 出力の扱い       |
| ------ | ---------------------------- | ------------ | ----------------------- | ----------- |
| 派生マクロ  | #\[proc\_macro\_derive(名前)\] | 1つ（対象の型）     | #\[derive(FieldNames)\] | 元の型の後ろに追加   |
| 属性マクロ  | #\[proc\_macro\_attribute\]  | 2つ（属性の引数、対象） | #\[timed\]              | 対象を置き換え     |
| 関数風マクロ | #\[proc\_macro\]             | 1つ（括弧の中身）    | upper!(“…”)             | 呼び出し箇所を置き換え |

使い分けは「元のコードを残すかどうか」で決まります。派生マクロは型定義に手を加えられず、`impl`ブロックなどを後ろに足すことしかできません。トレイト実装の自動生成はこれで足り、Serdeの`Serialize`や`Deserialize`が代表例です（使う側の書き方は[Serdeの読み方と使い方｜Rustのderive・rename\_all・defaultを実行結果で解説](/tech/details/5551/)で扱っています）。関数の中身を包む、構造体にフィールドを足すなど元のコードを書き換えたいときは属性マクロを使います。`#[tokio::main]`は`async fn main`をランタイム起動コード付きの同期関数に置き換える属性マクロです。

派生マクロには補助属性を登録できます。`#[proc_macro_derive(Builder, attributes(builder))]`と宣言すると、利用者はフィールドに`#[builder(default)]`のような属性を書けるようになり、マクロ側はそれを読んで生成内容を変えられます。`#[serde(rename = "...")]`はこの仕組みです。補助属性の解析を宣言的に書きたいときはdarling（最新0.24.1、syn 3対応済み）が使えます。

## syn 3とquoteによる手続き型マクロの実装

3種類のマクロを1つのクレートにまとめて実装します。構成は、マクロを定義する`my_macros`と、それを使う`app`の2クレートを持つワークスペースです。

```
# Cargo.toml（ワークスペースのルート）
[workspace]
members = ["my_macros", "app"]
resolver = "3"

# app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2024"

[dependencies]
my_macros = { path = "../my_macros" }
```

### Cargo.tomlのproc-macro指定と依存クレート

マクロ側のCargo.tomlでは`[lib]`に`proc-macro = true`を書きます。[Rust Referenceの手続き型マクロの章](https://doc.rust-lang.org/reference/procedural-macros.html)は、手続き型マクロはcrate typeがproc-macroのクレートのルートに定義し、定義したクレート自身からは使えないと定めています。マクロとそれを使うコードが別クレートになるのはこのためです。クレートの追加やワークスペースの操作は[Cargo（Rust）とは？インストールとcargo installの使い方・主要コマンド一覧](/tech/details/9572/)を参照してください。

```
# my_macros/Cargo.toml
[package]
name = "my_macros"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "3", features = ["full"] }

[dev-dependencies]
trybuild = "1"
```

synの`full`機能は、関数や文など型定義以外の構文を扱うときに必要です。既定機能でも`DeriveInput`や型・式の多くは扱えますが、関数全体を表す`ItemFn`やブロックを表す`Block`は`full`に含まれます。`full`を外したまま`ItemFn`を使うと`` error[E0432]: unresolved import `syn::ItemFn` ``で止まります。

### 派生マクロ：構造体のフィールド名一覧を生成

構造体に`field_names()`という関連関数を生やす派生マクロです。`parse_macro_input!`で入力を`DeriveInput`に変換し、フィールドを走査して名前の配列を作り、`quote!`で`impl`ブロックを出力します。

```
// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, ItemFn, LitStr};

/// 構造体のフィールド名一覧を返す関連関数を生やす派生マクロ
#[proc_macro_derive(FieldNames)]
pub fn derive_field_names(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let fields = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(f) => &f.named,
            _ => {
                return syn::Error::new_spanned(name, "FieldNames は名前付きフィールドの構造体にだけ使えます")
                    .to_compile_error()
                    .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(name, "FieldNames は構造体にだけ使えます")
                .to_compile_error()
                .into();
        }
    };
    let names = fields.iter().map(|f| f.ident.as_ref().unwrap().to_string());

    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    quote! {
        impl #impl_generics #name #ty_generics #where_clause {
            pub fn field_names() -> &'static [&'static str] {
                &[#(#names),*]
            }
        }
    }
    .into()
}
```

`quote!`の中では`#変数`で値を埋め込み、`#(#names),*`でイテレータの要素をカンマ区切りで展開します。`macro_rules!`の`$( ... ),*`と同じ考え方です。`split_for_impl()`は、`struct Wrapper<T: Clone>`のようなジェネリクス付きの型にも正しい`impl<T: Clone> Wrapper<T>`を組み立てるための関数で、省くとジェネリクス付きの型に付けた時点でコンパイルエラーになります。

### 属性マクロ：関数の実行時間を計測

通常の同期関数と非同期関数が正常終了するまでの経過時間を標準エラーへ出す`#[timed]`です。非同期関数では待機時間も含み、panicやFutureの途中破棄では出力されません。`const fn`は対象外です。受け取った関数の本体を、計測コードで包んだ新しいブロックに差し替えます。

```
/// 関数の実行時間を標準エラーへ出す属性マクロ
#[proc_macro_attribute]
pub fn timed(attr: TokenStream, item: TokenStream) -> TokenStream {
    if !attr.is_empty() {
        return syn::Error::new(proc_macro2::Span::call_site(), "timed は引数を取りません")
            .to_compile_error()
            .into();
    }
    let mut f = parse_macro_input!(item as ItemFn);
    let fn_name = f.sig.ident.to_string();
    let block = &f.block;
    // mixed_site で作った識別子は、利用者のコードにある同名の変数と衝突しない
    let start = syn::Ident::new("start", proc_macro2::Span::mixed_site());
    let ret = syn::Ident::new("ret", proc_macro2::Span::mixed_site());
    // async fn は async ブロック、同期関数はクロージャで包む
    let call: syn::Expr = if f.sig.asyncness.is_some() {
        syn::parse_quote!(async move #block.await)
    } else {
        syn::parse_quote!((move || #block)())
    };
    let new_block: syn::Block = syn::parse_quote!({
        let #start = ::std::time::Instant::now();
        let #ret = #call;
        ::std::eprintln!("{} took {:?}", #fn_name, #start.elapsed());
        #ret
    });
    f.block = Box::new(new_block);
    quote!(#f).into()
}
```

受け取った`ItemFn`を分解して組み直さず、`block`だけを差し替えて`quote!(#f)`で丸ごと出力しています。属性や可視性、syn 3で増えた`modifiers`を取りこぼさないためです。本体をクロージャや`async`ブロックで包むのは、関数の途中に`return`があっても計測コードを通すためです。`async fn`の分岐を省いた場合に何が起きるかは後半の失敗例で示します。

計測用のローカル変数は`Span::mixed_site()`で作っています。手続き型マクロは非衛生的なので、`quote!`に`let __start = ...`と直接書くと、`fn echo(__start: u64) -> u64 { __start }`のように同名の引数を持つ関数に付けた時点で本体の`__start`がマクロの変数を指してしまい、rustc 1.99.1では`error[E0308]: mismatched types`になります。`mixed_site`（Rust 1.45.0で安定化）の識別子は`macro_rules!`と同じ規則で解決されるため、上のコードは引数名が`start`や`ret`の関数に付けても正しく動きます。

### 関数風マクロ：文字列をコンパイル時に大文字化

関数風マクロは`名前!(...)`の括弧の中身をそのまま受け取ります。ここでは文字列リテラルを受け取り、大文字にしたリテラルを返します。変換はコンパイル時に終わるため、実行時のコストはありません。

```
/// 文字列リテラルを大文字にした &str をコンパイル時に作る関数風マクロ
#[proc_macro]
pub fn upper(input: TokenStream) -> TokenStream {
    let lit = parse_macro_input!(input as LitStr);
    let up = lit.value().to_uppercase();
    quote!(#up).into()
}
```

入力が文字列リテラルでなければ`parse_macro_input!`がsynのエラーを返し、利用者には`expected string literal`と表示されます。構文の検査をsynに任せられるのが、トークンを手で読むより安全な理由です。

### 呼び出し側のコードと実行結果

使う側の`app`は、前述のCargo.tomlで`my_macros`をpath依存に指定し、通常の関数と同じように`use`で取り込みます。

```
// app/src/main.rs
use my_macros::{timed, upper, FieldNames};

#[derive(FieldNames)]
struct User {
    id: u64,
    name: String,
    email: String,
}

#[timed]
fn sum(n: u64) -> u64 {
    (1..=n).sum()
}

const GREETING: &str = upper!("hello, proc macro");

fn main() {
    println!("{:?}", User::field_names());
    println!("{}", sum(1_000_000));
    println!("{GREETING}");
    let _ = User { id: 1, name: String::new(), email: String::new() };
}
```

`cargo run -p app`の出力です（計測時間はデバッグビルドの値で、環境によって変わります）。

```
["id", "name", "email"]
sum took 11.66602ms
500000500000
HELLO, PROC MACRO
```

## TokenStreamからコード生成までの処理の流れ

手続き型マクロの実体は、コンパイラが読み込む動的ライブラリです。`cargo build`では次の順に処理が進みます。

1. Cargoが`my_macros`をホスト向けにビルドする（クロスコンパイル中でもビルドを実行するマシン向け）
2. rustcが`app`を構文解析し、`#[derive(FieldNames)]`などの呼び出しを見つける
3. 該当箇所のトークン列を`proc_macro::TokenStream`としてマクロ関数に渡す
4. 返ってきたトークン列を元の位置に展開し、名前解決と型検査に進む

マクロが受け取るのはトークン列で、型情報はまだありません。`id: u64`の`u64`が標準の整数型なのか同名の別の型なのかは、マクロからは判別できません。型に依存する処理はトレイト境界として生成コードに書き、判定はコンパイラの型検査に任せます。

`proc_macro`と`proc_macro2`の2つが出てくるのは、`proc_macro`がコンパイラから呼ばれている間しか使えないためです。synとquoteは`proc_macro2::TokenStream`を中心に作られており、単体テストや通常のプログラムからも動かせます。マクロの入口と出口でだけ`.into()`で相互に変換します。

Rust Referenceは、手続き型マクロはコンパイル中に動くためコンパイラと同じ資源（標準入出力やファイルアクセス）を持ち、ビルドスクリプトと同じセキュリティ上の懸念があると明記しています。無限ループはコンパイラに捕捉されず、ビルドが止まったままになります。依存に加える手続き型マクロのクレートは、ビルドスクリプトと同じ基準で出どころを確認してください。

## syn 2からsyn 3への移行で壊れる箇所

syn 3.0.0は2026年7月18日に公開され、11月8日時点の最新は3.0.9（10月12日公開）です。対応するRustの最低バージョンはsyn 2と同じ1.71のままで、変わったのは構文木の型です。

### ItemFnの分解で出るE0027

syn 2向けの属性マクロでよく見る、`ItemFn`をフィールド列挙で分解する書き方は、syn 3ではそのまま通りません。syn 3.0.9でビルドすると次のエラーになります。

```
error[E0027]: pattern does not mention field `modifiers`
  --> my_macros/src/lib.rs:83:9
   |
83 |     let ItemFn { attrs, vis, sig, block } = parse_macro_input!(item as ItemFn);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing field `modifiers`
```

syn 3では、関数や型などの構文に`FnModifiers`をはじめとする`〜Modifiers`構造体が10種類加わりました。いずれもnon-exhaustive（将来フィールドが増える前提）です。直し方は、分解に`..`を足すか、前章の`#[timed]`のように分解せず必要なフィールドだけ書き換えるかの2通りです。後者なら、将来`modifiers`に値が入るようになっても出力から落ちません。未対応の構文を拒否したいときは、各`〜Modifiers`の`require_empty()`で該当箇所を指したエラーを返せます。

### 名前や型が変わった主なAPI

[syn 3.0.0のリリースノート](https://github.com/dtolnay/syn/releases/tag/3.0.0)の破壊的変更のうち、手続き型マクロの実装で触れることが多いものです。

| syn 2                  | syn 3                         |
| ---------------------- | ----------------------------- |
| Type::BareFn           | Type::FnPtr に改名               |
| BareFnArg              | NamedArg に改名                  |
| Signature の unsafety   | 3値の Safety 列挙型                |
| Arm の guard フィールド      | Pat::Guard に移動                |
| Punctuated::pop        | Option<T> を返す（旧挙動は pop\_pair） |
| LitInt の From<Literal> | 削除（Lit::new で生成）              |
| Lifetime のパース          | キーワードを拒否（parse\_any で許可）      |
| visit 系の Span 処理       | メソッド削除                        |

`DeriveInput`とフィールドしか触らない派生マクロなら影響は小さく済みます。関数のシグネチャや式を細かく読む属性マクロほど、移行時の修正箇所が増えます。

### 主要クレートの移行状況とsynの二重ビルド

crates.ioのAPIで、各クレートの最新版が依存するsynを確認しました（2026年11月8日時点）。

| クレート                       | 最新版     | 依存するsyn |
| -------------------------- | ------- | ------- |
| serde\_derive              | 1.0.229 | ^3      |
| thiserror-impl             | 2.0.21  | ^3      |
| tokio-macros               | 2.7.2   | ^3.0    |
| clap\_derive               | 4.6.7   | ^3.0.2  |
| pyo3-macros-backend        | 0.30.1  | ^3      |
| wasm-bindgen-macro-support | 0.2.130 | ^3.0    |
| darling\_core              | 0.24.1  | ^3.0.0  |
| pin-project-internal       | 1.1.13  | ^2.0.1  |
| derive\_more-impl          | 2.1.1   | ^2.0.45 |
| strum\_macros              | 0.28.0  | ^2.0    |

synの2系と3系はメジャーバージョンが違うため、Cargoは両方を別のクレートとしてビルドします。serde（derive機能付き）とpin-projectを同時に入れたプロジェクトで`cargo tree -d -e normal`を実行すると、両方が並びます。

```
syn v2.0.119
└── pin-project-internal v1.1.13 (proc-macro)
    └── pin-project v1.1.13
        └── syn_dup v0.1.0 (/private/tmp/9463_verify/syn_dup)

syn v3.0.9
└── serde_derive v1.0.229 (proc-macro)
    └── serde v1.0.229
        └── syn_dup v0.1.0 (/private/tmp/9463_verify/syn_dup)
```

新しく書くマクロはsyn 3で始めてください。serde・tokio・clap・pyo3の派生マクロが3系に移っているため、2系を選ぶと利用者にsyn 2を1本余計にビルドさせることになりがちです。既存のsyn 2のマクロは、pin-projectやderive\_more、strumのように2系に残るクレートもあるので、利用者の依存ツリーからsyn 2が消えるまで急いで移す理由はありません。

## エラー報告の書き方：panicとsyn::Error

Rust Referenceは、手続き型マクロのエラー報告手段としてpanicと`compile_error!`の2つを挙げています。`syn::Error::to_compile_error()`は後者を生成する関数です。enumに付けるという同じ誤用を、両方の方法で報告させました。panicで止めた場合の表示です。

```
error: proc-macro derive panicked
 --> app/examples/bad.rs:3:10
  |
3 | #[derive(Panicky)]
  |          ^^^^^^^
  |
  = help: message: Panicky は構造体にだけ使えます
```

`syn::Error::new_spanned`で返した場合（前章の`FieldNames`）の表示です。

```
error: FieldNames は構造体にだけ使えます
 --> tests/ui/fail_enum.rs:4:6
  |
4 | enum Color {
  |      ^^^^^
```

panicでは「マクロがpanicした」という見出しが先に出て、指す位置も`derive`の属性です。`syn::Error`なら自分のメッセージが見出しになり、問題のある`Color`を直接指せます。エラーが複数あるときは`syn::Error::combine`でまとめて返せば、利用者は1回のビルドで全部を確認できます。syn 3.0.2では、開始と終了のトークンで範囲を指す`Error::new_range`も追加されました。

エラー報告の補助クレートとして長く使われてきたproc-macro-errorは、[RustSecの情報勧告RUSTSEC-2024-0370](https://rustsec.org/advisories/RUSTSEC-2024-0370.html)（2024年9月5日公開、種別Unmaintained）の対象です。その保守版として作られたproc-macro-error2も、作者が保守終了を認めたとして[RUSTSEC-2026-0173](https://rustsec.org/advisories/RUSTSEC-2026-0173.html)（2026年6月7日公開）の対象になりました。どちらの勧告も代替としてmanyhowとproc-macro2-diagnosticsを挙げており、crates.io上の最新版（manyhow 0.14.1、proc-macro2-diagnostics 0.11.0）はどちらもsyn 3に依存しています。

## テストとデバッグ：trybuildと展開結果の確認

### trybuildでコンパイルエラーの文面を固定

手続き型マクロのテストでは、正しい入力でビルドが通ることに加えて、誤った入力で意図したエラーが出ることを確かめます。後者は通常の`#[test]`では書けないため、trybuild（最新1.0.122）を使います。

```
// my_macros/tests/ui.rs
#[test]
fn ui() {
    let t = trybuild::TestCases::new();
    t.pass("tests/ui/pass_struct.rs");
    t.compile_fail("tests/ui/fail_enum.rs");
}
```

テスト対象の2ファイルです。`pass_struct.rs`はビルドと実行が通ること、`fail_enum.rs`はコンパイルエラーになることを確かめます。

```
// my_macros/tests/ui/pass_struct.rs
use my_macros::FieldNames;

#[derive(FieldNames)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    assert_eq!(Point::field_names(), &["x", "y"]);
}

// my_macros/tests/ui/fail_enum.rs
use my_macros::FieldNames;

#[derive(FieldNames)]
enum Color {
    Red,
}

fn main() {}
```

`compile_fail`に渡したファイルは、同名の`.stderr`ファイルとコンパイラ出力を突き合わせます。初回は`TRYBUILD=overwrite cargo test`で期待値を書き出し、中身を確認してからコミットします。前章の`FieldNames`のエラー表示は、このとき生成された`fail_enum.stderr`の内容です。rustcのバージョンが上がると文面が変わることがあるため、CIのツールチェーンは固定しておきます。

### cargo expandで展開後のコードを確認

マクロが何を生成したかは、cargo expand（最新1.0.127）で確認します。`cargo install cargo-expand`で導入し、`cargo expand -p app`のように実行します。中身は`-Zunpretty=expanded`を付けたrustcの呼び出しです。`-Z`は本来nightly専用のオプションで、安定版のrustcに直接渡すと`` the option `Z` is only accepted on the nightly compiler ``で拒否されますが、cargo expandは環境変数`RUSTC_BOOTSTRAP=1`を設定して安定版でも通しています。

本記事の`app`に前章の`max!`と`add_ten!`も加えて展開した結果の一部です（後の2つは`println!`の引数の中から該当部分を抜き出し、コメントを足しています）。

```
impl User {
    pub fn field_names() -> &'static [&'static str] {
        &["id", "name", "email"]
    }
}
fn sum(n: u64) -> u64 {
    let start = ::std::time::Instant::now();
    let ret = (move || { (1..=n).sum() })();
    {
        ::std::io::_eprint(format_args!("{0} took {1:?}\n", "sum", start.elapsed()));
    };
    ret
}
const GREETING: &str = "HELLO, PROC MACRO";
// max!(3, 9, 4) の展開
{
    let a = 3;
    let b = {
        let a = 9;
        let b = 4;
        if a > b { a } else { b }
    };
    if a > b { a } else { b }
}
// add_ten!(base) の展開
{
    let base = 10;
    base + base
}
```

`upper!`はリテラルだけになっており、実行時に変換処理が残っていないことが分かります。`max!`は再帰がブロックの入れ子として展開されています。注意したいのは`add_ten!`で、テキスト上は`base + base`ですが、実際の実行結果は11です。衛生性による変数の区別はテキストにすると消えます。[cargo expandのREADME](https://github.com/dtolnay/cargo-expand)も、テキストへの展開は情報が落ちるデバッグ補助であり、展開結果がコンパイルできることも元と同じ動作をすることも期待しないよう注意しています。展開結果を`.expanded.rs`として保存し、差分を比べるテストにしたいときはmacrotest（最新1.3.0）が使えます。

## 手続き型マクロで失敗しやすい場面

### async fnに付けた属性マクロのE0728エラー

前章の`#[timed]`から`async fn`の分岐を外し、本体を常に同期クロージャで包む形にすると、`.await`を含む`async fn`に付けた時点で次のエラーになります。

```
error[E0728]: `await` is only allowed inside `async` functions and blocks
 --> app/examples/bad.rs:8:27
  |
6 | #[timed]
  | -------- this is not `async`
7 | async fn fetch() -> u32 {
8 |     std::future::ready(1).await
  |                           ^^^^^ only allowed inside `async` functions and blocks
```

本体を同期クロージャに移したため、`.await`が同期の文脈に入ったことが原因です。関数を包む属性マクロでは`f.sig.asyncness`を見て、`async fn`なら`async move`ブロックで包むか、対応しない旨を`syn::Error`で返す分岐が必須です。マクロ作者が想定していない書き方は、利用者には自分のコードのどこが悪いのか分からないエラーとして現れます。非同期関数の構造は[Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](/tech/details/8681/)で解説しています。

### synのfull機能と依存クレートのビルド時間

syn・quote・proc-macro2で書いたマクロを使うと、利用者の初回ビルドやクリーンビルドにこの3クレートのコンパイルが加わります（`proc_macro`だけで書けば依存は増えませんが、トークンを手で読むことになります）。Intel Core i9-9880H（macOS）でsyn 3.0.9だけを依存に持つクレートをクリーンビルドすると、既定機能で8.4〜9.4秒（中央値9.3秒）、`full`機能付きで10.3〜11.9秒（中央値11.5秒）でした（2026年11月8日・デバッグビルド・各3回）。並走ワーカーがあり、load average 4〜5の環境での測定なので、差の約2秒は参考値です。`full`が要るかはマクロの種類ではなく扱う構文で決まり、`DeriveInput`とフィールドの型だけを読む派生マクロなら既定機能で足ります。

利用者が型定義に1行足すだけで数十行のトレイト実装が手に入るなら、このビルド時間を払う価値があります。serde・thiserror・clapの派生マクロが広く使われているのは、この条件を満たしているからです。

## Rustのマクロについてよくある質問

### Rustのマクロにはどんな種類がありますか？

`macro_rules!`で定義する宣言的マクロ（`println!`・`vec!`・`dbg!`など）と、手続き型マクロの2系統です。手続き型マクロはさらに派生マクロ・属性マクロ・関数風マクロの3種類に分かれます。

### #\[derive(Debug)\]や#\[derive(Hash)\]も手続き型マクロですか？

標準ライブラリの`Debug`・`Hash`・`Clone`などの派生は、proc-macroクレートではなくコンパイラ組み込みのマクロです。Rust 1.99.1のcoreのソースでは`#[rustc_builtin_macro]`付きの`pub macro Debug`・`pub macro Hash`として宣言されています。書き方は同じ`#[derive(...)]`ですが、自作の派生マクロのような依存クレートは要りません。デバッグ出力用の`dbg!`はstdの中で`macro_rules!`で定義された宣言的マクロです。

### 手続き型マクロを定義したクレート内で使えないのはなぜですか？

マクロは、それを使うコードのコンパイル中に実行されるためです。定義と利用が同じクレートにあると、マクロをビルドし終える前にそのマクロが必要になります。ライブラリとして配るときは、`foo`と`foo-macros`（または`foo_derive`）の2クレートに分け、本体から再エクスポートする構成が一般的です。

### proc\_macroとproc\_macro2はどう違いますか？

`proc_macro`はコンパイラに同梱されたクレートで、マクロとしてコンパイラから呼ばれている間しか動きません。`proc_macro2`（crates.io上の名前はproc-macro2、最新1.0.107）はそれを包んだクレートで、通常のプログラムや単体テストからも使えます。マクロの入口と出口で`.into()`により相互に変換し、中の処理はproc\_macro2で書くのが定石です。

### syn 2とsyn 3は同じプロジェクトに混在できますか？

混在できます。メジャーバージョンが違うためCargoは別クレートとして両方をビルドし、コンパイルエラーにはなりません。代わりにビルド時間が増えます。`cargo tree -d`で重複を確認し、syn 2に依存しているクレートが新版で3系に移っていないかを確かめてください。

## 関連記事

- [Rust入門｜環境構築から所有権・基本文法、受託開発での採用判断まで](/tech/details/15373/)
- [メタプログラミングとは？仕組みとRuby・Python・JavaScript・C++・Rustの実装例](/tech/details/3510/)
- [Serdeの読み方と使い方｜Rustのderive・rename\_all・defaultを実行結果で解説](/tech/details/5551/)
- [Cargo（Rust）とは？インストールとcargo installの使い方・主要コマンド一覧](/tech/details/9572/)
- [Rustのasync/await入門｜Futureとpollの仕組み、Tokioでの実行とよくあるエラー](/tech/details/8681/)

---

出典: [Rustのマクロの書き方｜macro\_rules!と手続き型マクロ（proc macro）をsyn 3で実装](<https://www.issoh.co.jp/tech/details/9463/>)（株式会社一創）
