コンテンツにスキップ

Rustの手続き的マクロ:自作の#[derive]を書いてみよう

#[derive(Debug)] や #[derive(Serialize)] と書くたびに、実はコンパイラの中で小さなプログラムが動いています。そのプログラムが構造体を読み取って、impl ブロックを代わりに書いてくれるのです。この小さなプログラムが手続き的マクロ(procedural macro)で、自分で書くこともできます。

親切な事務員さんをイメージしてみてください。書類(構造体)を渡すと、事務員さんが項目(フィールド)をひとつずつ読んで、面倒な書類仕事を全部埋めて返してくれます。マクロのページでは、パターンのマッチングで動く macro_rules! を学びました。手続き的マクロは仕組みが違います。コードを受け取ってコードを返す、ふつうのRustの関数なのです。このページでは、本物の手続き的マクロを一歩ずつ作り、実際の実行結果も見ていきます。

手続き的マクロには3種類あります。中の仕組みは同じですが、使う側からの見た目が違います。

種類 使う人が書くもの 有名な例
deriveマクロ 構造体や列挙型に付ける #[derive(Serialize)] serdeの Serialize、thiserrorの Error、clapの Parser
属性風マクロ 関数などに付ける #[route(GET, "/")] #[tokio::main]、actix-webの #[get("/")]
関数風マクロ sql!(SELECT * FROM users) sqlxの query!、yewの html!
  • deriveマクロは、型の隣にコードを追加するだけです。構造体そのものは、書いたとおりのまま残ります。
  • 属性風マクロは、付けた要素を置き換えます。#[tokio::main] は async fn main を受け取り、tokioのランタイムを起動するふつうの fn main に書き換えます。
  • 関数風マクロは macro_rules! の呼び出しのように見えますが、入力の意味を決めるのはRustの関数です。sqlx::query! がコンパイル時にSQLをチェックできるのは、このおかげです。

ほかのクレートのderiveマクロなら、もう使ったことがありますよね。ここでは2つのderiveマクロが実際に働いています。Serialize はJSONを作るコードを書き(RustでJSONを扱うを参照)、Error は #[error(...)] の文章から Display のコードを書いてくれます(anyhowとthiserrorを参照)。

use serde::Serialize;
use thiserror::Error;
#[derive(Serialize)]
struct Order {
id: u32,
item: String,
quantity: u8,
}
#[derive(Debug, Error)]
enum ShopError {
#[error("{0}は在庫切れです")]
OutOfStock(String),
}
fn main() {
let order = Order { id: 42, item: "ティーポット".to_string(), quantity: 2 };
println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("ティーポット".to_string());
println!("{err}");
}

実行結果:

{"id":42,"item":"ティーポット","quantity":2}
ティーポットは在庫切れです
▶ 実際に試してみよう

Display の実装もJSONのコードも、一行も書いていません。プログラムのコンパイル中に、deriveが書いてくれたのです。

仕組み:トークンを受け取り、トークンを返す

Section titled “仕組み:トークンを受け取り、トークンを返す”

コンパイラは最初に、ソースコードをトークンに切り分けます。名前、記号、リテラル、かっこなどです。手続き的マクロは、そのトークンを TokenStream として受け取り、新しい TokenStream を返す関数です。コンパイラは返ってきたものを、まるであなたが自分で書いたかのようにコンパイルします。

関数の形は3通りです。どの種類なのかがコンパイラにわかるように、それぞれに属性を付けます。

use proc_macro::TokenStream;
// #[derive(HelloMacro)]:構造体や列挙型を受け取り、「追加する」コードを返す。
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]:属性自身のトークンと要素を受け取り、「置き換え後」のコードを返す。
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...):かっこの中身をすべて受け取り、置き換え後のコードを返す。
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }

生の TokenStream はただのトークンの並びなので、そのままでは扱いにくいです。そこで、ほとんどの手続き的マクロは3つの補助クレートを使います。

  • syn は、トークンを構文木に変換します。トークンの代わりに、型の名前、ジェネリクス、フィールドが入った DeriveInput を受け取れます。
  • quote は、Rustらしい見た目のコードをトークンに戻します。quote! { ... } の中にはふつうのRustを書き、#name で変数の中身を差し込みます。
  • proc-macro2 は、コンパイラの proc_macro の型を包んだもので、synとquoteが内部で使っています。コンパイラの外でも動くので、補助関数やテストが書きやすくなります。

