Rustの手続き的マクロ:自作の#[derive]を書いてみよう
#[derive(Debug)] や #[derive(Serialize)] と書くたびに、実はコンパイラの中で小さなプログラムが動いています。そのプログラムが構造体を読み取って、impl ブロックを代わりに書いてくれるのです。この小さなプログラムが手続き的マクロ(procedural macro)で、自分で書くこともできます。
親切な事務員さんをイメージしてみてください。書類(構造体)を渡すと、事務員さんが項目(フィールド)をひとつずつ読んで、面倒な書類仕事を全部埋めて返してくれます。マクロのページでは、パターンのマッチングで動く macro_rules! を学びました。手続き的マクロは仕組みが違います。コードを受け取ってコードを返す、ふつうのRustの関数なのです。このページでは、本物の手続き的マクロを一歩ずつ作り、実際の実行結果も見ていきます。
手続き的マクロの3つの種類
Section titled “手続き的マクロの3つの種類”手続き的マクロには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はまずマクロのクレートを(自分のパソコン用に)コンパイルし、コンパイラはそのマクロを使うクレートをビルドする途中でマクロを実行します。プログラムが実際に動くころには、マクロはもうどこにもありません。残っているのは、マクロが書いたコードだけです。
プロジェクトの準備
Section titled “プロジェクトの準備”手続き的マクロは、特別なクレートタイプを持つ専用のクレートに置く必要があります。そのクレートからエクスポートできるのはマクロだけで、ふつうの関数やトレイトはエクスポートできません。そのため、serdeとserde_deriveのように、トレイト用とderive用の2つのクレートを並べるのが定番の構成です。ここではさらに、それらを使う小さなプログラム shop を3つ目のクレートとして加えます(クレートやワークスペースを復習したいときはRustのCargoとクレートを見てください)。
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo 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に依存します。
[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 だけが必要です。
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }ステップ1:#[derive(HelloMacro)]
Section titled “ステップ1:#[derive(HelloMacro)]”まずはごく小さなものから始めましょう。#[derive(HelloMacro)] を付けた型に、型の名前を表示する hello_macro() 関数が付くようにします。最初はトレイトです。hello_macro/src/lib.rs に書きます。最後の行でderiveマクロを再エクスポートしているので、使う人はこのクレートひとつだけを依存に加えれば済みます。serdeもまったく同じやり方をしています。
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 に書きます。
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マクロに出てくる流れです。
parse_macro_input!がトークンをDeriveInputに変換します。ast.identは型の名前で、たとえばProductです。解析に失敗したときは、このマクロがきちんとしたコンパイルエラーを返してくれます。quote!がimplブロックを組み立てます。#nameで名前を差し込みます。stringify!(#name)は、名前を"Product"という文字列に変えます。.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 もまさにこの仕組みです。
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から、各行は実際の値から作る。 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);}ワークスペースのフォルダで実行します。
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[email protected],LisbonProduct #7: Teapot costs 1999 centsCompiling、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のツールと環境構築で紹介しています)。
cargo install cargo-expandcargo 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 ブロックを探してください。
マクロをテストする
Section titled “マクロをテストする”マクロのクレートはマクロしかエクスポートできないので、自分自身をテストするのは簡単ではありません。いちばん簡単な方法は、マクロを使う側のクレートにふつうのテストを書くことです。次のコードを 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 testtest 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は、マクロが生成したコードよりも、ふつうのコードのほうをよく理解できます。
試していく順番の目安はこうです。
- 関数、デフォルトメソッドを持つトレイト、またはジェネリクス。「同じようなコードの繰り返し」問題の多くは、ここで解決します。
macro_rules!マクロ(マクロを参照)。追加のクレートはいりません。- 手続き的マクロ。コードが型の構造(名前、フィールド、バリアント)によって変わり、多くの型で必要になり、ほかの人も使うときです。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() { println!("{} has fields {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}実行結果:
Customer has fields ["email", "city"][email protected],Lisbon自分のプログラムの中の1つか2つの型だけなら、こちら(あるいは impl を手で書くこと)のほうが良い選択になることもよくあります。deriveの良いところは、使う人にとって楽なことです。いつもどおりの構造体のまま、1行足すだけで済みます。
よくある間違い
Section titled “よくある間違い”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 で実行してください。
1. フィールドの数を数える
Section titled “1. フィールドの数を数える”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!を試しましょう。手続き的マクロが役に立つのは、多くの型が、その構造に応じたコードを必要とするときです。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. 手続き的マクロのコードが実行されるのはいつですか?
2. #[tokio::main] はどの種類の手続き的マクロですか?
3. synとquoteはそれぞれ何をしますか?
4. 自作のderiveで列挙型を受け付けないようにしたいとき、いちばん良い方法はどれですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- 属性からコードを生成する手続き的マクロ — The Rust Programming Language 日本語版:
HelloMacroのderiveを作る公式の解説と、属性風マクロ・関数風マクロの紹介。 - Procedural Macros — The Rust Reference(英語):3種類のマクロ、クレートタイプ、スパン、衛生性(hygiene)についての正確なルール。
- synのドキュメント(英語):このページで使った
DeriveInput、Data、Fields、Errorと、解析のための補助機能。 - quoteのドキュメント(英語):
quote!の中で#varの差し込みや#(...),*の繰り返しがどう動くか。 - proc-macro-workshop(英語):David Tolnayによる無料のハンズオン課題集(builderのderive、debugのderiveなど)。テストが一歩ずつ導いてくれます。