Rustマクロメタプログラミング実践:宣言マクロから手続きマクロまでの5つのプロダクションパターン
Rustマクロ、なぜ書いた直後に忘れるのか
macro_rules!を書いたのに、3日後には自分でもマッチングルールが理解できない;deriveマクロでSerializeを自動実装したかったが、proc-macroクレートの設定に落とし穴が多い;属性マクロでログデコレータを作ったら、TokenStream不一致のコンパイルエラーが大量発生;手続きマクロでコード生成しようとしたら、synパースとquoteスプライスのデバッグがcargo expand頼み。2026年、Rustのマクロシステムは宣言マクロ、3種類の手続きマクロ(derive/attribute/function-like)、成熟したsyn+quoteエコシステムを提供している——しかしマクロプログラミングはコンパイルが通ればよいというものではない。
本記事では宣言マクロから出発し、macro_rules! DSL→Deriveマクロ自動実装→属性マクロデコレータ→関数ライク手続きマクロ→syn/quoteプロダクションコード生成の5つの実践パターンを解説し、Rustマクロを「コンパイルできる」から「保守できる」レベルへ引き上げる。
コア概念
| 概念 | 説明 |
|---|---|
| macro_rules! | 宣言マクロ、パターンマッチングベースのコードテンプレート、コンパイル時展開 |
| Declarative Macro | 宣言マクロの別名、macro_rules!で定義 |
| Procedural Macro | 手続きマクロ、TokenStream入力を受け取りTokenStream出力を返す |
| Derive Macro | 派生マクロ、構造体/列挙型にtraitを自動実装 |
| Attribute Macro | 属性マクロ、itemにアノテーションし、アノテーションされたitemを変更または置換 |
| Function-like Proc Macro | 関数ライク手続きマクロ、関数のようにmac!()で呼び出し |
| TokenStream | トークンストリーム、手続きマクロの入出力型 |
| syn | Rustソースコード解析ライブラリ、TokenStreamをASTに変換 |
| quote | コード生成ライブラリ、RustコードテンプレートをTokenStreamに変換 |
| cargo expand | ツール、マクロを展開して生成コードを確認 |
| Span | ソースコード位置情報、エラーレポートの位置特定に使用 |
マクロ展開フロー
マクロ展開フロー:
1. コンパイラがマクロ呼び出し mac!(...) に遭遇
2. 宣言マクロ:パターンマッチarmに従いテンプレートコードに置換
手続きマクロ:入力をTokenStreamに変換
3. 手続きマクロがTokenStreamをASTにパース(syn::parse)
4. ASTを変換/新しいコードを生成
5. 生成コードをTokenStreamに変換(quote::quote!)
6. コンパイラが展開結果を通常のRustコードとしてコンパイル
7. 展開後のコードが型チェックと借用チェックに参加
問題分析:Rustマクロ開発の5つの課題
- 宣言マクロのパターンマッチングがデバッグ困難:
macro_rules!のマッチングルールが難解、$($x:tt),*の繰り返しパターンは書いたら忘れる、エラーメッセージが不親切 - 手続きマクロクレートの設定が煩雑:derive/attribute/function-likeマクロは独立した
proc-macro = trueクレートに配置する必要があり、メインクレートと分離 - TokenStreamのデバッグがcargo expand頼り:手続きマクロ内の
println!は出力されない、cargo expandやeprintln!のみ利用可能 - synによる複雑な構文のパースがエラーになりやすい:ジェネリクスパラメータ、ライフタイム、where句のパースは多くのエッジケースの処理が必要
- マクロ生成コードにIDEサポートがない:展開後のコードには補完、ジャンプ、リファクタリングが効かず、保守コストが高い
ステップバイステップ:5つのプロダクションマクロパターン
パターン1:宣言マクロmacro_rules!とDSL作成
macro_rules! define_enum_with_display {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[$variant_meta:meta])*
$variant:ident = $value:expr
),* $(,)?
}
) => {
$(#[$meta])*
$vis enum $name {
$(
$(#[$variant_meta])*
$variant = $value,
)*
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
$(
$name::$variant => write!(f, stringify!($variant)),
)*
}
}
}
impl $name {
pub fn values() -> Vec<Self> {
vec![$($name::$variant),*]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
$(
stringify!($variant) => Some($name::$variant),
)*
_ => None,
}
}
}
};
}
define_enum_with_display! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpStatus {
Ok = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
InternalServerError = 500,
}
}
macro_rules! builder_pattern {
(
$(#[$struct_meta:meta])*
$vis:vis struct $name:ident {
$(
$(#[$field_meta:meta])*
$field:ident : $ty:ty
),* $(,)?
}
) => {
$(#[$struct_meta])*
$vis struct $name {
$(
$(#[$field_meta])*
$field: Option<$ty>,
)*
}
impl $name {
pub fn builder() -> ${concat($name, Builder)} {
${concat($name, Builder)} {
$(
$field: None,
)*
}
}
}
pub struct ${concat($name, Builder)} {
$(
$field: Option<$ty>,
)*
}
impl ${concat($name, Builder)} {
$(
pub fn $field(mut self, value: $ty) -> Self {
self.$field = Some(value);
self
}
)*
pub fn build(self) -> Result<$name, String> {
let missing: Vec<&str> = vec![
$(
if self.$field.is_none() { stringify!($field) } else { "" },
)*
].into_iter().filter(|s| !s.is_empty()).collect();
if !missing.is_empty() {
return Err(format!("Missing required fields: {}", missing.join(", ")));
}
Ok($name {
$(
$field: self.$field,
)*
})
}
}
};
}
builder_pattern! {
pub struct User {
name: String,
email: String,
age: u32,
}
}
パターン2:DeriveマクロによるTrait自動実装
# Cargo.toml (proc-macro crate)
[package]
name = "my_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, GenericParam, Ident};
#[proc_macro_derive(CustomDebug, attributes(debug_name))]
pub fn derive_custom_debug(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let debug_impl = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
let field_debug: Vec<_> = fields.named.iter().map(|f| {
let field_name = &f.ident;
let field_name_str = field_name.as_ref().unwrap().to_string();
quote! {
.field(#field_name_str, &self.#field_name)
}
}).collect();
let struct_name_str = name.to_string();
quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(#struct_name_str)
#(#field_debug)*
.finish()
}
}
}
}
Fields::Unnamed(fields) => {
let field_debug: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, _)| {
let idx = syn::Index::from(i);
quote! {
.field(&self.#idx)
}
}).collect();
let struct_name_str = name.to_string();
quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(#struct_name_str)
#(#field_debug)*
.finish()
}
}
}
}
Fields::Unit => {
let struct_name_str = name.to_string();
quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, #struct_name_str)
}
}
}
}
}
}
Data::Enum(data) => {
let variant_debug: Vec<_> = data.variants.iter().map(|v| {
let variant_name = &v.ident;
let variant_name_str = variant_name.to_string();
match &v.fields {
Fields::Named(fields) => {
let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
let field_strs: Vec<_> = field_names.iter().map(|f| f.as_ref().unwrap().to_string()).collect();
quote! {
#name::#variant_name { #(#field_names),* } => {
f.debug_struct(#variant_name_str)
#( .field(#field_strs, #field_names) )*
.finish()
}
}
}
Fields::Unnamed(fields) => {
let field_names: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, _)| {
Ident::new(&format!("f{}", i), v.ident.span())
}).collect();
quote! {
#name::#variant_name(#(#field_names),*) => {
f.debug_tuple(#variant_name_str)
#( .field(#field_names) )*
.finish()
}
}
}
Fields::Unit => {
quote! {
#name::#variant_name => write!(f, #variant_name_str)
}
}
}
}).collect();
quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#(#variant_debug),*
}
}
}
}
}
Data::Union(_) => {
syn::Error::new_spanned(name, "CustomDebug does not support unions")
.to_compile_error()
.into()
}
};
debug_impl.into()
}
use my_derive::CustomDebug;
#[derive(CustomDebug)]
pub struct ApiResponse {
pub status: u16,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(CustomDebug)]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
パターン3:属性マクロによるログ/バリデーション/キャッシュデコレータ
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, FnArg, Pat, Signature, Visibility};
#[proc_macro_attribute]
pub fn traced(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let ItemFn { attrs, vis, sig, block, .. } = input;
let fn_name = &sig.ident.to_string();
let fn_name_lit = syn::LitStr::new(fn_name, sig.ident.span());
let expanded = quote! {
#(#attrs)*
#vis #sig {
let __start = std::time::Instant::now();
tracing::info!(function = #fn_name_lit, "→ entering");
let __result = #block;
let __elapsed = __start.elapsed();
tracing::info!(
function = #fn_name_lit,
elapsed_ms = __elapsed.as_millis() as u64,
"← exiting"
);
__result
}
};
expanded.into()
}
#[proc_macro_attribute]
pub fn validate_args(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attr_args: Vec<String> = attr.to_string()
.split(',')
.map(|s| s.trim().to_string())
.collect();
let ItemFn { attrs, vis, sig, block, .. } = input;
let fn_name = &sig.ident;
let validations: Vec<_> = sig.inputs.iter().filter_map(|arg| {
if let FnArg::Typed(pat_type) = arg {
if let Pat::Ident(pat_ident) = &*pat_type.pat {
let param_name = pat_ident.ident.to_string();
if attr_args.is_empty() || attr_args.contains(¶m_name) {
let ident = &pat_ident.ident;
return Some(quote! {
if #ident.is_empty() {
return Err(format!("Parameter '{}' must not be empty", #param_name));
}
});
}
}
}
None
}).collect();
let return_type = match &sig.output {
syn::ReturnType::Type(_, ty) => quote! { #ty },
syn::ReturnType::Default => quote! { () },
};
let inputs = &sig.inputs;
let output = &sig.output;
let expanded = quote! {
#(#attrs)*
#vis fn #fn_name(#inputs) #output {
#(#validations)*
#block
}
};
expanded.into()
}
#[proc_macro_attribute]
pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
let ttl_secs: u64 = if attr.is_empty() {
60
} else {
attr.to_string().parse().unwrap_or(60)
};
let input = parse_macro_input!(item as ItemFn);
let ItemFn { attrs, vis, sig, block, .. } = input;
let fn_name = &sig.ident;
let fn_name_str = fn_name.to_string();
let expanded = quote! {
#(#attrs)*
#vis #sig {
static CACHE: std::sync::OnceLock<std::sync::Mutex<(std::time::Instant, Option<_>)>> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new((std::time::Instant::now(), None)));
let mut guard = cache.lock().unwrap();
let now = std::time::Instant::now();
if let (created, Some(ref val)) = guard.0 {
if now.duration_since(created).as_secs() < #ttl_secs {
return val.clone();
}
}
drop(guard);
let result = #block;
let mut guard = cache.lock().unwrap();
*guard = (std::time::Instant::now(), Some(result.clone()));
result
}
};
expanded.into()
}
use my_macros::{traced, validate_args, cached};
#[traced]
async fn fetch_user(user_id: &str) -> Result<User, AppError> {
let client = reqwest::Client::new();
let resp = client.get(&format!("/api/users/{}", user_id)).send().await?;
Ok(resp.json().await?)
}
#[validate_args(name, email)]
fn create_user(name: String, email: String, age: u32) -> Result<User, String> {
Ok(User { name, email, age })
}
#[cached(300)]
fn get_config() -> AppConfig {
load_config_from_file()
}
パターン4:関数ライク手続きマクロによるSQLクエリビルダ
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitStr, Token, Result as SynResult};
struct SqlQuery {
table: Ident,
columns: Vec<Ident>,
where_clause: Option<LitStr>,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> SynResult<Self> {
let table: Ident = input.parse()?;
let mut columns = vec![table.clone()];
if input.peek(Token![.]) {
input.parse::<Token![.]>()?;
let method: Ident = input.parse()?;
if method != "select" {
return Err(syn::Error::new(method.span(), "Expected .select()"));
}
input.parse::<Token![()]>();
let content;
syn::parenthesized!(content in input);
columns.clear();
loop {
let col: Ident = content.parse()?;
columns.push(col);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
} else {
break;
}
}
}
let where_clause = if input.peek(Token![.]) {
input.parse::<Token![.]>()?;
let method: Ident = input.parse()?;
if method == "where_clause" {
input.parse::<Token![()]>();
let content;
syn::parenthesized!(content in input);
Some(content.parse()?)
} else {
None
}
} else {
None
};
Ok(SqlQuery { table, columns, where_clause })
}
}
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let query = match syn::parse::<SqlQuery>(input) {
Ok(q) => q,
Err(e) => return e.to_compile_error().into(),
};
let table_name = query.table.to_string();
let column_names: Vec<String> = query.columns.iter().map(|c| c.to_string()).collect();
let column_refs: Vec<_> = column_names.iter().map(|c| c.as_str()).collect();
let struct_name = Ident::new(
&format!("{}Row", to_pascal_case(&table_name)),
query.table.span(),
);
let field_names: Vec<Ident> = column_names.iter()
.map(|c| Ident::new(&to_snake_case(c), query.table.span()))
.collect();
let field_types: Vec<_> = column_names.iter().map(|_| quote! { String }).collect();
let select_clause = if column_names.len() == 1 && column_names[0] == table_name {
"*".to_string()
} else {
column_names.join(", ")
};
let where_part = match &query.where_clause {
Some(w) => format!(" WHERE {}", w.value()),
None => String::new(),
};
let sql_string = format!("SELECT {} FROM {}{}", select_clause, table_name, where_part);
let expanded = quote! {
struct #struct_name {
#(pub #field_names: #field_types,)*
}
impl #struct_name {
pub fn sql() -> &'static str {
#sql_string
}
pub fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
Ok(Self {
#(
#field_names: row.try_get(stringify!(#field_names))?,
)*
})
}
}
};
expanded.into()
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
})
.collect()
}
fn to_snake_case(s: &str) -> String {
s.to_lowercase()
}
use my_macros::sql;
sql!(users.select(id, name, email).where_clause("active = true"));
sql!(orders.select(id, user_id, total));
sql!(products);
パターン5:syn/quoteによるプロダクションコード生成
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, DeriveInput, Data, Fields, Attribute, Meta, Lit, NestedMeta,
GenericParam, LifetimeParam, TypeParam, ConstParam,
};
#[proc_macro_derive(ApiEndpoint, attributes(api_route, api_method, api_auth))]
pub fn derive_api_endpoint(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let route = get_string_attr(&input.attrs, "api_route").unwrap_or_else(|| "/".to_string());
let method = get_string_attr(&input.attrs, "api_method").unwrap_or_else(|| "GET".to_string());
let auth_required = get_bool_attr(&input.attrs, "api_auth").unwrap_or(false);
let request_type = Ident::new(&format!("{}Request", name), name.span());
let response_type = Ident::new(&format!("{}Response", name), name.span());
let handler_name = Ident::new(&format!("handle_{}", to_snake_case(&name.to_string())), name.span());
let (request_fields, response_fields) = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
let req_fields: Vec<_> = fields.named.iter()
.filter(|f| !has_attr(&f.attrs, "response_only"))
.map(|f| {
let fname = f.ident.as_ref().unwrap();
let ftype = &f.ty;
quote! { pub #fname: #ftype }
}).collect();
let resp_fields: Vec<_> = fields.named.iter()
.filter(|f| !has_attr(&f.attrs, "request_only"))
.map(|f| {
let fname = f.ident.as_ref().unwrap();
let ftype = &f.ty;
quote! { pub #fname: #ftype }
}).collect();
(req_fields, resp_fields)
}
_ => (vec![], vec![]),
}
}
_ => (vec![], vec![]),
};
let method_ident = match method.to_uppercase().as_str() {
"POST" => quote! { axum::routing::post },
"PUT" => quote! { axum::routing::put },
"DELETE" => quote! { axum::routing::delete },
"PATCH" => quote! { axum::routing::patch },
_ => quote! { axum::routing::get },
};
let auth_middleware = if auth_required {
quote! {
.layer(axum::middleware::from_fn(auth_middleware))
}
} else {
quote! {}
};
let route_lit = syn::LitStr::new(&route, name.span());
let generics = &input.generics;
let phantom_generics: Vec<_> = generics.params.iter().map(|param| {
match param {
GenericParam::Type(t) => {
let ident = &t.ident;
quote! { std::marker::PhantomData<#ident> }
}
GenericParam::Lifetime(l) => {
let lifetime = &l.lifetime;
quote! { std::marker::PhantomData<&#lifetime ()> }
}
GenericParam::Const(c) => {
let ident = &c.ident;
quote! { std::marker::PhantomData<[(); #ident]> }
}
}
}).collect();
let expanded = quote! {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #request_type {
#(#request_fields,)*
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct #response_type {
#(#response_fields,)*
#(pub #phantom_generics,)*
}
pub async fn #handler_name(
axum::Json(req): axum::Json<#request_type>,
) -> Result<axum::Json<#response_type>, AppError> {
let response = #name::handle(req).await?;
Ok(axum::Json(response))
}
impl #name {
pub fn register(router: axum::Router<SharedState>) -> axum::Router<SharedState> {
router.route(#route_lit, #method_ident(#handler_name))
#auth_middleware
}
}
};
expanded.into()
}
fn get_string_attr(attrs: &[Attribute], name: &str) -> Option<String> {
attrs.iter().find_map(|attr| {
if attr.path.is_ident(name) {
if let Ok(Meta::List(list)) = attr.parse_meta() {
if let Some(NestedMeta::Lit(Lit::Str(s))) = list.nested.first() {
return Some(s.value());
}
}
}
None
})
}
fn get_bool_attr(attrs: &[Attribute], name: &str) -> Option<bool> {
attrs.iter().find_map(|attr| {
if attr.path.is_ident(name) {
if let Ok(Meta::Path(_)) = attr.parse_meta() {
return Some(true);
}
}
None
})
}
fn has_attr(attrs: &[Attribute], name: &str) -> bool {
attrs.iter().any(|attr| attr.path.is_ident(name))
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
}
result
}
use my_derive::ApiEndpoint;
#[derive(ApiEndpoint)]
#[api_route("/api/users")]
#[api_method("GET")]
#[api_auth]
pub struct ListUsers {
pub page: u32,
pub page_size: u32,
#[request_only]
pub search: Option<String>,
#[response_only]
pub users: Vec<User>,
#[response_only]
pub total: u64,
}
#[derive(ApiEndpoint)]
#[api_route("/api/users")]
#[api_method("POST")]
pub struct CreateUser {
pub name: String,
pub email: String,
#[response_only]
pub id: String,
#[response_only]
pub created_at: String,
}
落とし穴ガイド
落とし穴1:macro_rules!の衛生性(hygiene)の罠
// ❌ 間違い:宣言マクロで導入した変数名が呼び出し元のスコープと衝突する可能性
macro_rules! swap {
($a:expr, $b:expr) => {
let temp = $a; // tempが呼び出し元のtempと衝突する可能性
$a = $b;
$b = temp;
};
}
// ✅ 正しい:ユニークなプレフィックスで名前衝突を回避
macro_rules! swap {
($a:expr, $b:expr) => {
let __swap_temp = $a;
$a = $b;
$b = __swap_temp;
};
}
落とし穴2:手続きマクロクレートをメインクレートに配置
# ❌ 間違い:手続きマクロとメインコードを同じクレートに配置
[package]
name = "my_app"
[lib]
proc-macro = true # コンパイルエラー!proc-macroクレートには非マクロコードを含められない
# ✅ 正しい:手続きマクロは独立したクレートに配置
# my_derive/Cargo.toml
[package]
name = "my_derive"
[lib]
proc-macro = true
# my_app/Cargo.toml
[dependencies]
my_derive = { path = "../my_derive" }
落とし穴3:TokenStreamのデバッグで出力が見えない
// ❌ 間違い:手続きマクロ内のprintln!はコンパイル時に出力されない
#[proc_macro_derive(MyDebug)]
pub fn derive_debug(input: TokenStream) -> TokenStream {
println!("Debugging: {}", input); // コンパイル時に見えない!
// ...
}
// ✅ 正しい:eprintln!でstderrに出力、またはcargo expandを使用
#[proc_macro_derive(MyDebug)]
pub fn derive_debug(input: TokenStream) -> TokenStream {
eprintln!("Debugging: {}", input); // コンパイル時にstderrに出力
// ...
}
// その後: RUST_BACKTRACE=1 cargo build 2>&1 | head
// または: cargo expand
落とし穴4:synでジェネリクスをパースする際のwhere句の見落とし
// ❌ 間違い:ジェネリクス制約を無視、生成コードがコンパイルエラーになる可能性
let name = &input.ident;
let generics = &input.generics;
quote! {
impl #generics MyTrait for #name #generics { ... }
}
// ✅ 正しい:split_for_implでジェネリクスを正しく処理
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics MyTrait for #name #ty_generics #where_clause { ... }
}
落とし穴5:属性マクロが元のitemを消費してしまう
// ❌ 間違い:属性マクロが元の関数を保持せず、他の属性が失われる
#[proc_macro_attribute]
pub fn my_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
// 新しいコードだけを返し、元のItemFnが破棄される
quote! { fn new_func() {} }.into()
}
// ✅ 正しい:元のitemをベースにラップし、元のコードを保持
#[proc_macro_attribute]
pub fn my_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let ItemFn { attrs, vis, sig, block, .. } = input;
quote! {
#(#attrs)*
#vis #sig {
// 前置ロジック
let __result = #block;
// 後置ロジック
__result
}
}.into()
}
エラートラブルシューティング
| # | エラー | 原因 | 解決方法 |
|---|---|---|---|
| 1 | proc-macro crate cannot export non-macro items |
proc-macroクレートに非マクロのpub項目を定義 | 補助型/関数を別クレートに移動、proc-macroクレートにはマクロ定義のみ |
| 2 | cannot find macro my_derive in this scope |
deriveクレートの依存関係が未追加 | Cargo.tomlにmy_derive = { path = "..." }を追加 |
| 3 | expected ident, found punctuation |
synがTokenStreamパース時に識別子を期待したが記号を発見 | マクロ呼び出し構文を確認、パラメータの順序と型が一致していることを確認 |
| 4 | recursion limit reached while expanding macro |
宣言マクロの再帰展開がデフォルト制限を超過 | lib.rsの先頭に#![recursion_limit = "256"]を追加 |
| 5 | mismatched types: expected TokenStream, found String |
手続きマクロがStringではなくTokenStreamを返すべき | quote!{...}.into()またはinput.to_string().parse().unwrap()を使用 |
| 6 | the trait bound X: Y is not satisfied |
マクロ生成コードにtrait制約が欠落 | ジェネリクスパラメータに制約を追加:where T: Trait |
| 7 | procedural macro panicked |
手続きマクロ内部でpanicが発生 | syn::Error::new_spanned()でコンパイルエラーを返すよう変更 |
| 8 | variable 'temp' is not available in current scope |
宣言マクロの衛生性問題、導入変数が不可視 | $crate::プレフィックスを使用または変数をパラメータとして渡す |
| 9 | cargo expandで予期しないコードが表示 |
マクロ展開結果が期待と異なる | 手続きマクロにeprintln!デバッグを追加、quote!出力を段階的に確認 |
| 10 | attribute macro must be in proc-macro crate |
属性マクロが通常クレートに定義されている | 属性マクロをproc-macro = trueの独立クレートに移動 |
高度な最適化
1. 手続きマクロのエラーレポートとSpan位置特定
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Error};
#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let validations = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
let field_checks: Vec<_> = fields.named.iter().map(|f| {
let field_name = f.ident.as_ref().unwrap();
let field_name_str = field_name.to_string();
let type_checks = if let Some(attr) = f.attrs.iter().find(|a| a.path.is_ident("validate")) {
let mut checks = Vec::new();
if let Ok(meta) = attr.parse_meta() {
if let syn::Meta::List(list) = meta {
for nested in list.nested.iter() {
match nested {
syn::NestedMeta::Meta(syn::Meta::Path(path)) => {
if path.is_ident("non_empty") {
checks.push(quote! {
if self.#field_name.is_empty() {
return Err(format!("Field '{}' must not be empty", #field_name_str));
}
});
} else if path.is_ident("positive") {
checks.push(quote! {
if self.#field_name <= 0 {
return Err(format!("Field '{}' must be positive", #field_name_str));
}
});
}
}
_ => {}
}
}
}
}
checks
} else {
vec![]
};
Ok(type_checks)
}).collect::<Result<Vec<_>, _>>();
match field_checks {
Ok(checks) => checks.into_iter().flatten().collect(),
Err(e) => return e.to_compile_error().into(),
}
}
_ => {
return Error::new_spanned(
name,
"Validate derive only supports structs with named fields",
).to_compile_error().into();
}
}
}
_ => {
return Error::new_spanned(
name,
"Validate derive only supports structs",
).to_compile_error().into();
}
};
let expanded = quote! {
impl #name {
pub fn validate(&self) -> Result<(), String> {
#(#validations)*
Ok(())
}
}
};
expanded.into()
}
2. 宣言マクロの再帰とTTチューアー
macro_rules! impl_from_for_enum {
(@inner $name:ident { $($variant:ident($inner:ty)),* $(,)? }) => {
$(
impl From<$inner> for $name {
fn from(value: $inner) -> Self {
$name::$variant(value)
}
}
)*
};
($name:ident { $($variant:ident($inner:ty)),* $(,)?) => {
impl_from_for_enum!(@inner $name { $($variant($inner)),* });
};
}
impl_from_for_enum! {
AppError {
Io(std::io::Error),
Serde(serde_json::Error),
Http(reqwest::Error),
Database(sqlx::Error),
}
}
macro_rules! hash_map {
(@single $($x:tt)*) => { () };
(@count $($rest:expr),*) => { <[()]>::len(&[$(hash_map!(@single $rest)),*]) };
($($key:expr => $value:expr),* $(,)?) => {{
let cap = hash_map!(@count $($key),*);
let mut map = std::collections::HashMap::with_capacity(cap);
$(
map.insert($key, $value);
)*
map
}};
}
let config = hash_map! {
"host" => "localhost",
"port" => "8080",
"database" => "myapp",
};
3. 手続きマクロのインクリメンタルコンパイルとパフォーマンス最適化
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro_derive(LargeDerive, attributes(large_attr))]
pub fn derive_large(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let trait_impl = generate_trait_impl(&input);
quote! {
impl #impl_generics LargeTrait for #name #ty_generics #where_clause {
#trait_impl
}
}.into()
}
fn generate_trait_impl(input: &syn::DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
match &input.data {
syn::Data::Struct(data) => {
match &data.fields {
syn::Fields::Named(fields) => {
let field_count = fields.named.len();
if field_count > 50 {
return syn::Error::new_spanned(
name,
format!("Struct has {} fields, consider splitting into smaller structs", field_count),
).to_compile_error();
}
let field_names: Vec<_> = fields.named.iter()
.filter_map(|f| f.ident.as_ref())
.collect();
let field_strs: Vec<String> = field_names.iter().map(|f| f.to_string()).collect();
quote! {
fn field_count() -> usize {
#field_count
}
fn field_names() -> Vec<&'static str> {
vec![#(#field_strs),*]
}
}
}
_ => quote! { fn field_count() -> usize { 0 } },
}
}
_ => quote! { fn field_count() -> usize { 0 } },
}
}
比較分析
| 次元 | macro_rules! | Deriveマクロ | 属性マクロ | 関数ライク手続きマクロ | C++テンプレート |
|---|---|---|---|---|---|
| 定義方法 | macro_rules! |
#[proc_macro_derive] |
#[proc_macro_attribute] |
#[proc_macro] |
template<> |
| 入力型 | パターンマッチ | TokenStream | TokenStream×2 | TokenStream | 型パラメータ |
| 出力型 | テンプレート置換 | TokenStream | TokenStream | TokenStream | 型/関数 |
| ASTアクセス | ❌なし | ✅synパース | ✅synパース | ✅synパース | ❌なし |
| エラーレポート | ⚠️不十分 | ✅Span位置特定 | ✅Span位置特定 | ✅Span位置特定 | ❌非常に不十分 |
| デバッグ方法 | cargo expand | cargo expand | cargo expand | cargo expand | コンパイルエラー |
| 学習曲線 | ⭐中 | ⭐急 | ⭐急 | ⭐急 | ⭐非常に急 |
| 適用場面 | DSL/テンプレート | trait自動実装 | デコレータ/コード注入 | カスタム構文 | ジェネリックアルゴリズム |
| 独立クレート | ❌不要 | ✅必須 | ✅必須 | ✅必須 | ❌不要 |
| コンパイル速度 | ⭐速い | ⭐遅い | ⭐遅い | ⭐遅い | ⭐非常に遅い |
| 型安全性 | ❌なし | ⚠️展開後チェック | ⚠️展開後チェック | ⚠️展開後チェック | ✅コンパイル時 |
まとめ:Rustマクロメタプログラミングのコアアドバンテージはコンパイル時コード生成にある——宣言マクロのテンプレート置換から手続きマクロのAST変換まで、マクロはランタイムパフォーマンスを犠牲にすることなく重複コードを排除できる。2026年のプロダクション実践:
macro_rules!でDSLとテンプレート構築→Deriveマクロでtrait自動実装→属性マクロでデコレータ→関数ライク手続きマクロでカスタム構文→syn+quoteで複雑なコード生成。重要な洞察:マクロの境界を理解すること——マクロはコード生成器であり、ランタイム抽象ではない。ジェネリクスで解決できるならマクロを使わない、traitで解決できるならマクロを使わない、マクロは最後の武器である。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format — マクロ生成JSON設定コードのフォーマット
- コードフォーマッター:/ja/dev/code-formatter —
cargo expand出力のマクロ展開コードのフォーマット - Hash計算ツール:/ja/encode/hash — APIキーや設定ファイルのハッシュ値計算
ブラウザローカルツールを無料で試す →