プロシージャルマクロはRustプログラムを用いてトークンを処理し、トークンを出力します。宣言的マクロでは表現しにくい構造化された解析やコード生成に適しています。ホスト側のコンパイル環境で実行され、その出力のみがターゲットのcrateでコンパイルされます。任意の型やトレイト実装を照会できる汎用のコンパイラプラグインではありません。
呼び出し元のTokenStream deriveの入力型;attributeの入力属性と注釈付き項目 1 構文構造を解析・検証 synがASTを提供;ここではまだ任意の型推論結果はない 2 TokenStreamを生成 quoteによる補間、ジェネリクス、制約、診断位置を保持 3 呼び出し側でコンパイル継続 生成された項目は名前解決、型チェック、借用チェックに参加 4 プロシージャルマクロはホスト側で実行され、出力コードはターゲットcrateに入る マクロの実行環境と最終プログラムのターゲットプラットフォームは異なる場合があります。
3つのマクロが呼び出し位置に与える影響
カテゴリ 入力 出力動作
#[derive(MyTrait)] 派生対象のstruct、enum、またはunion 元の定義の直後に生成された項目を追加し、元の型定義を保持
#[my_attr(...)] 属性引数と注釈付き項目の2つのトークンストリーム 注釈付き項目を置き換え、複数の項目を出力可能
my_macro!(...) 区切り子内のトークンストリーム マクロ呼び出しを置き換え、構文上の位置に従う必要がある
attributeマクロで元の関数の出力を忘れると、本当に元の関数が削除されてしまいます。deriveは同名の新しい構造体を出力して元のフィールドを変更することはできません。マクロはエラーを返したり、パニックしたり、無限実行に陥る可能性もあるため、エラー入力に対する動作をインターフェース設計の一部として扱うべきです。Rustプロシージャルマクロのルール
ジェネリクスを保持するderiveの実装例
ワークスペースのルートにあるCargo.tomlを作成します:
[workspace]
members = ["hello-derive", "demo"]
resolver = "3"
hello-derive/Cargo.toml:
[package]
name = "hello-derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
syn = "2"
quote = "1"
hello-derive/src/lib.rs:
use proc_macro:: TokenStream;
use quote:: quote;
use syn:: { parse_macro_input, DeriveInput} ;
# [ proc_macro_derive ( Hello ) ]
pub fn derive_hello ( input : TokenStream) -> TokenStream {
let input = parse_macro_input! ( input as DeriveInput) ;
if matches! ( input.data, syn:: Data:: Union( _ ) ) {
return syn:: Error:: new_spanned( & input.ident, " Hello does not support unions" )
.to_compile_error ( ) .into ( ) ;
}
let name = & input.ident;
let ( impl_generics, type_generics, where_clause) = input.generics.split_for_impl ( ) ;
let expanded = quote! {
impl #impl_generics crate ::Hello for #name #type_generics #where_clause {
fn hello_name ( ) -> & 'static str {
:: core:: stringify! ( # name)
}
}
} ;
expanded.into ( )
}
demo/Cargo.toml:
[package]
name = "demo"
version = "0.1.0"
edition = "2024"
[dependencies]
hello-derive = { path = "../hello-derive" }
demo/src/main.rs:
trait Hello {
fn hello_name ( ) -> & 'static str ;
}
# [ derive ( hello_derive::Hello ) ]
struct Example < T> where T: Copy {
value : T,
}
fn main ( ) {
let value = Example { value: 42_ u32 } ;
assert_eq! ( value.value, 42 ) ;
assert_eq! ( Example:: < u32 > :: hello_name( ) , " Example" ) ;
println! ( " {} " , Example:: < u32 > :: hello_name( ) ) ;
}
cargo run -p demo を実行すると、Example が出力されるはずです。この教学用マクロは、呼び出し元crateのルートに Hello トレイトが存在することを明示的に要求しています。正式な公開ライブラリでは、安定したトレイトパスまたは設定可能なパスを設計し、依存関係がリネームされる場合も考慮すべきです。この規約をすべてのユーザーに暗黙的に課さないでください。
なぜジェネリクスを3つに分割するのか
impl<T>、Example<T>、および where T: Copy は、出現位置と構文上の役割が異なるため、generics オブジェクト全体をすべての位置で繰り返し補間することはできません。split_for_impl はそれぞれに対応する形式を提供し、impl 内でデフォルト引数を繰り返さないなどの詳細を処理します。syn Generics
生成されたメソッドがフィールドを読み取り、Display などのトレイトを要求する場合は、適切な制約を追加する必要があります。利便性のためにすべてのジェネリックパラメータに同じ境界を一律に付与しないでください。フィールドは PhantomData、関連型、またはそのパラメータに関係なく使用される可能性があります。生成コードの実際のニーズに基づいて設計してください。
例では expanded を変数名として使用しています。Rust 2024 では gen は予約語であるため、旧いチュートリアルの let gen は、原始識別子 r#gen としない限り、そのエディションではそのまま使用できません。gen キーワード
同じderiveで、3種類のジェネリクスとデフォルト引数を検証する
hello-derive を変更せずに、demo/src/main.rs を以下の完全なプログラムに置き換えます。この例は、ライフタイム、型パラメータ、constパラメータ、およびwhere節を同時に検証し、サブモジュール内でマクロを使用します。
trait Hello { fn hello_name ( ) -> & 'static str ; }
mod model {
# [ derive ( hello_derive::Hello ) ]
pub struct Record < 'a , T = u8 , const N: usize = 2>
where T: Copy,
{
pub name : & 'a str ,
pub values : [T; N],
}
}
fn main ( ) {
let name = String :: from( " sample" ) ;
let record: model:: Record< '_ > = model:: Record {
name: & name, values: [ 1 , 2 ] ,
} ;
assert_eq! ( record.name, " sample" ) ;
assert_eq! ( record.values, [ 1 , 2 ] ) ;
assert_eq! ( < model:: Record< '_ , u8 , 2> as Hello> :: hello_name( ) , " Record" ) ;
}
生成された実装のヘッダは構文的に impl<'a, T, const N: usize> crate::Hello for Record<'a, T, N> where T: Copy に相当します。impl 宣言箇所ではデフォルト引数を繰り返さず、型適用箇所では T と N を渡す必要があります。where節は型の後に続きます。これら3つの位置のトークンは同一の構文ではないため、宣言全体をそのまま繰り返し補間すると無効なコードが生成される可能性があります。
ここでは、ルートトレイトはサブモジュール内でも crate::Hello によって解決されます。トレイトを別の場所に移動した場合は、生成パスを同期して変更するか、設定可能なエントリポイントを設計する必要があります。「サブモジュールでの呼び出しが成功した」からといって、「任意の依存関係レイアウトに自動的に適応する」わけではありません。
生成コードがフィールドを使用していない場合、フィールドの制約を課すべきではない
以下も置き換え可能な demo/src/main.rs です。Hello は型名のみを返し、フィールドを読み取ったりコピーしたりしないため、T が Clone、Copy、または Display を持っていなくても、実装は成立するはずです。
trait Hello { fn hello_name ( ) -> & 'static str ; }
struct NoTraits ;
# [ derive ( hello_derive::Hello ) ]
struct Wrapper < T> ( std:: marker:: PhantomData< T> ) ;
fn main ( ) {
let _value = Wrapper:: < NoTraits> ( std:: marker:: PhantomData) ;
assert_eq! ( Wrapper:: < NoTraits> :: hello_name( ) , " Wrapper" ) ;
}
将来フィールドの印刷機能を追加する場合、実際にフォーマットされるフィールドの型に制約を課す必要があります。すべての T に機械的に Display を追加してはいけません。例えばフィールドが <T as Trait>::Item の場合、必要な能力は関連型に属している可能性があります。フィールドが PhantomData<T> の場合、T は同じ能力を持つ必要がないかもしれません。マクロの作者は、生成されたメソッドが実際に実行する操作から逆算して制約を導き出す必要があります。
同じジェネリックを 3 部分に分ける理由
既定値はコピーの誤りを見つけやすい箇所です。struct では T=u8 と宣言できても impl には既定値を繰り返さず、Record の適用では T だけを使います。切り替えて、split_for_impl が行うのは位置ごとの構文整理であり、フィールドの型検査ではないと確認します。
図を準備しています 再読み込み
同じジェネリックを 3 部分に分ける理由 impl はパラメータを宣言し、型適用は引数を渡し、where は制約を保持します。既定値は impl に繰り返しません。
{"id":"rust-derive-generics","title":"同じジェネリックを 3 部分に分ける理由","summary":"impl はパラメータを宣言し、型適用は引数を渡し、where は制約を保持します。既定値は impl に繰り返しません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"lifetime\" checked><span data-i18n=\"lifetime\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"default\" checked><span data-i18n=\"default\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"constant\" checked><span data-i18n=\"constant\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"bound\" checked><span data-i18n=\"bound\"></span></label><section class=\"panel\"><h3 data-i18n=\"declaration\"></h3><div id=\"declaration\"></div></section><section class=\"panel\"><h3 data-i18n=\"parts\"></h3><div id=\"parts\"></div></section><section class=\"panel\"><h3 data-i18n=\"generated\"></h3><div id=\"generated\"></div></section><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"explain\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n.panel>div{font:13px/1.8 ui-monospace,monospace;min-height:7.2em;white-space:pre-line;overflow-wrap:anywhere}#parts{min-height:10.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));function draw(){let l=$('#lifetime').checked,d=$('#default').checked,c=$('#constant').checked,b=$('#bound').checked,decl=[...(l?[\"'a\"]:[]),'T'+(d?' = u8':''),...(c?['const N: usize = 4']:[])],impl=[...(l?[\"'a\"]:[]),'T',...(c?['const N: usize']:[])],args=[...(l?[\"'a\"]:[]),'T',...(c?['N']:[])],w=b?'where T: Copy':'';set('declaration','struct Record<'+decl.join(', ')+'> '+w+' { … }');set('parts','impl_generics = <'+impl.join(', ')+'>\\nty_generics = <'+args.join(', ')+'>\\nwhere_clause = '+(w||'∅'));set('generated','impl<'+impl.join(', ')+'> Hello\\nfor Record<'+args.join(', ')+'>\\n'+w+' { … }');set('explanation',t('explain'));}inputs(draw);","audio":false,"strings":{"bound":"where T: Copy","constant":"const N を含む","declaration":"元の型宣言","default":"T の既定値を u8 にする","explain":"宣言・型引数の適用・制約は別の役割で、入れ替えられません。特に impl に既定値をコピーしてはいけません。","generated":"生成した impl ヘッダ","heading":"同じジェネリックを 3 部分に分ける理由","intro":"ライフタイム、型の既定値、const パラメータ、where を切り替え、生成位置を見ます。","lifetime":"ライフタイムを含む","parts":"split_for_impl の 3 出力","reset":"リセット","scope":"本文の Hello の実装ヘッダだけを生成し、汎用のトークン解析やフィールドの trait 推論は行いません。","scopeLabel":"モデルの範囲と前提"}}
syn と quote は型チェッカーではない
syn は構文木を解析し、quote は補間を通じてトークンを構築します。フィールドの型エイリアス Alias を見ても、マクロがそれが最終的にどの型に解決されるかを照会できるわけではありません。トークン文字列から型を推測することも、パス、ジェネリクス、フォーマットの変化の影響を受けやすいため容易ではありません。
プロシージャルマクロ全体として不衛生(unhygienic)であり、生成された名前やパスは呼び出し環境の影響を受けます。適切な修飾パスを使用してユーザーの項目との衝突を避け、入力の span を使用してエラーを関連する宣言に指向してください。TokenStream を文字列に変換して文字列置換を行うと、構造と診断の品質が失われることがほとんどです。
ヘルパー属性にも完全な構文契約が必要
derive が #[hello(...)] を受け付ける場合、#[proc_macro_derive(Hello, attributes(hello))] 内で対応するヘルパー属性を登録し、解析コードでそれを処理する必要があります。登録はコンパイラにその属性の存在を許可するだけで、設定を自動的に実行するわけでも、マクロがその位置や内容を検証するわけでもありません。
例えば #[hello(trait_path = "crate::api::Hello")] のサポートを準備する場合、以下の動作を定義する必要があります。前述の最小限の Hello 実装にはこのオプションはまだ提供されていません。
入力 マクロが明確に決定すべき結果
パスが未設定 ドキュメントで規定されたデフォルトパスを使用
有効なパス文字列が設定 syn::Path に解析し、生成コードに使用
不明なキー名 そのキーを指し、サポートされていないオプションを報告
パスが重複指定 重複を拒否するか、明確な上書きルールを定義し、最後の値が偶然取られるのを防ぐ
パスの構文が無効 入力文字列の位置で解析エラーを報告
サポートされていない位置に属性を記述 明確に拒否し、静かに無視されないようにする
syn の parse_nested_meta は一般的なキー値/ネスト属性構文に適しており、LitStr はさらに Path に解析できます。カスタムDSLの場合は Parse を実装します。属性全体を一度文字列に変換し、カンマとイコールで分割するのは避けてください。括弧のグループ化、エスケープ、ネストがその解析方法を壊す可能性があるためです。syn 2 属性解析
属性構文エラー キー名、重複キー、パラメータ形式 サポートされない構造 例えば Hello は union を拒否 生成コードの型エラー 対象 crate で型検査 入力のspanを優先して保持し、具体的な理由と修正可能な位置を示す エラーはそれを最もよく理解している段階に残し、入力位置を指し示す すべての失敗を「proc macro panicked」に変えるのではなく、文字列で型を推測しない これら3つのチェックの責任は異なります。マクロが構文を解析できるからといって、型チェッカーの全情報を持っている わけではありません。
crate の依存関係の方向
マクロ定義は proc-macro crate のルートに配置する必要があり、それを定義する同じcrate内では直接使用できません。独立したユーザーやインテグレーションテストなどから呼び出すことができます。このcrateにはプライベートな補助関数やモジュールを含めることができますが、通常のライブラリのように任意の公開ランタイムAPIをエクスポートすることはできません。
ワークスペースメンバーは当然、別のメンバーが提供するプロシージャルマクロに依存できます。制限は、依存関係グラフが循環してはならないことです。一般的な構造は、ランタイムインターフェースライブラリとderiveライブラリが役割を分担し、必要に応じてインターフェースライブラリがマクロを再エクスポートすることです。生成されたコードが某パスを参照しているからといって、マクロcrate自身がそのランタイムcrateに依存しているわけではありません。
マクロの実行はホストのファイルシステムなどのリソースにアクセスできるため、ビルドは外部入力を制御し、偶発的な現在のディレクトリや追跡不可能な環境変化への依存を避けるべきです。クロスコンパイル時、マクロ内の cfg はホストの設定を見ます。ターゲット条件が必要な場合は、通常、ターゲットコード内の cfg を生成するか、明示的に渡されたビルド情報を使用する必要があります。
生成されるターゲットパスとマクロ自身のビルド依存関係
マクロが ::runtime_api::Trait を出力する場合、これはユーザーが生成コードをコンパイルする際にそのパスに解決できることを要求するものであり、マクロ自身が runtime_api のコードを呼び出しているわけではありません。マクロcrateはトークンを生成するために syn/quote のみを依存関係として持つことができます。ランタイムAPI crate が derive を再エクスポートしても、それによって逆方向の依存関係ループが生じるわけではありません。
依存関係のリネームは、使用側から見えるcrate名を変更します。公開マクロはパス設定を提供するか、検証済みの依存関係名解決スキームを採用できます。単に絶対に見えるパスをハードコードしても、一部の名前衝突は解決できますが、リネームの問題は解決されません。プロシージャルマクロ内で宣言的マクロ専用の $crate を汎用的なパスパッチとして使用することはできません。
診断と展開結果の使い方
予期されるエラー入力については、syn::Error 等を使用して compile_error! を生成し、位置と明確なメッセージを保持し、panic をすべてのエラー報告手段として使用しないでください。上記の union は型名箇所で拒否理由を示します。
cargo expand は追加でインストールするツールであり、生成構造を確認するのに役立ちます。テキスト化された展開結果を表示しますが、衛生性などのコンパイラ内部情報を完全に保持するものではないため、出力をコピーしても動作が完全に同等である保証はなく、最終的なアセンブリでもありません。cargo-expand
プロシージャルマクロを検証する際は、少なくとも単純な型、ジェネリクスとwhere節、モジュール位置、およびサポートされない入力を確認してください。サンプルコードの展開が「implのように見える」からといって、生成されたライブラリが実際の呼び出し元に適していることを証明するものではありません。
エラーサンプルはコンパイル失敗を、成功サンプルは実際に呼び出されることを確認
ユースケース 確認すべき動作
通常の struct 生成された実装が呼び出可能で、元のフィールドが残っている
ライフタイム + 型 + const ジェネリクス 実装ヘッダと型適用パラメータが合法
デフォルトジェネリックパラメータ デフォルト値が impl ジェネリック宣言にコピーされない
サブモジュール内の型 生成パスが規約通りに解決される
ジェネリックパラメータに追加のトレイトがない マクロの生成により無関係な制約が増えない
union 「Hello does not support unions」を明確に報告
トレイトが存在しない、またはリネームされている 生成コードが名前チェックを通過したふりをしない
専用のコンパイルテストフレームワークを使用してパス/フェイル用例を管理することも、小規模なワークスペースで独立したサンプルを個別にコンパイルすることもできます。診断アサーションは、マクロが提供するコアメッセージと重要な位置に焦点を当てるべきです。完全な rustc の出力はバージョンによって変化する可能性があるため、スナップショットを更新する際は変更理由を確認してください。
cargo expand は「何が生成されたか」を確認するのに役立ち、サンプルを実行して「インターフェースが期待通りに動作しているか」を確認し、失敗サンプルで「どの入力が明確にサポートされていないか」を確認します。これら3つの証拠は互いに補完し合います。美しいトークンを出力しただけでは、ジェネリクス、パス、および診断が実際の呼び出し元で成立する保証にはなりません。