[易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]
实用知识
宏Macro
我们今天来讲讲Rust中强大的宏Macro。
Rust的宏macro是实现元编程的强大工具。
宏主要作用为:
1.减少重复代码。
2.编写DSL(Domain-specific languages。
3.可变参数接口定义。
在Rust主要分两种宏:
声明式宏declarative macros (一般用macro_rules!定义)
过程式宏 procedural macros,像一个过程函数,更易懂。它主要功能是把属性代码作为输入,作用于现有代码,并生成新的代码,从而实现特定功能。过程式宏又分为三种:
2.1自定义派生宏custom derive,主要用于给结构体或枚举增加派生属性#[derive]。
2.2自定义属性(属性风格宏Attribute-like),主要用于给任意元素增加属性。
2.3 函数宏(函数风格宏Function-like),主要用于AST抽象语法树的操作。
这里简单说明 一下,如果要理解宏,就要简单了解下编译原理,一般来说现代化的编译器的编译过程如下 :
编译过程
分词(词条流)→解析(抽象语法树)→简化(高级中间语言)→简化(中级中间语言)→转译(LLVM中间语言)→优化(机器码)
简单来说,编译器就像一个翻译者,它把程序代码,一步步翻译成机器码,让机器能理解并执行人类的代码。
先来看看声明式宏,macro_rules! 是使用递归和模式匹配、字符串替换的函数式风格定义宏。
我们来看看简单的宏定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1// 宏名字为:`say_hello`.
2macro_rules! say_hello {
3 // `()` 这里括号里没有任何表达式,表示这个宏不用接受任何参数.
4 () => {
5 // 宏将把上面的括号()内的表达式展开到这个大括号{}中的代码。
6 println!("Hello!");
7 };
8}
9
10fn main() {
11 // 调用这个宏,将直接把这个宏展开为代码: `println!("Hello");`
12 say_hello!()
13}
14
我们再来看看稍微有点复杂的宏定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 1macro_rules! create_function {
2 // 这个宏定义有两个参数:ident为标识符,主要用来标识变量或函数,表明宏展开为变量或函数;
3 //而$func_name则为宏代码中代码展开的参数名,这里为函数名.
4 ($func_name:ident) => {
5 fn $func_name() {
6 //这里的宏 `stringify!`直接把函数名字转换成字符串。
7 println!("You called {:?}()", stringify!($func_name));
8 }
9 };
10}
11
12macro_rules! print_result {
13 //这个宏把一个表达式,并把表达式的字符串和表达式结果一起打印出来。
14 //而标识符expr,主要用来标识表达式。
15 ($expression:expr) => {
16 // `stringify!` 宏将把表达式转换成字符串形式
17 println!("{:?} = {:?}", stringify!($expression), $expression);
18 };
19}
20
21fn main() {
22 // 用上面的宏create_function!分别创建foo和bar函数
23 create_function!(foo);
24 create_function!(bar);
25
26 foo();//调用foo函数,即调用代码:println!("You called {:?}()", stringify!(foo));
27 bar();/调用foo函数,即调用代码:println!("You called {:?}()", stringify!(bar));
28
29//用上面宏print_result!,将把代码展开为:
30//println!("{:?} = {:?}", stringify!(1u32 + 1), 1u32 + 1);
31 print_result!(1u32 + 1);
32
33 // 同样,将大括号的代码当成表达式参数传给上面的宏print_result!
34 print_result!({
35 let x = 1u32;
36
37 x * x + 2 * x - 1
38 });
39}
40
41
运行上面的代码,打印结果为:
1
2
3
4
5 1You called "foo"()
2You called "bar"()
3"1u32 + 1" = 2
4"{ let x = 1u32; x * x + 2 * x - 1 }" = 2
5
我们从上面的代码来一一分析,一般来说宏定义用:macro_rules! 开头,表明这是个宏。
基本形式为:() => { };
其中小括号为宏定义的参数:主要用来定义宏的参数,其中参数有个标识符,主要用来表明宏定义的表达式展开的参数类型。
有以下标识符:
- item,语言项,比如模块、声明、函数定义、类型定义、结构体定义、impl实现等。
block,代码块,由花括号限定的代码;
stmt,语句,一般是指以分号结尾的代码;
expr,表达式,会生成具体的值;
pat,模式;
ty,类型;
ident,标识符;
path,路径,比如foo、std::iter等;
meta,元信息,包含在#[…]或者#[…]属性内的信息;
tt,TokenTree的缩写,词条树;
vis,可见性,比如pub;
lifetime,生命周期参数。
代码重载
宏可以像模式匹配一样,可以根据参数不同,匹配不同的宏定义代码,如下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 1/ `test!` 宏主要用来比较 `$left` 和 `$right`
2// 它会根据你调用时的参数自动匹配不一样的代码:
3macro_rules! test {
4 // 参数之间不一定要用逗号隔开
5 // 任何形式的模板代码都可以
6 ($left:expr; and $right:expr) => {
7 println!("{:?} and {:?} is {:?}",
8 stringify!($left),
9 stringify!($right),
10 $left && $right)
11 };
12 // 每个left参数必须以分号结尾
13 ($left:expr; or $right:expr) => {
14 println!("{:?} or {:?} is {:?}",
15 stringify!($left),
16 stringify!($right),
17 $left || $right)
18 };
19}
20
21fn main() {
22 test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
23 test!(true; or false);
24}
25
26
运行上面代码,结果为:
1
2
3 1"1i32 + 1 == 2i32" and "2i32 * 2 == 4i32" is true
2"true" or "false" is true
3
重复调用
宏定义可以用加号+来表示,可以传入一个或多个参数;同样,也可以星号*来表示可以传入零个或多个参数。
我们看看简单例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1// `min!` 宏主要用来求多个表达式结果中的最小值.
2macro_rules! find_min {
3 //一个参数的情况:
4 ($x:expr) => ($x);
5 // 参数`$x` 和至少一个参数 `$y,`的情况
6 ($x:expr, $($y:expr),+) => (
7 // 调用标准库中的最小值求值函数 ,并且让`find_min!`作用于参数 `$y`和更多其它参数
8 std::cmp::min($x, find_min!($($y),+))
9 )
10}
11
12fn main() {
13 println!("{}", find_min!(1u32));
14 println!("{}", find_min!(1u32 + 2, 2u32));
15 println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
16}
17
运行结果:
1
2
3
4
5 11
22
34
4
5
当然,宏最大的用法,就是DSL(Domain Specific Languages),即领域特定语言。我们来看看简单例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1macro_rules! calculate {
2 (eval $e:expr) => {{
3 {
4 let val: usize = $e; // 强制类型转换成integer
5 println!("{} = {}", stringify!{$e}, val);//打印结果
6 }
7 }};
8}
9
10fn main() {
11 calculate! {
12 eval 1 + 2 // 还好 `eval`不是Rust的关键词!
13 }
14
15 calculate! {
16 eval (1 + 2) * (4 / 4)
17 }
18}
19
20
结果为:
1
2
3 11 + 2 = 3
2(1 + 2) * (4 / 4) = 3
3
可变参数接口
很多时候,我们的接口可能要适应一个或多个参数的情况,也就是可变参数接口。那这样的宏又如何实现呢?我们来扩展下上面的宏,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1macro_rules! calculate {
2 // The pattern for a single `eval`
3 (eval $e:expr) => {{
4 {
5 let val: usize = $e; // Force types to be integers
6 println!("{} = {}", stringify!{$e}, val);
7 }
8 }};
9
10 // Decompose multiple `eval`s recursively
11 (eval $e:expr, $(eval $es:expr),+) => {{
12 calculate! { eval $e }
13 calculate! { $(eval $es),+ }
14 }};
15}
16
17fn main() {
18 calculate! { // Look ma! Variadic `calculate!`!
19 eval 1 + 2,
20 eval 3 + 4,
21 eval (2 * 3) + 1
22 }
23}
24
25
运行结果为:
1
2
3
4 11 + 2 = 3
23 + 4 = 7
3(2 * 3) + 1 = 7
4
导入导出
#[macro_export]表示下面的宏定义对其他包也是可见的。#[macro_use]可以导入宏。
在宏定义中使用$crate,可以在被导出时,让编译器根据上下文推断包名,避免依赖问题。
我们再来看看过程宏,过程宏有三个:1.自定义派生宏custom derive 2.自定义属性 3.函数宏
1.自定义派生宏custom derive
所谓自定义derive属性,即可自动为结构体或枚举类型进行语法扩展。
我们来看看例子。
先用命令生成目录:test_derive_macro
1
2 1cargo new test_derive_macro
2
新建一个lib.rs文件和目录tests,并创建一个test.rs文件,目录结构如下:
1
2
3
4
5
6 1|-Cargo.toml
2|-src
3 |- lib.rs
4|-tests
5 |- test.rs
6
我们先更新Cargo.toml ,完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 1[package]
2name = "test_derive_macro"
3version = "0.1.0"
4authors = ["gyc567 <gyc567@126.com>"]
5edition = "2018"
6
7# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8#设置lib的类型,这个一定要加上
9[lib]
10proc_macro = true
11[dependencies]
12
13
test.rs的代码:
1
2
3
4
5
6
7
8
9
10
11 1#[macro_use]
2extern crate test_derive_macro;
3
4#[derive(A)]
5struct A;
6#[test]
7fn test_derive_a() {
8 assert_eq!("hello from impl A".to_string(), A.a());
9}
10
11
lib.rs代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1extern crate proc_macro;
2use self::proc_macro::TokenStream;
3
4// 自定义派生属性
5#[proc_macro_derive(A)]
6pub fn derive(input: TokenStream) -> TokenStream {
7 let _input = input.to_string();
8 assert!(_input.contains("struct A;"));
9 r#"
10 impl A {
11 fn a(&self) -> String{
12 format!("hello from impl A")
13 }
14 }
15 "#
16 .parse()
17 .unwrap()
18}
19
20
我们在当前工程目录下运行:cargo test
显示测试通过 。
我们再来看看独立crate工程的例子(这个官方案例代码更全面):
我们先用命令创建一个工程:hello_macro:
1
2 1$ cargo new hello_macro --lib
2
我在当前工程下的src/lib.rs,定义一个公共特征,写入如下代码:
1
2
3
4 1pub trait HelloMacro {
2 fn hello_macro();
3}
4
然后我们在当前工程目录创建另一个工程:hello_macro_derive,用如下命令:
1
2 1$ cargo new hello_macro_derive --lib
2
修改在这个工程目录下文件:hello_macro_derive/Cargo.toml,主要增加 两个库syn和quote:
1
2
3
4
5
6
7 1[lib]
2proc-macro = true
3
4[dependencies]
5syn = "0.14.4"
6quote = "0.6.3"
7
然后在文件:hello_macro_derive/src/lib.rs,写入如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 1extern crate proc_macro;
2
3use crate::proc_macro::TokenStream;
4use quote::quote;
5use syn;
6
7#[proc_macro_derive(HelloMacro)]
8pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
9 // Construct a representation of Rust code as a syntax tree
10 // that we can manipulate
11 //从Rust代码构造出我们可以操作的语法树ast
12 //文档:https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html
13 let ast: syn::DeriveInput = syn::parse(input).unwrap();
14
15 // 实现特征方法 Build the trait implementation
16 impl_hello_macro(&ast)
17}
18
19// 实现特征方法
20fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
21 let name = &ast.ident;
22 let gen = quote! {//主要用来替换相关字符串,生成特定代码,
23 //文档:https://docs.rs/quote/1.0.2/quote/
24 impl HelloMacro for #name {
25 fn hello_macro() {
26 println!("Hello, Macro! My name is {}", stringify!(#name));
27 }
28 }
29 };
30 gen.into()
31}
32
33
然后在目录hello_macro,运行命令:
1
2 1cargo build
2
现在退出当前工程,当前工程的上一级目录下创建一个新工程,用命令:
1
2 1cargo new pancakes
2
更新工程文件:pancakes/Cargo.toml文件下的依赖:
1
2
3
4 1[dependencies]
2hello_macro = { path = "../hello_macro" }
3hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
4
更新main文件pancakes/src/main.rs,完整代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12 1use hello_macro::HelloMacro;
2use hello_macro_derive::HelloMacro;
3// struct Pancakes;
4
5#[derive(HelloMacro)]
6struct Pancakes;
7
8fn main() {
9 Pancakes::hello_macro();
10}
11
12
运行以上代码,正常情况下,会打印正确结果:
Hello, Macro! My name is Pancakes
我们再来看看后两种过程宏:
2.自定义属性(属性风格宏Attribute-like)
我们来看看代码例子,首先生成目录:
1
2 1cargo new test_macro_attribute
2
然后到工程目录test_macro_attribute下直接生成lib crate目录,用命令:
1
2 1cargo new my_macro --lib
2
生成的目录结构为:
1
2
3
4
5
6
7
8
9 1test_macro_attribute
2├── Cargo.toml
3├── my_macro
4│ ├── Cargo.toml
5│ ├── src
6│ │ └── lib.rs
7└── src
8 └── main.rs
9
然后我们在当着工程目录test_macro_attribute下的Cargo.toml文件,新增一行,如下:
1
2
3
4
5
6
7
8
9
10
11
12 1[package]
2name = "test_macro_attribute"
3version = "0.1.0"
4authors = ["gyc567 <gyc567@126.com>"]
5edition = "2018"
6
7# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8
9[dependencies]
10#新增一行,自定义宏lib依赖,这里就是:my_macro
11my_macro = { path = "my_macro" }
12
src/main.rs代码如下:
1
2
3
4
5
6
7
8
9
10
11
12 1#[macro_use]
2extern crate my_macro;
3
4#[log_entry_and_exit(hello, "world")]
5fn this_will_be_destroyed() -> i32 {
6 42
7}
8
9fn main() {
10 dummy()
11}
12
my_macro/Cargo.toml文件,新增一行宏的属性定义,完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 1[package]
2name = "my_macro"
3version = "0.1.0"
4authors = ["gyc567 <gyc567@126.com>"]
5edition = "2018"
6
7# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8[lib]
9#新增一行,定义为宏的lib
10proc-macro = true
11[dependencies]
12
13
my_macro/src/lib.rs代码如下 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1extern crate proc_macro;
2
3use proc_macro::*;
4
5#[proc_macro_attribute]
6pub fn log_entry_and_exit(args: TokenStream, input: TokenStream) -> TokenStream {
7 let x = format!(
8 r#"
9 fn dummy() {{
10 println!("entering");
11 println!("args tokens: {{}}", {args});
12 println!("input tokens: {{}}", {input});
13 println!("exiting");
14 }}
15 "#,
16 args = args.into_iter().count(),
17 input = input.into_iter().count(),
18 );
19
20 x.parse().expect("Generated invalid tokens")
21}
22
23
保存好代码,直接用cargo run运行,结果为:
1
2
3
4
5 1entering
2args tokens: 3
3input tokens: 7
4exiting
5
我们再来看看函数宏,在my_macro/src/lib.rs新增如下代码:
1
2
3
4
5
6
7
8 1#[proc_macro]
2pub fn hw(input: TokenStream) -> TokenStream {
3 // r#"println!("Hello, World!----in pm");"#.parse().unwrap()
4 println!("in pm ----{:?}", input);
5
6 TokenStream::new()
7}
8
回到工程目录下的main.rs,新增一行调用代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 1#[macro_use]
2extern crate my_macro;
3
4#[log_entry_and_exit(hello, "world")]
5fn this_will_be_destroyed() -> i32 {
6 42
7}
8my_macro::hw!();//新增一行调用代码,注意,一定要放在这里才不报错
9fn main() {
10 dummy()
11}
12
13
cargo run的最终运行结果为:
PS E:\code\rustProject\test_macro_attribute> cargo run
Compiling test_macro_attribute v0.1.0 (E:\code\rustProject\test_macro_attribute)
in pm —-TokenStream []
Finished dev [unoptimized + debuginfo] target(s) in 0.75s
Running target\debug\test_macro_attribute.exe
entering
args tokens: 3
input tokens: 7
exiting
我们发现,新增的代码:my_macro::hw!();
在编译的时候打印相关信息,说明它是在操作在编译期。
新增代码一定要写在main函数上面,否则报错:error[E0658]: procedural macros cannot be expanded to statements。见:https://stackoverflow.com/questions/54174361/cannot-call-a-function-like-procedural-macro-cannot-be-expanded-to-statements
这可能主要是因为现在的宏还在进化和完善中,还没有最终稳定下来。
所以,在Rust,宏是很强大的工具,但要用好,要好好深入掌握相关知识,才能写更好的代码。
本篇宏的专题,先到这里吧,以后有更好的案例,我会慢慢加入进来。
以上,希望对你有用。
1
2 1如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust
2
参考文章:
https://doc.rust-lang.org/stable/rust-by-example/macros.html
https://danielkeep.github.io/tlborm/book/index.html
https://xr1s.me/2018/12/08/introduction-to-rust-proc-macro/
https://tinkering.xyz/introduction-to-proc-macros/
https://github.com/dtolnay/proc-macro-workshop\#attribute-macro-sorted
https://blog.x5ff.xyz/blog/easy-programming-with-rust-macros/
https://stackoverflow.com/questions/52585719/how-do-i-create-a-proc-macro-attribute?noredirect=1