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)加以限定,就跟普通的变量绑定一样用“冒号”将变量和类型分开,宏支持以下几种指示符:

相对于 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

4. 参考

Author: cig01

Created: <2020-12-05 Sat>

Last updated: <2021-01-21 Thu>

Creator: Emacs 27.1 (Org mode 9.4)