大事なポイントがひとつあります。マクロはコンパイル時に動きます。 Cargoはまずマクロのクレートを(自分のパソコン用に)コンパイルし、コンパイラはそのマクロを使うクレートをビルドする途中でマクロを実行します。プログラムが実際に動くころには、マクロはもうどこにもありません。残っているのは、マクロが書いたコードだけです。

手続き的マクロは、特別なクレートタイプを持つ専用のクレートに置く必要があります。そのクレートからエクスポートできるのはマクロだけで、ふつうの関数やトレイトはエクスポートできません。そのため、serdeとserde_deriveのように、トレイト用とderive用の2つのクレートを並べるのが定番の構成です。ここではさらに、それらを使う小さなプログラム shop を3つ目のクレートとして加えます(クレートやワークスペースを復習したいときはRustのCargoとクレートを見てください)。

Terminal window
mkdir procmacro-demo && cd procmacro-demo
cargo new --lib hello_macro
cargo new --lib hello_macro_derive
cargo new shop

完成したときのファイル構成はこうなります。

procmacro-demo/
├── Cargo.toml (ワークスペース)
├── hello_macro/
│ ├── Cargo.toml
│ └── src/lib.rs (トレイトと、deriveの再エクスポート)
├── hello_macro_derive/
│ ├── Cargo.toml (proc-macro = true)
│ └── src/lib.rs (マクロ本体)
└── shop/
├── Cargo.toml
└── src/main.rs (#[derive(...)] を使うプログラム)

いちばん上の Cargo.toml で、3つのクレートをひとつのワークスペースにまとめます。

[workspace]
members = ["hello_macro", "hello_macro_derive", "shop"]
resolver = "2"

特別な設定が必要なのはマクロのクレートだけで、それが proc-macro = true です。さらに、syn、quote、proc-macro2に依存します。

hello_macro_derive/Cargo.toml
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = "2"

残りの2つはふつうのクレートです。hello_macro はderiveのクレートに依存し、shop は hello_macro だけが必要です。

hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }
shop/Cargo.toml
[dependencies]
hello_macro = { version = "0.1.0", path = "../hello_macro" }

まずはごく小さなものから始めましょう。#[derive(HelloMacro)] を付けた型に、型の名前を表示する hello_macro() 関数が付くようにします。最初はトレイトです。hello_macro/src/lib.rs に書きます。最後の行でderiveマクロを再エクスポートしているので、使う人はこのクレートひとつだけを依存に加えれば済みます。serdeもまったく同じやり方をしています。

hello_macro/src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
pub trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
}
// deriveマクロを再エクスポートする。使う人はこのクレートだけを使えばよい。
pub use hello_macro_derive::{Describe, HelloMacro};

(Describe はステップ2で使います。)次はマクロ本体です。hello_macro_derive/src/lib.rs に書きます。

hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields}; // DataとFieldsはステップ2で使う
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 1. トークンを、中身を調べられる構文木に変換する。
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
// 2. quote! で新しいコードを組み立てる。
let generated = quote! {
impl hello_macro::HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
// 3. 新しいトークンをコンパイラに返す。
generated.into()
}

3つのステップがあります。これはほとんどすべてのderiveマクロに出てくる流れです。

  1. parse_macro_input! がトークンを DeriveInput に変換します。ast.ident は型の名前で、たとえば Product です。解析に失敗したときは、このマクロがきちんとしたコンパイルエラーを返してくれます。
  2. quote! が impl ブロックを組み立てます。#name で名前を差し込みます。stringify!(#name) は、名前を "Product" という文字列に変えます。
  3. .into() が、quoteの proc_macro2::TokenStream をコンパイラの proc_macro::TokenStream に変換します。

hello_macro::HelloMacro とフルパスで書いている点に注目してください。生成されたコードは使う人のファイルに貼り付けられるので、その人がどんな use を書いているかには頼れないのです。

