过程宏用 Rust 程序处理 token 并输出 token,适合声明宏难以表达的结构化解析和代码生成。它运行在宿主编译环境,输出才交给目标 crate 编译;它不是可以查询任意变量真实类型或 trait 实现的通用编译器插件。
调用者的 TokenStream derive 输入类型;attribute 输入属性与被标注项 1 解析并检查语法结构 syn 提供 AST;这里还没有任意类型推导结果 2 生成 TokenStream quote 插值,保留泛型、约束与诊断位置 3 调用者继续编译 生成项参与名称解析、类型与借用检查 4 宏执行环境与最终程序的目标平台可能不同。 过程宏在宿主运行,输出代码进入目标 crate
三种宏如何改变调用位置
类别 输入 输出行为
#[derive(MyTrait)] 被派生的 struct、enum 或 union 在原定义后追加生成项,保留原类型定义
#[my_attr(...)] 属性参数及被标注项两个 token 流 替换被标注项,可输出多个项
my_macro!(...) 分隔符内的 token 流 替换宏调用,须符合所在语法位置
attribute 宏如果忘记把原函数输出,就会真的移除原函数;derive 不能靠输出一个同名新结构体修改原字段。宏也可能报错、panic 或陷入无限执行,应把错误输入的行为作为接口设计的一部分。Rust 过程宏规则
一个保留泛型的 derive 工程
创建 workspace 根 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 trait;正式公共库应设计稳定的 trait 路径或可配置路径,并考虑依赖被重命名的情况,不要把这个约定隐含给所有用户。
为什么要拆开泛型三部分
impl、Example 和 where T: Copy 出现的位置与语法职责不同,不能把整个 generics 对象在所有位置重复插值。split_for_impl 为它们提供对应形式,并处理 impl 中不应重复的默认参数等细节。syn Generics
如果生成方法读取字段并要求 Display 等 trait,还需要添加恰当约束。不能为了方便给全部泛型参数一律加同一个 bound,字段可能使用 PhantomData、关联类型或不涉及该参数;应根据生成代码的真实需求设计。
示例使用 expanded 作为变量名;gen 在 Rust 2024 中是保留关键字,旧教程的 let gen 不能原样用于该 edition,除非写成原始标识符 r#gen。gen 关键字
同一个 derive,验证三类泛型与默认参数
保持 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 子句跟在类型之后。这三种位置的 token 不是同一段语法,直接反复插值整份声明可能生成非法代码。
这里根 trait 在子模块中仍由 crate::Hello 找到。如果将 trait 移到别处,就要同步改变生成路径或设计可配置入口;“子模块调用成功”不等于“任意依赖布局都自动适配”。
生成代码没用到字段,就不该强加字段约束
下面也是可替换的 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=u8,impl 的参数声明却不应照抄默认值,Record 的应用位置只写 T。依次切换这些语法要素,观察 split_for_impl 解决的是位置相关的语法组织,而不是字段类型检查。
正在呈现知识画面 重新加载
同一组泛型,为什么拆成三份? impl 声明参数,类型应用传入参数,where 保留约束;默认值不重复写进 impl。
{"id":"rust-derive-generics","title":"同一组泛型,为什么拆成三份?","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":"生成的实现头","heading":"同一组泛型,为什么拆成三份?","intro":"开关生命周期、类型默认值、const 参数与 where,观察生成位置。","lifetime":"包含生命周期","parts":"split_for_impl 的三个输出","reset":"重置","scope":"仅生成本文 Hello 的实现头,不做通用 Rust token 解析,也不自动判断字段所需 trait。","scopeLabel":"模型范围与约定"}}
syn 与 quote 不是类型检查器
syn 解析语法树,quote 通过插值构造 token。看到字段类型别名 Alias,不代表宏能查询它最终等于什么类型;通过 token 字符串猜测类型也容易受路径、泛型和格式变化影响。
过程宏整体上是不卫生的,生成的名字和路径会受调用环境影响。优先使用合适的限定路径,避免与用户项冲突,并用输入 span 把错误指向相关声明。TokenStream 转成字符串再做字符串替换,通常会丢失结构和诊断质量。
辅助属性也需要完整语法契约
如果 derive 希望接受 #[hello(...)],必须在 #[proc_macro_derive(Hello, attributes(hello))] 中注册相应 helper attribute,并在解析代码中处理它。注册只是让编译器允许该属性存在,不会自动执行配置,也不会替宏验证它的位置或内容。
例如准备支持 #[hello(trait_path = "crate::api::Hello")] 时,应先定义以下行为;本篇上面的最小 Hello 实现尚未提供这个选项。
输入 宏应明确决定的结果
未配置路径 使用文档规定的默认路径
配置合法路径字符串 解析为 syn::Path 后用于生成代码
未知键名 指向该键并报告不支持的选项
重复指定路径 拒绝重复或明确覆盖规则,避免偶然取最后一个
路径语法无效 在输入字符串位置报告解析错误
属性写在不支持的位置 明确拒绝,避免静默忽略
syn 的 parse_nested_meta 适合常见的键值/嵌套属性语法,LitStr 可以进一步解析为 Path;自定义 DSL 则可实现 Parse。不要先把整个属性转成字符串,再依靠逗号和等号 split,因为括号分组、转义和嵌套都会破坏这种解析方式。syn 2 属性解析
属性语法错误 键名、重复键、参数格式 不支持的结构 例如 Hello 拒绝 union 生成代码的类型错误 由目标 crate 的类型检查发现 优先保留输入 span,给出具体原因和可修改的位置 把错误留在最了解它的阶段,并指向输入位置 不要把所有失败都变成 proc macro panicked,也不要用字符串猜类型 这三类检查的责任不同;宏能解析语法,不意味着它拥有类型检查器的全部信息。
crate 依赖方向
宏定义必须位于 proc-macro crate 根,不能在定义它的同一个 crate 中直接使用;可由独立使用者、集成测试等调用。该 crate 可以有私有辅助函数和模块,但不能像普通库一样导出任意公开运行时 API。
workspace 成员当然可以依赖另一个成员提供的过程宏,限制是依赖图不能成环。常见结构是运行时接口库与 derive 库分工,必要时由接口库再导出宏;生成的代码引用某条路径不等于宏 crate 自己必须依赖那个运行时 crate。
宏执行可以访问宿主文件系统等资源,所以构建应控制外部输入、避免依赖偶然的当前目录和不可追踪的环境变化。跨编译时,宏内 cfg 看到宿主配置;需要目标条件通常应生成目标代码中的 cfg,或采用明确传入的构建信息。
生成的目标路径与宏自己的构建依赖
若宏输出 ::runtime_api::Trait,这是要求使用者编译生成代码时能解析到该路径,并不表示宏自身正在调用 runtime_api 的代码。宏 crate 可以只依赖 syn/quote 来生成 token;运行时 API crate 再重导出 derive,也不必因此制造反向依赖环。
依赖重命名会改变使用方可见的 crate 名字。公共宏可以提供路径配置,或采用经过验证的依赖名解析方案;仅写死一个看起来绝对的路径,解决了部分名称冲突,却没有解决重命名问题。过程宏里也不能把声明宏专用的 $crate 当作通用路径补丁。
诊断与展开结果的用法
预期的错误输入宜用 syn::Error 等生成 compile_error!,保留位置和明确消息,不用 panic 充当所有错误的报告方式。上面的 union 会在类型名处给出拒绝原因。
cargo expand 是额外安装的工具,可帮助查看生成结构。它显示的是文本化展开结果,不能完整保留卫生性等编译器内部信息,因此输出不保证复制后行为完全等价,也不是最终汇编。cargo-expand
验证过程宏时至少检查简单类型、泛型及 where 子句、模块位置和不支持输入。只看到示例代码展开得“像一个 impl”,还不能证明生成库适合真实调用者。
错误样例要编译失败,成功样例要真的被调用
用例 应验证的行为
普通 struct 生成实现可调用,原字段仍存在
生命周期 + 类型 + const 泛型 实现头与类型应用参数合法
默认泛型参数 不把默认值复制到 impl 泛型声明
子模块中的类型 生成路径按约定解析
泛型参数没有额外 trait 不因宏生成而多出无关约束
union 明确报 Hello does not support unions
trait 不存在或被改名 不能假装生成代码已经通过名称检查
可以用专门的编译测试框架管理 pass/fail 用例,也可以在小型 workspace 中分别编译独立样例。诊断断言宜关注宏提供的核心消息和关键位置;完整 rustc 输出可能随版本变化,更新快照时应检查变化原因。
cargo expand 帮助检查“生成了什么”,运行样例检查“接口是否按预期工作”,失败样例检查“哪些输入明确不支持”。这三种证据互相补充;打印出漂亮的 token 并不足以保证泛型、路径和诊断在真实使用方成立。