宏的分类

使用泳道图

宏编程使用对比泳道图

两类四种宏对比(具体内容看详细版泳道图)基于TokenStream定义过程函数宏打开过程宏手工抽取TokenStream定义派生宏打开过程宏syn/quote抽取TokenStream为DeriveInput定义派生宏打开过程宏syn/quote抽取TokenStream为DeriveInput定义属性宏打开过程宏使用anyhow与askama抽取TokenStream中的信息1. 分别定义BuilderContext和Fd2. BuilderContext处理:jinja模版数据结构3. Fd描述每个fieldtemplates/builder.j2 编写与tokenstream对应的jinja2模版实现对应抽取方法1. Fd实现new方法处理TokenTree2. BuilderContext实现下列方法:- new:从 TokenStream 中提取信息,构建 BuilderContext- render:把jinja2模版渲染成字符串代码> render里面用到split和get_struct_fields方法使用syn与quote抽取TokenStream中的信息1. 同样定义BuilderContext和Fd不需要自己手动定义抽取模版,直接实现方法1. 比起手动方式,BuilderContext和Fd还需要实现From Trait。2. 接着Fd就不需要再处理,主要在BuilderContext3. BuilderContext实现下列方法- render: 用到quote!- gen_optionized_fields(&self)- gen_methods(&self)- gen_assigns(&self)使用syn与quote抽取TokenStream中的信息1. 定义Opts、Fd、BuilderContext2. 比起派生宏,多了Opts用于捕获Fd的属性不需要自己手动定抽取模版,直接实现方法1. 和派生宏一样,给Fd、BuilderContext和Fd还需要实现From Trait2. BuilderContext同样实现下列方法:- render: 用到quote!- gen_optionized_fields(&self)- gen_methods(&self)- gen_assigns(&self)定义声明宏定义过程宏定义过程派生宏定义过程派生宏定义过程派生宏和派生宏不同的是这里多了一个attributes(builder) 属性,这是告诉编译器,请允许代码中出现的1.[builder(...)],它是我这个宏认识并要处理的使用声明宏没有TokenStream难以调试使用过程宏Terminal查看打印的TokenStream使用派生宏抽取Terminal查看打印的TokenStream使用派生宏抽取Terminal查看打印的DeriveInput使用派生宏抽取Terminal查看打印的DeriveInputCargo.tomlsrc/<抽取字段与方法定义>.rssrc/lib.rsexamples/调用代码

表格

Macros
Define
Usage
note
Example
Declarative Macro
#[macro_export]/macro_rules! macro_name{}
macro_name!()
println!
Function Macro
#[proc_macros]/pub fn macro_name
macro_name!()
advanced declarative macro
Derive Macro
#[proc_macros_derive(DeriveMacroName)]/pub fn other_fn_name
DeriveMacroName!()
#[derive(Debug)]
Attritubte Macro
#[proc_macros_derive(AttributeMacroName, attributes(attr_name))]/pub fn other_fn_name
Only Diff with DeriveMacro when define struct

声明宏(declarative macros): macro_rules!(bang)

对代码模版做简单替换 声明宏可以用 macro_rules! 来描述, 如果重复性的代码无法用函数来封装,那么声明宏就是一个好的选择

声明宏的缺陷,而后有了过程宏

为什么过程宏和声明宏那么像

过程宏的缺陷

  1. 缺乏对宏自动完成和扩展的支持
  2. 调试声明性宏很困难
  3. 修改能力有限
  4. 较大的二进制文件
  5. 更长的编译时间(这适用于声明性宏和过程宏)

过程宏是语法树级别的转换 过程宏是宏的更高级版本。过程宏允许你扩展现有的 Rust 语法。它接受任意输入并返回有效的 Rust 代码。 过程宏是将 TokenStream 作为输入并返回另一个 Token Stream 的函数。过程宏操作输入 TokenStream 以产生输出流。

过程宏:深度定制与生成代码

陈天B站视频另外提供详细演示

主要以如何使用 function-like macro 在不依赖于 syn / quote 的情况下,把 Json Schema 在编译期转换成 Rust struct。主要目的是让大家熟悉基本的处理 TokenStream 的思路

主要通过一个 derive Builder 宏,来展示使用 syn/quote 如何开发过程宏。

做个收尾,对上一讲的 derive macro 支持 attributes。 我们可以直接解析 attributes 相关的 TokenStream,也可以使用 darling 这个很方便的库,直接把 attributes 像 Clap/Structopts 那样收集到一个数据结构中,然后再进一步处理。

总结

这三讲的内容虽然简单,但足以应付大家绝大多数宏编程的需求。 其实我们现在对 syn 库的使用还只是一个皮毛,我们还没有深入 去撰写自己的数据结构去实现 Parse trait,像 DeriveInput 那样可以直接把 TokenStream 转换成我们想要的东西。

大家感兴趣的话,可以自行去看 syn 库的文档。

1. 函数宏

看起来像函数的宏,但在编译期进行处理.

sqlx 用函数宏来处理SQL query、tokio使用属性宏 #[tokio::main] 来引入 runtime。 它们可以帮助目标代码的实现逻辑变得更加简单, 但一般除非特别必要,否则并不推荐写。 并没有特定的使用场景

2. 属性宏

可以在其他代码块上添加属性,为代码块提供更多功能。

3. 派生宏

为 derive属性添加新的功能。这是我们平时使用最多的宏,比如 #[derive(Debug)].

如果你定义的 trait 别人实现起来有固定的模式可循,那么可以考虑为其构建派生宏