誰かが #[derive(HelloMacro)] struct Product と書くと、コンパイラは、次の手書きバージョンとまったく同じように動くコードを追加します。こちらはマクロのクレートを使っていないので、Playgroundで実行できます(サンプルプロジェクトと出力をそろえるため、文字列は英語のままにしています)。

trait HelloMacro {
fn hello_macro();
}
struct Product;
// #[derive(HelloMacro)] が代わりに書いてくれるのは、これです。
impl HelloMacro for Product {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", "Product");
}
}
fn main() {
Product::hello_macro();
}

実行結果:

Hello, Macro! My name is Product!
▶ 実際に試してみよう

一度だけなら手で書いても問題ありません。でも、40個の型に同じものを書くとなったら、deriveの出番です。

ステップ2:フィールドを読む #[derive(Describe)]

Section titled “ステップ2:フィールドを読む #[derive(Describe)]”

HelloMacro が使ったのは型の名前だけでした。serdeのような本格的なderiveは、フィールドも読み取ります。そこで Describe を作ってみましょう。構造体のフィールド名を一覧にしてくれるもので、CSVのヘッダー行を作るときなどに便利です。synでは ast.data からフィールドを取り出せます。

  • Data::Struct、Data::Enum、Data::Union:どの種類の型なのかを表します。
  • 構造体なら、Fields::Named(struct Product { id: u32 })、Fields::Unnamed(struct Meters(f64))、Fields::Unit(struct Marker;)のどれかです。

hello_macro_derive/src/lib.rs の1つ目のマクロの下に、次のコードを追加します。

#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
expand_describe(&ast)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &ast.ident;
let Data::Struct(data) = &ast.data else {
return Err(syn::Error::new_spanned(
name,
"Describe can only be derived for structs",
));
};
let field_names: Vec<String> = match &data.fields {
Fields::Named(fields) => fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap().to_string())
.collect(),
Fields::Unit => Vec::new(),
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
name,
"Describe needs named fields, like `struct Point { x: i32 }`",
));
}
};
Ok(quote! {
impl hello_macro::Describe for #name {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
}
})
}

新しく出てきたポイントです。

  • 実際の処理は、syn::Result を返すふつうの補助関数の中で行います。こうすると、ほかのエラー処理のコードと同じように ? や return Err(...) が使えます。いちばん上のマクロ関数は、エラーをコンパイルエラーに変えるだけです。
  • let ... else で「構造体ではない」場合をすぐに処理するので、うまくいく場合の流れが平らなまま読みやすく保てます。
  • #(#field_names),* はquoteの繰り返しです。Vec の中の文字列をすべて、カンマ区切りで差し込みます。macro_rules! の $(...),* と同じ考え方です。それぞれの String は文字列リテラルになるので、ここは &["id", "name", "price_cents"] に変わります。
  • ここでの field.ident.as_ref().unwrap() は安全です。Fields::Named では、どのフィールドにも必ず名前があるからです。

では、shop/src/main.rs で2つのderiveを使ってみましょう。use hello_macro::{Describe, HelloMacro} で、トレイトとderiveマクロの両方を取り込んでいる点に注目してください。名前は同じでも、別々の名前空間にあるので衝突しません。serde::Serialize もまさにこの仕組みです。

shop/src/main.rs
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]
struct Product {
id: u32,
name: String,
price_cents: u64,
}
#[derive(HelloMacro, Describe)]
struct Customer {
email: String,
city: String,
}
fn main() {
Product::hello_macro();
Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// CSVファイル:ヘッダーはderiveから、各行は実際の値から作る。
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
println!("{}", Customer::field_names().join(","));
println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 };
println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);
}

ワークスペースのフォルダで実行します。

Terminal window
cargo run -p shop

実行結果(何もビルドしていない状態から実行した場合):

Compiling proc-macro2 v1.0.107
Compiling unicode-ident v1.0.27
Compiling quote v1.0.47
Compiling syn v2.0.119
Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive)
Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro)
Compiling shop v0.1.0 (/home/you/procmacro-demo/shop)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s
Running `target/debug/shop`
Hello, Macro! My name is Product!
Hello, Macro! My name is Customer!
Product has fields ["id", "name", "price_cents"]
email,city
Product #7: Teapot costs 1999 cents

