在 Rust 中宏分为两大类:声明式宏 macro_rules!
和过程宏(派生宏、类属性宏、类函数宏)。println!
、vec!
、assert_eq!
都是相当常用的宏。宏是通过一种代码来生成另一种代码,且可以拥有可变数量的参数,这个展开过程是发生在编译器对代码进行解释之前,并没有运行期的性能损耗。
声明式宏允许我们写出类似 match
的代码,将表达式的结果与多个模式进行匹配,一旦匹配了某个模式,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。例如,一个简单的vec!实现:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
#[macro_export]
注释将宏进行了导出,macro_rules!
进行了宏定义,宏的名称是 vec,而感叹号是在调用的时候才加上。
( $( $x:expr ),* )
的含义:
1、圆括号 ()
将整个宏模式包裹其中。紧随其后的是$()
,包含的是模式 $x:expr
,该模式中的 expr
表示会匹配任何 Rust 表达式,并给予该模式一个名称 $x,例如,$x
模式可以跟整数 1
进行匹配,也可以跟字符串 "hello" 进行匹配。
2、$()
之后的逗号说明在 $()
所匹配的代码的后面会有一个可选的逗号分隔符,紧随逗号之后的 *
说明 *
之前的模式会被匹配零次或任意多次,例如使用 vec![1, 2, 3]
来调用该宏时,$x
模式将被匹配三次
3、$()
中的 temp_vec.push()
将根据模式匹配的次数生成对应的代码,当执行let v = vec![1, 2, 3]
时,下面这段生成的代码将替代传入的源代码,替代 vec![1, 2, 3]
,并将返回的值temp_vec
赋予给变量v
let v = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。输出的代码并不会替换之前的代码,这与声明宏不同。而且创建过程宏时,它的定义必须要放入一个独立的包中。
$ cargo new hello_macro
$ cd hello_macro/
$ touch src/lib.rs
$ cargo new hello_macro_derive --lib
//hello_macro 项目的目录结构
hello_macro
├── Cargo.toml
├── src
│ ├── main.rs
│ └── lib.rs
└── hello_macro_derive
├── Cargo.toml
├── src
└── lib.rs
$ cat hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
# 也可以使用下面的相对路径
# hello_macro_derive = { path = "./hello_macro_derive" }
$ cat hello_macro/src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Sunfei;
#[derive(HelloMacro)]
struct Sunface;
fn main() {
Sunfei::hello_macro();
Sunface::hello_macro();
}
$ cat hello_macro/src/lib.rs //定义过程宏所需的 HelloMacro特征和其关联函数:
pub trait HelloMacro {
fn hello_macro();
}
$ cat hello_macro/hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
$ cat hello_macro/hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
use syn::DeriveInput;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 基于 input 构建 AST 语法树
let ast:DeriveInput = syn::parse(input).unwrap();
// 构建特征实现代码
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident; //将结构体的名称赋予给name,
let gen = quote! { //使用 quote! 定义我们想要返回的Rust代码。
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name)); //#name 是字面值形式
}
}
};
gen.into()
}
$ cargo run
Running `target/debug/hello_macro`
Hello, Macro! My name is Sunfei!
Hello, Macro! My name is Sunface!
为了在项目的 src/main.rs
中引用 hello_macro_derive
包的内容,第一种是将 hello_macro_derive
发布到 crates.io
或 GitHub
中,另一种就是使用相对路径引入的本地化方式,即在hello_macro/Cargo.toml文件添加依赖,
此时hello_macro
项目就可以成功的引用到 hello_macro_derive
本地包了
在 hello_macro_derive/Cargo.toml
文件中,syn
和 quote
依赖包都是定义过程宏所必需的,同时,还需要在 [lib]
中将过程宏的开关开启
在 hello_macro_derive/src/lib.rs中,过程宏的格式基本是一样的,
只在过程宏的实现impl_hello_macro(&ast)
有所区别,其中proc_macro
包是 Rust 自带的,它包含了相关的编译器 API
,可以用于读取和操作 Rust 源代码。syn
将字符串形式的 Rust 代码解析为一个 AST 树的数据结构。syn::parse
调用会返回一个 DeriveInput
结构体来代表解析后的 Rust 代码:
DeriveInput {
// --snip--
vis: Visibility,
ident: Ident {
ident: "Sunfei",
span: #0 bytes(95..103)
},
generics: Generics,
// Data是一个枚举,分别是DataStruct,DataEnum,DataUnion,这里以 DataStruct 为例
data: Data(
DataStruct {
struct_token: Struct,
fields: Fields,
semi_token: Some(
Semi
)
}
)
}
derive过程宏只能用在struct/enum/union上,多数用在结构体上
在运行之前,可以先用 expand 展开宏,观察是否有错误或符合预期,如果没有需要先安装
$ cargo install cargo-expand
$ cargo expand --bin hello_macro