参考:https://doc.rust-lang.org/book/ch19-06-macros.html中文版

macro 含义

macro 指 Rust 中一系列的功能,包括以下两类:

  1. macro_rules! 声明(declarative)的宏
  2. 自定义式、属性式、函数式 三种过程(procedural) 宏

    声明式(declarative)

    语法:macro_rules!(args...)
    发生在编译期、类似于 match 表达式的结构:将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。
    作用:匹配传入的参数(代码、表达式)和模式(pattern),当每个模式匹配成功的时候,传入宏的参数(代码、表达式)被替换成相关的(Rust 源)代码
    eg:无法使用函数做到和 vec! 一样的功能——组成任意多且类型不同的“向量”。以下是 vec! 宏的简化版本: ``` //! 标准库中实际定义的 vec! 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。

// 如果没有 #[macro_export] 注解,这个宏不能被引入作用域

[macro_export]

// macro_rules! 和宏名称开始宏定义,且所定义的宏并 不带 感叹号 macro_rules! vec { ( $( $x:expr ), ) => { // 单边模式 ( $( $x:expr ), ) ,后跟 => 以及和模式相关的代码块 { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; }

  1. `vec!` 定义过程的的解读:
  2. 1.
  3. `( $( $x:expr ),* )` 被称为 “模式”(one arm with the pattern):
  4. 1. 首先,一对括号包含了整个模式
  5. 1. 接下来是美元符号( `$` ),后跟一对括号,捕获了符合括号内模式的值以用于替换后的代码
  6. 1. `$()`内则是`$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式记作 `$x`
  7. 1. `,` 表示多于一个元素时必须以逗号分隔,最后一个元素后的逗号可以省略。除了逗号分隔,如果需要也可用 `;` 做分隔符。
  8. 1. 紧随逗号之后的 `*` 说明该模式匹配零个或更多个 `*` 之前的任何模式
  9. 1. 当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1``2` `3` 进行了三次匹配
  10. 1. 假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
  11. 1.
  12. `=> { ... }` 被称为 与模式相关联的代码块:
  13. 1.
  14. 对于每个在 `=>` 前面的模式中的 `$()` 的部分,都会在 `=>` 后面的 `$()*` 内生成 `temp_vec.push()` 代码,生成的个数取决于该模式被匹配的次数
  15. 1.
  16. `$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:

