参考:https://doc.rust-lang.org/book/ch19-06-macros.html、中文版
macro 含义
macro 指 Rust 中一系列的功能,包括以下两类:
- 用
macro_rules!
声明(declarative)的宏 - 自定义式、属性式、函数式 三种过程(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 } }; }
对 `vec!` 定义过程的的解读:
1.
`( $( $x:expr ),* )` 被称为 “模式”(one arm with the pattern):
1. 首先,一对括号包含了整个模式
1. 接下来是美元符号( `$` ),后跟一对括号,捕获了符合括号内模式的值以用于替换后的代码
1. `$()`内则是`$x:expr` ,其匹配 Rust 的任意表达式,并将该表达式记作 `$x`
1. `,` 表示多于一个元素时必须以逗号分隔,最后一个元素后的逗号可以省略。除了逗号分隔,如果需要也可用 `;` 做分隔符。
1. 紧随逗号之后的 `*` 说明该模式匹配零个或更多个 `*` 之前的任何模式
1. 当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 进行了三次匹配
1. 假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
1.
`=> { ... }` 被称为 与模式相关联的代码块:
1.
对于每个在 `=>` 前面的模式中的 `$()` 的部分,都会在 `=>` 后面的 `$()*` 内生成 `temp_vec.push()` 代码,生成的个数取决于该模式被匹配的次数
1.
`$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.
`$()*` 之外的部分就是一般的语句、表达式;可以看到 `vec` 最后返回 temp_vec 这个变量的值
1.
至此我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码
> 宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。对于全部的宏模式语法,请查阅[参考](275da7032c73008cd2d1bf1697c75f97)。
`macro_rules!` 中有一些奇怪的地方。在将来,会有第二种采用 `macro` 关键字的声明宏,其工作方式类似但修复了这些极端情况。在此之后,`macro_rules!` 实际上就过时(deprecated)了。在此基础之上,同时鉴于大多数 Rust 程序员 **使用** 宏而非 **编写** 宏的事实,此处不再深入探讨 `macro_rules!`。请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”](30bd57640f0307514314a1688d914e7d) 来更多地了解如何写宏。
# 过程宏(procedural)
过程宏:
1.
更像函数(一种过程类型)
1.
接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出
1.
不像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码
1.
有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似
1.
当创建过程宏时,其定义必须位于一种特殊类型的属于它们自己的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制
1.
使用过程宏需采用类似于 `#[proc_macro*]` 的代码形式,其中 `proc_macro*` 是过程宏的声明。三种过程宏的声明和使用方式:
- derive macro:`#[proc_macro_derive(xxx)]` 在某个函数前声明;使用时在结构体/枚举体前添加 `#[derive(xxx)]`
- attribute-like:`#[proc_macro_attribute]` 在某个函数前声明;使用时在 item 前添加 `#[函数名(属性相关的参数)]`
- function-like:`#[proc_macro]` 在某个函数前声明;使用时类似于声明式宏 `macro_rules!` 进行调用:`函数名!(相关参数)`
1.
一般来说,编写过程宏需要使用 `proc-macro`(或者 [`proc-macro2`](https://docs.rs/proc-macro2/1.0.26/proc_macro2/))、`syn`、`quote` 三个 crate:
1.
`proc-macro` 是 Rust compiler 自带的,无需在 `Cargo.toml` 的 `[dependencies]` 中声明,就可以直接在代码中使用,当如引入其内部 item 到作用域就使用 `use proc-macro::item`。注意 `proc-macro` 只能用在与 过程宏 有关的代码中,所以 `main.rs` 里面是不能使用这个 crate 的,也不能利用它对过程宏进行单元测试。
1.
`proc-macro2` 是对 `proc-macro` 的 API 的封装,它们的 API 是一样的,目的是让 `proc-macro` 可以像正常的 crate 一样在任何地方被使用,包括 `main.rs` 之类的 binary crate 、单元测试代码中。同样,`syn` 和 `quote` 虽然主要为过程宏服务,但它们是通用的 Rust 引用库,并不局限于过程宏。
1.
[`syn`](cf3afb5e078adf7ff8b60eacd063897f) 解析 token stream 并生成 Rust 源代码的 syntax tree ([AST 抽象语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree))。token 在 syntax analysis 中表示“标记”。AST 核心要求包括以下内容:
- 必须保留变量类型,以及每个声明在源代码中的位置。
- 可执行语句的顺序必须明确表示并定义良好。
- 二元操作符的左右部分必须存储并正确识别。
- 标识符及其分配的值必须存储用于分配语句。
比如 要获取变量/类型的名称(属于变量/类型声明范畴),这在 Rust 的源代码中使用函数是做不到的,需要通过过程宏,从 AST 中获取到。
1.
[`quote`](https://docs.rs/quote/1.0.9/quote/) 主要功能是 提供 [`quote!`](5318ffd72865f0c6421589df502a84ee) 声明宏,来把 Rust AST 数据结构转换为源代码 token。
- 由于 Rust 中的过程宏接收 token stream 作为输入,执行任意 Rust 代码来确定如何操作这些 token,并把新生成的 token stream 交还给编译器以编译进调用方 () 的 crate。
- 准引用 (quasi-quoting,在 `quote` crate 中就是 `quote!` 宏) 就是用来生成新 token stream 的,以便返回这些 token stream 到编译器,最终实现过程宏的功能。
- 准引用的主要思想是把编写的 Rust 代码视为“数据”。在声明/编写过程宏时,我们可以借助 `quote!` 来将看起来像 Rust 源代码的内容获得 IDE 中的大括号匹配、语法突出显示、缩进以及自动补全的所有功能。但是,这些内容实际会被当作数据来进行处理、传递并最终将其编译到调用宏的 crate 创建的程序中,而不是将其编译为当前(编写过程宏的) crate 中。<br />

## 自定义式 / 派生 (custom / derive)
在 结构体 和 枚举体 上添加 `derive` 属性的代码。<br />这里看两个例子:
- `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
`syn/examples` 是一个单独的 workspace(虽然在 _syn_ 目录下,但不在 `syn` workspace 中),所以需要进入 _examples_ 目录或者 _examples/heapsize_ 目录运行,不能直接在 _syn_ 目录里面使用 `cargo run --example heapsize`。
- 来自 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):笔者有所整理
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,非真实的使用场景
目录结构如下:
. ├── 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
2. `./` 的位置是进入 _macro_project_;一些**附加**的配置,其他内容默认就好:
./Cargo.toml
清空原内容
[workspace] members = [ “hello_macro”, “hello_macro_derive”, “use_macro”, “use_trait” ]toml
```
# ./hello_macro_derive/Cargo.toml
# 原默认配置不变
# 以下两个附加配置是编写 derive macro 的标准配置
[lib]
proc-macro = true # 用于声明该 lib 是过程宏
[dependencies]
syn = "1.0"
quote = "1.0"
# use_macro/Cargo.toml
# 原默认配置不变
[dependencies]
hello_macro = {path = "../hello_macro"}
hello_macro_derive= {path = "../hello_macro_derive"toml
# use_trait/Cargo.toml
# 原默认配置不变
[dependencies]
hello_macro = {path="../hello_macro"}
- 在相应
.rs
文件里添加代码: ``` // ./hello_macro/src/lib.rs pub trait HelloMacro { fn hello_macro(); }
```
// ./hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 将 Rust 代码解析为语法树以便进行操作
let ast = syn::parse(input).unwrap(); ‣ast: DeriveInput
// 构建 trait 实现
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; ‣name: &Ident
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
// ./use_macro/src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() { Pancakes::hello_macro(); }
// ./use_trait/src/main.rs
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() { Pancakes::hello_macro(); } // 实际需要自己编写手写结构体的名称,不使用宏做不到打印结构体名称
属性式(attribute-like)
可用于任意项的自定义属性。类属性宏与自定义派生宏工作方式一致:创建 proc-macro
crate 类型的 crate 并实现希望生成代码的函数。
允许你创建新的属性。它们也更为灵活;derive
只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route
的属性用于注解 web 应用程序框架(web application framework)的函数:
#[route(GET, "/")]
fn index() {
#[route]
属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:
#[proc_macro_attribute]
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!
宏:
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules!
可以做到的更为复杂的处理。sql!
宏应该被定义为如此:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
宏与函数(macro vs function)
macro 通过展开来生成更多代码的方式,即元编程(metaprogramming)
【优点】扮演了函数的角色,但比函数提供更多的功能:
参数形式上:宏可以接收可变(或未知)数量的参数;函数只能接收确定数量(和类型)的参数
从时间上:宏先于函数,宏在编译之前(具体来说是编译器解释代码之前);函数在运行时被调用
对于 trait 的作用:宏通过展开来实现(产生)给定类型的 trait(trait 必须在编译期实现);函数在 trait 确定之后才能运行
【缺点】
间接性:定义宏比定义函数更复杂、更难读懂和维护
调用时:宏必须在调用之前定义好,且被引入作用域;函数可以在任何地方定义和调用