attribute 宏与 derive 宏的执行顺序?
解读
国内面试中,这道题常被用来区分“只会用宏”与“真正懂宏展开机制”的候选人。
面试官想确认两点:
- 你是否清楚 attribute-like 宏(包括自定义 attribute 宏) 与 derive 宏 在 编译器管道中的阶段差异;
- 你是否能据此解释 宏之间是否存在先后依赖,以及 如何规避“宏抢跑”导致的代码失效或语义漂移。
知识点
- 宏展开阶段(Expansion Phase)
Rust 编译器在 AST 生成之后、名称解析(Name Resolution)之前 统一做一次宏展开;attribute 宏与 derive 宏处于同一展开阶段,并无“谁先谁后”的硬性规定。 - 同一条目上的顺序由书写顺序决定
对同一结构体/枚举/函数,靠近条目名称的宏先展开,远离的后展开。
例:
因此 attribute 宏先于 derive 宏 获得“未变形”的 AST,并可将后续宏所需节点保留或删除。#[tokio::main] // 1. 先展开 #[derive(Debug)] // 2. 后展开 async fn main() {} - 跨条目无全局顺序保证
若 A 条目的宏想依赖 B 条目宏展开后的结果,必须显式使用 include! 或间接生成代码,否则编译器可能以任意顺序并行展开,导致找不到符号。 - ** hygiene 与节点标识**
宏展开后编译器会重新生成 NodeId;后展开的宏看不到前展开宏的“内部”标识,只能看到最终 AST 形状。
答案
“attribute 宏与 derive 宏处于同一宏展开阶段;在单一条目上,谁写在前面谁先展开,写在后面的后展开。因此,如果 attribute 宏位于 derive 宏之前,它就先执行;反之则后执行。编译器不保证跨条目的展开顺序,依赖顺序必须显式控制。”
拓展思考
- 实战陷阱
在写 #[serde(remote = "...")] 与 #[derive(Serialize, Deserialize)] 组合时,若把 derive 写在前面,attribute 宏展开时 AST 尚未被 serde 改写,可能导致 remote trait 找不到;调换顺序即可修复。 - proc-macro 作者视角
若你的 attribute 宏需要“消费掉”某些字段(例如删除标记为 #[cache] 的成员),必须在 derive 宏之前展开,否则 derive 宏看到的仍是完整字段列表,生成代码会包含已删除字段,造成编译失败。 - 并行编译趋势
从 1.80 起编译器已支持 并行宏展开;未来同一 crate 内不同条目的宏展开顺序更加不可预测,因此 绝不要依赖“全局顺序”做语义假设,而应通过 生成中间 trait 或显式 include! 建立依赖。