let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec

  1. 1.
  2. `$()*` 之外的部分就是一般的语句、表达式;可以看到 `vec` 最后返回 temp_vec 这个变量的值
  3. 1.
  4. 至此我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码
  5. > 宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。对于全部的宏模式语法,请查阅[参考](275da7032c73008cd2d1bf1697c75f97)。
  6. `macro_rules!` 中有一些奇怪的地方。在将来,会有第二种采用 `macro` 关键字的声明宏,其工作方式类似但修复了这些极端情况。在此之后,`macro_rules!` 实际上就过时(deprecated)了。在此基础之上,同时鉴于大多数 Rust 程序员 **使用** 宏而非 **编写** 宏的事实,此处不再深入探讨 `macro_rules!`。请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”](30bd57640f0307514314a1688d914e7d) 来更多地了解如何写宏。
  7. # 过程宏(procedural)
  8. 过程宏:
  9. 1.
  10. 更像函数(一种过程类型)
  11. 1.
  12. 接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出
  13. 1.
  14. 不像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码
  15. 1.
  16. 有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似
  17. 1.
  18. 当创建过程宏时,其定义必须位于一种特殊类型的属于它们自己的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制
  19. 1.
  20. 使用过程宏需采用类似于 `#[proc_macro*]` 的代码形式,其中 `proc_macro*` 是过程宏的声明。三种过程宏的声明和使用方式:
  21. - derive macro`#[proc_macro_derive(xxx)]` 在某个函数前声明;使用时在结构体/枚举体前添加 `#[derive(xxx)]`
  22. - attribute-like`#[proc_macro_attribute]` 在某个函数前声明;使用时在 item 前添加 `#[函数名(属性相关的参数)]`
  23. - function-like`#[proc_macro]` 在某个函数前声明;使用时类似于声明式宏 `macro_rules!` 进行调用:`函数名!(相关参数)`
  24. 1.
  25. 一般来说,编写过程宏需要使用 `proc-macro`(或者 [`proc-macro2`](https://docs.rs/proc-macro2/1.0.26/proc_macro2/))、`syn`、`quote` 三个 crate:
  26. 1.
  27. `proc-macro` Rust compiler 自带的,无需在 `Cargo.toml` `[dependencies]` 中声明,就可以直接在代码中使用,当如引入其内部 item 到作用域就使用 `use proc-macro::item`。注意 `proc-macro` 只能用在与 过程宏 有关的代码中,所以 `main.rs` 里面是不能使用这个 crate 的,也不能利用它对过程宏进行单元测试。
  28. 1.
  29. `proc-macro2` 是对 `proc-macro` API 的封装,它们的 API 是一样的,目的是让 `proc-macro` 可以像正常的 crate 一样在任何地方被使用,包括 `main.rs` 之类的 binary crate 、单元测试代码中。同样,`syn` `quote` 虽然主要为过程宏服务,但它们是通用的 Rust 引用库,并不局限于过程宏。
  30. 1.
  31. [`syn`](cf3afb5e078adf7ff8b60eacd063897f) 解析 token stream 并生成 Rust 源代码的 syntax tree ([AST 抽象语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree))。token 在 syntax analysis 中表示“标记”。AST 核心要求包括以下内容:
  32. - 必须保留变量类型,以及每个声明在源代码中的位置。
  33. - 可执行语句的顺序必须明确表示并定义良好。
  34. - 二元操作符的左右部分必须存储并正确识别。
  35. - 标识符及其分配的值必须存储用于分配语句。
  36. 比如 要获取变量/类型的名称(属于变量/类型声明范畴),这在 Rust 的源代码中使用函数是做不到的,需要通过过程宏,从 AST 中获取到。
  37. 1.
  38. [`quote`](https://docs.rs/quote/1.0.9/quote/) 主要功能是 提供 [`quote!`](5318ffd72865f0c6421589df502a84ee) 声明宏,来把 Rust AST 数据结构转换为源代码 token。
  39. - 由于 Rust 中的过程宏接收 token stream 作为输入,执行任意 Rust 代码来确定如何操作这些 token,并把新生成的 token stream 交还给编译器以编译进调用方 () crate
  40. - 准引用 (quasi-quoting,在 `quote` crate 中就是 `quote!` 宏) 就是用来生成新 token stream 的,以便返回这些 token stream 到编译器,最终实现过程宏的功能。
  41. - 准引用的主要思想是把编写的 Rust 代码视为“数据”。在声明/编写过程宏时,我们可以借助 `quote!` 来将看起来像 Rust 源代码的内容获得 IDE 中的大括号匹配、语法突出显示、缩进以及自动补全的所有功能。但是,这些内容实际会被当作数据来进行处理、传递并最终将其编译到调用宏的 crate 创建的程序中,而不是将其编译为当前(编写过程宏的) crate 中。<br />
  42. ![image.png](assets/image-20210430111553-l8k6vo8.png)
  43. ## 自定义式 / 派生 (custom / derive)
  44. 结构体 枚举体 上添加 `derive` 属性的代码。<br />这里看两个例子:
  45. - `syn` crate 提供的 example:计算某个具体结构体在堆上的内存大小 [https://github.com/dtolnay/syn/tree/master/examples/heapsize](https://github.com/dtolnay/syn/tree/master/examples/heapsize)

git clone https://github.com/dtolnay/syn.git cd syn/examples/heapsize cargo run

  1. `syn/examples` 是一个单独的 workspace(虽然在 _syn_ 目录下,但不在 `syn` workspace 中),所以需要进入 _examples_ 目录或者 _examples/heapsize_ 目录运行,不能直接在 _syn_ 目录里面使用 `cargo run --example heapsize`
  2. - 来自 Rust Book 中的 [例子](https://kaisery.github.io/trpl-zh-cn/ch19-06-macros.html#%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E8%87%AA%E5%AE%9A%E4%B9%89-derive-%E5%AE%8F):笔者有所整理
  3. 1. 首先创建一些路径:

mkdir macro_project # 工作空间主目录 cd macro_project touch Cargo.toml # 创建配置文件 cargo new hello_macro —lib # lib crate:编写 trait cargo new hello_macro_derive —lib # lib crate:编写 derive macro cargo new use_macro —bin # binary crate:使用 derive macro,真实的使用场景 cargo new use_trait —bin # binary crate:使用 trait 来“模拟”做到 derive macro,非真实的使用场景

  1. 目录结构如下:

. ├── Cargo.toml ├── hello_macro │ ├── Cargo.toml │ └── src │ └── lib.rs ├── hello_macro_derive │ ├── Cargo.toml │ └── src │ └── lib.rs ├── use_macro │ ├── Cargo.toml │ └── src │ └── main.rs └── use_trait ├── Cargo.toml └── src └── main.rs

  1. 2. `./` 的位置是进入 _macro_project_;一些**附加**的配置,其他内容默认就好:

./Cargo.toml

清空原内容

[workspace] members = [ “hello_macro”, “hello_macro_derive”, “use_macro”, “use_trait” ]toml

  1. ```
  2. # ./hello_macro_derive/Cargo.toml
  3. # 原默认配置不变
  4. # 以下两个附加配置是编写 derive macro 的标准配置
  5. [lib]
  6. proc-macro = true # 用于声明该 lib 是过程宏
  7. [dependencies]
  8. syn = "1.0"
  9. quote = "1.0"
  1. # use_macro/Cargo.toml
  2. # 原默认配置不变
  3. [dependencies]
  4. hello_macro = {path = "../hello_macro"}
  5. hello_macro_derive= {path = "../hello_macro_derive"toml
  1. # use_trait/Cargo.toml
  2. # 原默认配置不变
  3. [dependencies]
  4. hello_macro = {path="../hello_macro"}
  1. 在相应 .rs 文件里添加代码: ``` // ./hello_macro/src/lib.rs pub trait HelloMacro { fn hello_macro(); }
  1. ```
  2. // ./hello_macro_derive/src/lib.rs
  3. use proc_macro::TokenStream;
  4. use quote::quote;
  5. use syn;
  6. #[proc_macro_derive(HelloMacro)]
  7. pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
  8. // 将 Rust 代码解析为语法树以便进行操作
  9. let ast = syn::parse(input).unwrap(); ‣ast: DeriveInput
  10. // 构建 trait 实现
  11. impl_hello_macro(&ast)
  12. }
  13. fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
  14. let name = &ast.ident; ‣name: &Ident
  15. let gen = quote! {
  16. impl HelloMacro for #name {
  17. fn hello_macro() {
  18. println!("Hello, Macro! My name is {}", stringify!(#name));
  19. }
  20. }
  21. };
  22. gen.into()
  23. }
  1. // ./use_macro/src/main.rs
  2. use hello_macro::HelloMacro;
  3. use hello_macro_derive::HelloMacro;
  4. #[derive(HelloMacro)]
  5. struct Pancakes;
  6. fn main() { Pancakes::hello_macro(); }
  1. // ./use_trait/src/main.rs
  2. use hello_macro::HelloMacro;
  3. struct Pancakes;
  4. impl HelloMacro for Pancakes {
  5. fn hello_macro() {
  6. println!("Hello, Macro! My name is Pancakes!");
  7. }
  8. }
  9. fn main() { Pancakes::hello_macro(); } // 实际需要自己编写手写结构体的名称,不使用宏做不到打印结构体名称

属性式(attribute-like)

可用于任意项的自定义属性。类属性宏与自定义派生宏工作方式一致:创建 proc-macro crate 类型的 crate 并实现希望生成代码的函数。
允许你创建新的属性。它们也更为灵活;derive 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route 的属性用于注解 web 应用程序框架(web application framework)的函数:

  1. #[route(GET, "/")]
  2. fn index() {

#[route] 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:

  1. #[proc_macro_attribute]
  2. pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

这里有两个 TokenStream 类型的参数;第一个用于属性内容本身,也就是 GET, "/" 部分。第二个是属性所标记的项:在本例中,是 fn index() {} 和剩下的函数体。

函数式(function-like)

像函数那样调用、作用于参数传递的 token。
类似于自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。
类似于 macro_rules!,它们比函数更灵活;例如,可以接受未知数量的参数。然而 macro_rules! 宏只能使用之前 “使用 macro_rules! 的声明宏用于通用元编程” 介绍的类匹配的语法定义。类函数宏获取 TokenStream 参数,其定义使用 Rust 代码操纵 TokenStream,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 sql! 宏:

  1. let sql = sql!(SELECT * FROM posts WHERE id=1);

这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules! 可以做到的更为复杂的处理。sql! 宏应该被定义为如此:

  1. #[proc_macro]
  2. pub fn sql(input: TokenStream) -> TokenStream {

宏与函数(macro vs function)

macro 通过展开来生成更多代码的方式,即元编程(metaprogramming)
【优点】扮演了函数的角色,但比函数提供更多的功能:
参数形式上:宏可以接收可变(或未知)数量的参数;函数只能接收确定数量(和类型)的参数
从时间上:宏先于函数,宏在编译之前(具体来说是编译器解释代码之前);函数在运行时被调用
对于 trait 的作用:宏通过展开来实现(产生)给定类型的 trait(trait 必须在编译期实现);函数在 trait 确定之后才能运行
【缺点】
间接性:定义宏比定义函数更复杂、更难读懂和维护
调用时:宏必须在调用之前定义好,且被引入作用域;函数可以在任何地方定义和调用