9.2 过程宏:派生、属性与函数式


9.2 过程宏:派生、属性与函数式

本节摘要:过程宏接收语法树(TokenStream)、用代码生成代码,分派生、属性、函数式三类;derive(Debug) 之类的日常魔法都出自它。本节讲三类宏的形态与适用、派生宏的工作流程。读完你对生态里"注解即实现"的库有机制级理解。

三类过程宏

类别 形态 用途 生态代表
派生宏 加在类型定义上 自动实现 Trait Debug、Clone、serde 序列化
属性宏 包住一个条目 改写或包装代码 测试框架的路由注册
函数式宏 像函数调用 完全自定义语法 SQL 与配置内嵌 DSL
#[derive(Debug, Clone)] // 派生宏:为 Exhibit 自动写实现 struct Exhibit { id: u8 } #[route("/archive")] // 属性宏:把函数登记为处理入口 fn archive_page() -> &'static str { "档案首页" } sql_query!(SELECT * FROM cases); // 函数式宏:自定义语法(示意)

派生宏的工作流程

以 derive(Debug) 为例:编译器把 struct Exhibit { id: u8 } 的语法树交给派生宏;宏遍历字段(id 是 u8,u8 实现了 Debug),生成一份匹配的 fmt 实现;生成的代码与手写的完全等价,拼回编译流程。serde 派生同原理,只是生成的序列化代码更精细——字段名、跳过规则、重命名,都从属性参数里读。

过程宏必须放在独立的 proc-macro 类型的 crate 里(清单里声明),它编译成动态库供编译器加载。这是第 7.2 节 crate 形态知识的延伸:proc-macro 是第三种 crate

图:派生宏的编译期流水

图:派生宏的编译期流水

要点回顾

  • 三类过程宏各有辖区:派生补实现、属性改条目、函数式造语法;
  • 生成物与手写等价,魔法只是自动化的样板;
  • proc-macro 是独立类型的 crate,由编译器加载执行;
  • 读生态库的 derive 文档时,看到的参数都是语法树上的注解。

三类过程宏的席位

过程宏是编译器插件,接收 token 流、返回 token 流。三类入口对应三种挂载点:derive 挂在 struct/enum 上,attribute 挂在任意项上并改写其本体,function-like 当函数调用。

// Cargo.toml —— 过程宏必须是独立 crate // [lib] // proc-macro = true use proc_macro::TokenStream; #[proc_macro_derive(CaseNumber, attributes(case_no))] // 派生宏:自动生成 impl pub fn derive_case(input: TokenStream) -> TokenStream { // input 是被标注的 struct/enum 的 token 流 let _src = input.to_string(); // 最低配:转字符串再拼装 let out = format!(r#" impl CaseNumber for filed {{ fn case_no() -> u32 {{ 173 }} }} "#); out.parse().expect("生成的 token 不合法") } #[proc_macro_attribute] // 属性宏:可吞掉并改写本体 pub fn audited(_attr: TokenStream, item: TokenStream) -> TokenStream { item // 原样放行 = 仅标记 } #[proc_macro] // 函数式:像函数一样调用 pub fn bench_case(input: TokenStream) -> TokenStream { input }
// 使用方 #[derive(CaseNumber)] struct filed; fn main() { println!("{}", filed::case_no()); // 173 —— impl 由宏生成 }

真实工程不手拼字符串,而是 syn 解析成语法树、quote 模板化生成——derive(CaseNumber) 的演示版刻意保留了"字符串进出"的最简形态,先看清插件协议再上工具,出问题时才有能力下钻。

derive 的解剖课

use proc_macro::TokenStream; #[proc_macro_derive(VerdictCount)] pub fn derive_count(input: TokenStream) -> TokenStream { let src = input.to_string(); // 约定:字段名为 u32 的都计入——演示"读结构、生成代码"的最短闭环 let n = src.matches(':').count(); let out = format!( "impl VerdictCount for Any {{ fn count() -> u32 {{ {} }} }}", n.max(0) ); out.parse().unwrap() }

这个演示说明 derive 的本质:编译期反射。#[derive(Debug)] 到 thiserror 的错误类型生成都在同一条流水线上,差别只在语法树分析的深度。排错要点:过程宏 panic 时编译器会把它转成错误输出并指向宏调用点,cargo expand(外部工具)能展开产物直接看生成代码,是过程宏调试的第一证物。

声明宏与过程宏的分界卡

维度 macro_rules 过程宏
匹配对象 有限模式语法 任意 token 流
生成方式 模板替换 程序构造
依赖 proc-macro2 / syn / quote
适合 样板展开、DSL 骨架 derive、属性改写、复杂代码生成

默认先用声明宏,模式语法表达不了意图再升级——过程宏的能力换来的编译成本与调试成本都高一档。

结案自测

三个判断题收束本章第二节:一,过程宏能在展开时读到字段类型吗——能,syn 解析出的语法树含类型信息,这是它对声明宏的本质优势;二,过程宏 crate 能被同一项目的 bin 直接依赖吗——能,但路径依赖即可,无需发布;三,derive 宏能访问被派生类型的私有字段吗——能,生成代码与类型同模块可见性。三题都指向同一认知:过程宏是编译期的反射程序,能力边界就是"语法树里有什么"。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U