Compiling、Finished、Running の行はCargoが表示するもので、いつも英語です。順番に注目してください。Cargoは shop より先に syn と hello_macro_derive をビルドしています。shop をコンパイルする途中で、コンパイラがあなたのマクロを実行する必要があるからです。Customer にフィールドを追加すれば、次のビルドでCSVのヘッダーも自動で変わります。

わかりやすいエラーメッセージ

Section titled “わかりやすいエラーメッセージ”

誰かが列挙型に #[derive(Describe)] を付けたら、どうなるでしょうか。expand_describe は syn::Error を返すので、使う人にはふつうのコンパイルエラーと同じ見た目のエラーが表示されます(このページの「壊れた例」は shop/src/bin/broken.rs に置いて試したので、そのパスが表示されています)。

use hello_macro::Describe;
#[derive(Describe)]
enum OrderStatus {
Pending,
Shipped,
}
error: Describe can only be derived for structs
--> shop/src/bin/broken.rs:4:6
|
4 | enum OrderStatus {
| ^^^^^^^^^^^

このメッセージは、マクロの中で私たちが書いた文章(「Describeは構造体にしかderiveできません」)です。このエラーがわかりやすいのは、次の2つのおかげです。

  • syn::Error::new_spanned(name, ...) は、使う人のファイルのどこに名前があるか(そのスパン、つまりコード上の位置)を覚えています。だから ^^^^^ が、マクロの中のどこかの行ではなく OrderStatus を指しているのです。
  • to_compile_error() は、エラーを compile_error!("...") の呼び出しに変えます。するとコンパイラが、ほかのエラーと同じように報告してくれます。この compile_error! マクロは、ふつうのコードでも使えます。

タプル構造体の場合は、もうひとつのメッセージ(「Describeには名前付きフィールドが必要です」)が出ます。こちらも正しい場所を指しています。

error: Describe needs named fields, like `struct Point { x: i32 }`
--> shop/src/bin/broken.rs:4:8
|
4 | struct Meters(f64);
| ^^^^^^

メッセージは、マクロを使う人に向けて書きましょう。何がいけないのか、代わりにどうすればいいのかを伝えるのがコツです。

cargo expandで生成されたコードを見る

Section titled “cargo expandで生成されたコードを見る”

マクロは魔法のように感じられることがありますが、魔法はデバッグしにくいものです。cargo-expand を使うと、すべてのマクロが展開されたあとのコードを見られます。一度インストールしておきましょう(ほかの便利なツールはRustのツールと環境構築で紹介しています)。

Terminal window
cargo install cargo-expand
cargo expand -p shop

このプロジェクトで表示される内容の一部です。

impl hello_macro::Describe for Product {
fn type_name() -> &'static str {
"Product"
}
fn field_names() -> &'static [&'static str] {
&["id", "name", "price_cents"]
}
}
impl hello_macro::Describe for Customer {
fn type_name() -> &'static str {
"Customer"
}
fn field_names() -> &'static [&'static str] {
&["email", "city"]
}
}

