Rust Macros
Table of Contents
1. 宏简介
宏可以生成代码,也就是说在调用宏的地方,编译器会先将宏进行展开,然后再编译展开后的代码。 很多语言都支持宏,如 C/Lisp/Rust 等等,本文介绍 Rust 中的宏。
Rust 中使用 macro_rules! (它本身也是宏)可以定义一个宏。下面例子中,使用 macro_rules! 定义了一个名为 say_hello 的宏:
// This is a simple macro named `say_hello`.
macro_rules! say_hello {
// `()` indicates that the macro takes no argument.
() => {
// The macro will expand into the contents of this block.
println!("Hello!");
};
}
fn main() {
// This call will expand into `println!("Hello!");`
say_hello!()
}
Rust 中宏看起来像函数,调用宏时在宏名后面加个 ! 即可,如常见的 println! 就是宏调用, 上面例子中出现的 say_hello! 和 macro_rules! 都是宏调用。
1.1. 查看宏展开后的代码
使用 rustc -Z unstable-options --pretty=expanded 可以查看宏展开后的代码。下面是个例子:
$ cat test.rs
fn main() {
println!("Hello world"); // println 是宏
}
$ rustc -Z unstable-options --pretty=expanded test.rs
#![feature(prelude_import)]
#![no_std]
#[prelude_import]
use ::std::prelude::v1::*;
#[macro_use]
extern crate std;
fn main() {
{
::std::io::_print(::core::fmt::Arguments::new_v1(&["Hello world\n"],
&match () {
() => [],
}));
};
}
1.2. 宏和函数的区别
宏和函数的区别:
- 宏是一种生成其他代码的方式,即所谓的元编程(Metaprogramming)。
- Rust 中函数不支持变长参数。相比之下,宏能够接受不同数量的参数。
- 宏比函数复杂,通过宏定义要比函数定义更难阅读、理解以及维护。
- 宏和函数的最后一个重要的区别是:在一个文件里调用宏之前必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
2. 宏语法
下面我们看个简单例子来说明一下 Rust 中的宏展开:
macro_rules! foo {
($x:ident) => { $x * $x } // $x 是宏参数;indent 是宏参数的类型指示符,这里表示 $x 必须是“标识符”
// ^ ^
// | |
// 宏参数 展开后的目标代码
}
fn main() {
let a = 5;
let b = foo!(a); // foo!(a) 会被展开为 a * a
println!("b = {}", b)
}
宏中的参数都是以 $ 开头, 可以对宏变量类型使用指示符(designator)加以限定,就跟普通的变量绑定一样用“冒号”将变量和类型分开,宏支持以下几种指示符:
- item: an Item
- block: a BlockExpression
- stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
- pat: a Pattern
- expr: an Expression
- ty: a Type
- ident: an IDENTIFIER_OR_KEYWORD
- path: a TypePath style path
- tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
- meta: an Attr, the contents of an attribute
- lifetime: a LIFETIME_TOKEN
- vis: a possibly empty Visibility qualifier
- literal: matches LiteralExpression
相对于 C 中纯文本替换的宏,Rust 中宏可以对变量类型加以限定,这更加安全可靠。比如,前面例子中,我们不能把 $x 的类型指示符修改为 ty (表示“类型”),因为两个 ty 相乘没有意义。
macro_rules! foo {
($x:ty) => { $x * $x } // 这是错误的,两个 ty 相乘没有意义
}
此外,宏参数也可以使用 {} 包围,展开体也可以使用 () 包围,即前面的 foo 也可以写为:
macro_rules! foo {
{$x:ident} => ( $x * $x ) // 参数和展开体可以使用 {} 或 () 包围,是一对就行
}
2.1. 宏展开实例:创建函数
下面看一个宏的例子,这个宏会展开为一个函数:
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("function {:?} is called", stringify!($func_name))
}
}
}
fn main() {
create_function!(foo); // 展开为一个名为 foo 的函数
foo();
create_function!(bar); // 展开为一个名为 bar 的函数
bar();
}
2.2. 宏展开实例:多个参数 Pattern
下面看一个宏的例子,参数可以是不同的 Pattern:
macro_rules! add {
(one to $input:expr) => {$input + 1};
(two to $input:expr) => {$input + 2};
}
fn main() {
println!("Add one: {}", add!(one to 10)); // add!(one to 10) 会展开为 10 + 1
println!("Add two: {}", add!(two to 10)); // add!(two to 10) 会展开为 10 + 2
}
2.3. 宏重复(可实现变长参数)
Rust 宏中支持“重复”,被重复的项需要放在 $(...) 中,后面接 * (表示任意重复)或者 + (表示至少一个)或者 ? (表示零个或者一个)。在 */+/? 之前可以有个分隔符,分隔符常常是 , 或者 ; 。注:重复的最后一个项目没有分隔符。
比如, $( $i:ident ),* 表示任意个使用逗号分隔的标识符。
macro_rules! vector { // 版本一
($($x:expr),*) => {
// ^ ^
// | |
// 表示重复 重复标记 * 前面的这个逗号为分隔符
{
let mut temp_vec = Vec::new();
$(temp_vec.push($x);)*
// ^\ / ^
// | -----重复项----- |
// 表示重复 重复标记 * 前面没有分隔符(分号在重复项中,并不是分隔符)
temp_vec
}
};
}
fn main() {
let a = vector![1, 2, 3];
println!("{:?}", a);
}
上面代码中 vector![1, 2, 3] 会被展开为:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
宏 vector 的定义中, $(temp_vec.push($x);)* 可以改写为 $(temp_vec.push($x));* 吗?答案是可以的,只是麻烦一些。由于重复的最后一个项目没有分隔符,所以 $(temp_vec.push($x));* 在展开时会生成:
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3) // 最后一项没分隔符,这里还少个分号
这样,我们还需要额外增加一个分号,如:
macro_rules! vector { // 版本二
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(temp_vec.push($x));*
// ^\ / ^
// | -----重复项---- |
// 表示重复 重复标记前面有个分号分隔符
; // 重复的最后一个项目没有分隔符,这里需要额外增加一个分号
temp_vec
}
};
}
2.3.1. 宏重复的实例
下面是个宏重复的实例:
// from https://doc.rust-lang.org/rust-by-example/macros/repeat.html
//
// `find_min!` will calculate the minimum of any number of arguments.
macro_rules! find_min {
// Base case:
($x:expr) => ($x);
// `$x` followed by at least one `$y,`
($x:expr, $($y:expr),+) => ( // $($y:expr),+ 是宏重复
// Call `find_min!` on the tail `$y`
std::cmp::min($x, find_min!($($y),+)) // 递归调用宏
)
}
fn main() {
println!("{}", find_min!(1));
println!("{}", find_min!(1 + 2, 2));
println!("{}", find_min!(5, 2 * 3, 4));
}
2.4. macro_use, macro_export 属性
macro_use 属性有两个作用,下面将一一介绍。
macro_use 作用一:如果宏定义在 module 中,默认宏仅能在 module 内部使用,如果 module 上声明 macros_use 属性,则 module 内定义的宏的作用域不会结束,可以被外部使用。比如:
#[macro_use] // 表示 mod1 内的宏 m 可以被外部使用
mod mod1 {
macro_rules! m {
() => {};
}
}
fn main() {
m!(); // 如果没有 #[macro_use],则这里会报错
}
需要说明的是, 在宏 m 上标记 macro_export 属性,可以让 m 被外部使用。 如:
mod mod1 {
#[macro_export]
macro_rules! m {
() => {};
}
}
fn main() {
m!(); // 宏名上指定 #[macro_export],也可以让宏在外部使用
}
macro_use 作用二:引入外部 crate 中定义的宏。比如外部 crate bar 中定义了宏 baz,可以这样引入进来:
// Rust 2015
#[macro_use(baz)] // 引入 bar 中的宏 baz,如果省略小括号,则引入 bar 中所有宏
extern crate bar;
fn main() {
baz!();
}
不过,Rust 2018 简化了这样的写法,上面代码可写为:
// Rust 2018
use bar::baz;
fn main() {
baz!();
}
2.5. 宏作用域
出于历史的原因,宏有两种形式的作用域:Textual scope 和 Path-based scope。两者的区别在于:如果调用宏时,直接使用不带 Path 的名称,则先在 Textual scope(当前环境)中寻找宏,找不到再去引入的 Path 中寻找宏;如果调用宏时,指定了 Path,则仅仅在引入的 Path 中寻找宏,忽略 Textual scope 中的宏。下面是个例子:
use lazy_static::lazy_static; // Path-based import,引入 crate lazy_static 中的宏 lazy_static
macro_rules! lazy_static { // Textual definition,定义一个宏 lazy_static(同名宏不是好习惯,仅演示)
() => {};
}
lazy_static!{} // 调用宏时没有 Path,先找本文件中定义的 lazy_static,找不到再找引入的 lazy_static
self::lazy_static!{} // 调用宏时有 Path `self::`,忽略本文件中定义的 lazy_static,仅找引入的 lazy_static
fn main() {}
2.6. $crate 让宏行为更明确
在定义宏时,可以使用 Metavariable $crate 来表示定义宏的 crate 的起始 path,这可以使宏的行为更明确。如:
pub mod inner {
pub fn foo() {
println!("inner::foo called.");
}
#[macro_export]
macro_rules! call_foo1 {
() => { foo() };
}
#[macro_export]
macro_rules! call_foo2 {
() => { $crate::inner::foo() }; // $crate 表示定义宏的 crate 的起始 path
}
}
fn foo() {
println!("foo called.");
}
fn main() {
call_foo1!(); // 输出 foo called.
call_foo2!(); // 输出 inner::foo called.
}
3. Procedural Macros
除了 macro_rules! 定义宏外,过程宏(Procedural Macros)也可以生成新代码,过程宏有下面 3 种:
- 派生宏(Derive macro) -
#[derive(CustomDerive)],如#[derive(Debug)]等 - 属性宏(Attribute macro) -
#[CustomAttribute],如#[inline]等 - 函数式宏(Function-like macro) -
custom!(...)
关于 Procedural Macros 可参考:https://doc.rust-lang.org/reference/procedural-macros.html