attribute 宏与 derive 宏的执行顺序?

解读

国内面试中,这道题常被用来区分“只会用宏”与“真正懂宏展开机制”的候选人。
面试官想确认两点:

  1. 你是否清楚 attribute-like 宏(包括自定义 attribute 宏)derive 宏编译器管道中的阶段差异
  2. 你是否能据此解释 宏之间是否存在先后依赖,以及 如何规避“宏抢跑”导致的代码失效或语义漂移

知识点

  1. 宏展开阶段(Expansion Phase)
    Rust 编译器在 AST 生成之后、名称解析(Name Resolution)之前 统一做一次宏展开;attribute 宏与 derive 宏处于同一展开阶段,并无“谁先谁后”的硬性规定。
  2. 同一条目上的顺序由书写顺序决定
    对同一结构体/枚举/函数,靠近条目名称的宏先展开,远离的后展开
    例:
    #[tokio::main]          // 1. 先展开
    #[derive(Debug)]        // 2. 后展开
    async fn main() {}
    
    因此 attribute 宏先于 derive 宏 获得“未变形”的 AST,并可将后续宏所需节点保留或删除。
  3. 跨条目无全局顺序保证
    若 A 条目的宏想依赖 B 条目宏展开后的结果,必须显式使用 include! 或间接生成代码,否则编译器可能以任意顺序并行展开,导致找不到符号。
  4. ** hygiene 与节点标识**
    宏展开后编译器会重新生成 NodeId;后展开的宏看不到前展开宏的“内部”标识,只能看到最终 AST 形状。

答案

“attribute 宏与 derive 宏处于同一宏展开阶段;在单一条目上,谁写在前面谁先展开,写在后面的后展开。因此,如果 attribute 宏位于 derive 宏之前,它就先执行;反之则后执行。编译器不保证跨条目的展开顺序,依赖顺序必须显式控制。”

拓展思考

  1. 实战陷阱
    在写 #[serde(remote = "...")]#[derive(Serialize, Deserialize)] 组合时,若把 derive 写在前面,attribute 宏展开时 AST 尚未被 serde 改写,可能导致 remote trait 找不到;调换顺序即可修复。
  2. proc-macro 作者视角
    若你的 attribute 宏需要“消费掉”某些字段(例如删除标记为 #[cache] 的成员),必须在 derive 宏之前展开,否则 derive 宏看到的仍是完整字段列表,生成代码会包含已删除字段,造成编译失败。
  3. 并行编译趋势
    从 1.80 起编译器已支持 并行宏展开;未来同一 crate 内不同条目的宏展开顺序更加不可预测,因此 绝不要依赖“全局顺序”做语义假设,而应通过 生成中间 trait 或显式 include! 建立依赖。