フィールド名が埋め込まれた impl ブロックがちゃんとあり、stringify!(#name) もすでに "Product" に変わっていますね。deriveが期待どおりに動かないときは、まず cargo expand を実行してみましょう。println! などほかのマクロも展開されるので、自分の impl ブロックを探してください。

マクロのクレートはマクロしかエクスポートできないので、自分自身をテストするのは簡単ではありません。いちばん簡単な方法は、マクロを使う側のクレートにふつうのテストを書くことです。次のコードを hello_macro/tests/describe.rs に置いて、cargo test -p hello_macro を実行します。

use hello_macro::Describe;
#[allow(dead_code)]
#[derive(Describe)]
struct Product {
id: u32,
name: String,
}
#[test]
fn lists_field_names_in_order() {
assert_eq!(Product::type_name(), "Product");
assert_eq!(Product::field_names(), ["id", "name"]);
}
running 1 test
test lists_field_names_in_order ... ok

では、列挙型に付けたときにわかりやすいエラーが出ることは、どうやってテストすればいいのでしょうか。ふつうのテストではできません。テストのファイル自体がコンパイルできなくなるからです。そのためにあるのが trybuild クレートです。「失敗するはず」の小さなファイルをフォルダに入れておくと、trybuildがひとつずつコンパイルして、コンパイラのエラーを保存済みの .stderr ファイルと比べてくれます。

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

serdeやthiserrorをはじめ、人気のあるマクロのクレートの多くは、この方法でエラーメッセージをテストしています。

手続き的マクロを書くべきなのはいつ?

Section titled “手続き的マクロを書くべきなのはいつ?”

手続き的マクロは強力ですが、コストもかかります。書き始める前に、本当に必要か正直に考えてみましょう。

  • コンパイル時間: synは大きなクレートです。あなたのマクロを使うプロジェクトは、最初のビルドでsynもコンパイルしなければなりません(deriveならsynのデフォルトのフィーチャーで十分です。関数の本体などまで解析したいときだけ features = ["full"] が必要で、そうするとさらに遅くなります)。
  • 複雑さ: クレートがひとつ増え、コードを書くコードを管理することになり、エラーも追いかけにくくなります。
  • ツールの対応: エディタやrust-analyzerは、マクロが生成したコードよりも、ふつうのコードのほうをよく理解できます。

試していく順番の目安はこうです。

  1. 関数、デフォルトメソッドを持つトレイト、またはジェネリクス。「同じようなコードの繰り返し」問題の多くは、ここで解決します。
  2. macro_rules! マクロ(マクロを参照)。追加のクレートはいりません。
  3. 手続き的マクロ。コードが型の構造(名前、フィールド、バリアント)によって変わり、多くの型で必要になり、ほかの人も使うときです。serde、thiserror、clapがderiveを使っているのはこのためです。

比べてみるために、Describe を macro_rules! で作ってみました。ちゃんと動きますし、追加のクレートもいりません。ただし、構造体をマクロ呼び出しの中に書かなければなりません。ドキュメントコメントや pub フィールド、ほかのderiveも付けたくなると、すぐに書きにくくなります。

trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
}
macro_rules! describe_struct {
(struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
struct $name {
$($field: $ty),*
}
impl Describe for $name {
fn type_name() -> &'static str {
stringify!($name)
}
fn field_names() -> &'static [&'static str] {
&[$(stringify!($field)),*]
}
}
};
}
describe_struct!(struct Customer { email: String, city: String });
fn main() {
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
println!("{} has fields {:?}", Customer::type_name(), Customer::field_names());
println!("{},{}", ana.email, ana.city);
}

実行結果:

Customer has fields ["email", "city"]
▶ 実際に試してみよう

自分のプログラムの中の1つか2つの型だけなら、こちら(あるいは impl を手で書くこと)のほうが良い選択になることもよくあります。deriveの良いところは、使う人にとって楽なことです。いつもどおりの構造体のまま、1行足すだけで済みます。

proc-macro = true を忘れる。 マクロのクレートの Cargo.toml に [lib] セクションがないと、そのクレートはふつうのライブラリになり、コンパイラにエラーで止められます。

error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type
--> hello_macro_derive/src/lib.rs:5:1
|
5 | #[proc_macro_derive(HelloMacro)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`

コンパイラは「#[proc_macro_derive] は proc-macro タイプのクレートでしか使えない」と言っています。同じ理由で、proc_macro クレートも見つからないのです。

使う人のミスに panic! や unwrap() を使う。 一応「動き」はしますが、エラーがあいまいで、問題の場所ではなく #[derive] 全体を指してしまいます。

error: proc-macro derive panicked
--> shop/src/bin/broken.rs:3:10
|
3 | #[derive(Describe)]
| ^^^^^^^^
|
= help: message: Describe can only be derived for structs

コンパイラが伝えているのは「deriveマクロがパニックした」ということだけで、メッセージは補足(help)として表示されるだけです。ステップ2のように、new_spanned で作った syn::Error を返しましょう。

生成するコードで短いパスを使う。 impl hello_macro::Describe for #name ではなく impl Describe for #name と書くと、使う人がたまたま Describe をインポートしているときしか動きません。#[derive(hello_macro::Describe)] と書いた人には、次のエラーが出ます。

error[E0405]: cannot find trait `Describe` in this scope
--> shop/src/bin/broken.rs:1:10
|
1 | #[derive(hello_macro::Describe)]
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope
|
= note: this error originates in the derive macro `hello_macro::Describe`

「このスコープにトレイト Describe が見つからない」という意味です。quote! の中では、hello_macro::Describe、::std::fmt::Display、::core::option::Option のように、いつもフルパスを使いましょう。

この練習問題は本物のプロジェクトを書き換えるものなので、「Try it」ボタンはありません。このページのプロジェクトを自分のパソコンで作って、cargo run -p shop で実行してください。

Describe トレイトとderiveに、3つ目のメソッド fn field_count() -> usize を追加してください。Product::field_count() が3を返すようにします。ヒント: field_names のベクタはもうあります。quote! の前で field_names.len() を変数に入れておき、# で差し込みましょう。

解答を見る

hello_macro/src/lib.rs で、トレイトにメソッドを追加します。

pub trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
fn field_count() -> usize;
}

expand_describe でフィールドを数え、生成する impl にメソッドを追加します。

let count = field_names.len();
Ok(quote! {
impl hello_macro::Describe for #name {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
fn field_count() -> usize {
#count
}
}
})

あとは main に println!("{} has {} fields", Product::type_name(), Product::field_count()); を書くと、次のように表示されます。

Product has 3 fields

数える処理は、マクロの中でコンパイル時に行われます。cargo expand で見ると、結果はただの fn field_count() -> usize { 3usize } です。実行時に残っている処理は何もない、ただの定数です。

2. ジェネリックな構造体に対応する

Section titled “2. ジェネリックな構造体に対応する”

struct Page<T> { items: Vec<T>, number: u32 } のようなジェネリックな構造体に #[derive(Describe)] を付けてみてください。error[E0107]: missing generics for struct Page(ジェネリクスが足りない)というエラーになります。生成されたコードが impl hello_macro::Describe for Page となっていて、<T> が抜けているからです。ジェネリックな構造体でも動くように、deriveを直してください。ヒント: ast.generics.split_for_impl() を使うと、impl の行に差し込む3つの部品が手に入ります。impl<T> の書き方はジェネリクスで復習できます。

解答を見る

expand_describe でジェネリクスを分割し、3つの部品をすべて impl の行で使います。

let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! {
impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
}
})

これで shop では次のコードが動きます。

#[derive(Describe)]
struct Page<T> {
items: Vec<T>,
number: u32,
}
fn main() {
let page = Page { items: vec!["a", "b"], number: 1 };
println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());
}
2 1 ["items", "number"]

impl_generics は(境界があればそれも含めた)<T>、ty_generics は <T>、where_clause は where ... の部分です(あれば)。本格的なderiveはどれもこれをやっているので、split_for_impl はぜひ覚えておきましょう。

  • 手続き的マクロは、コンパイル時に動くRustの関数です。TokenStream を受け取り、TokenStream を返します。
  • 種類は3つあります。derive(#[derive(Serialize)])、属性風(#[tokio::main])、関数風(sql!(...))です。
  • 手続き的マクロは proc-macro = true を設定した専用のクレートに置きます。ふつうは、メインのクレートがトレイトと一緒に再エクスポートします。
  • syn が入力を解析し(DeriveInput、Data、Fields)、quote が出力を書きます(#name、#(...),*)。
  • 使う人のミスは syn::Error::new_spanned(...).to_compile_error() で報告し、結果は cargo expand で確かめ、エラーはtrybuildでテストします。
  • まずは関数、トレイト、macro_rules! を試しましょう。手続き的マクロが役に立つのは、多くの型が、その構造に応じたコードを必要とするときです。

理解度チェック

1. 手続き的マクロのコードが実行されるのはいつですか?

2. #[tokio::main] はどの種類の手続き的マクロですか?

3. synとquoteはそれぞれ何をしますか?

4. 自作のderiveで列挙型を受け付けないようにしたいとき、いちばん良い方法はどれですか?

スコア: 0 / 4