什么是 Rust 宏? —— 浅谈 Rust 宏

HoshinoTented

2020-08-02 13:06:14

科技·工程

阅读须知:本文中大部分包含宏的代码无法直接编译,需要将人名币符号 '¥' 替换为美元符号 '$' 才可编译。

计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。—— Wikipedia

C 所使用的 CPP(C PreProcessor,即 C 预处理器)实现的宏是简单的文字查找与替换,不过它能接收一些编译时的参数来 “动态地” 替换代码。

Rust 宏

而 Rust 的宏不是简单的文字查找与替换,Rust 宏基于 语法树 进行操作,比如:Rust 宏的参数可以指定类型(如 字面量 literal,表达式 expr 等等)

除了 类函数 的宏,Rust 还提供了另外两种宏:

但以上两种宏都属于 过程宏(Procedural Macros),本文不加讨论。

使用 Rust 宏

以最常见的 Rust 宏 println! 为例,你可以这样使用它:

let ar = "ar";
let gu = String::from("gu");
let s = 's';

println!("literal string, and here are the {}{}{}{}.", ar, gu, "ment", s);

我们来解析一下这个宏的调用:

三者缺一不可

解析 Rust 宏的定义

panic! 宏为例:

std::panic

macro_rules! panic {
    () => ({ ¥crate::panic!("explicit panic") });
    (¥msg:expr) => ({ ¥crate::rt::begin_panic(¥msg) });
    (¥msg:expr,) => ({ ¥crate::panic!(¥msg) });
    (¥fmt:expr, ¥(¥arg:tt)+) => ({
        ¥crate::rt::begin_panic_fmt(&¥crate::format_args!(¥fmt, ¥(¥arg)+))
    });
}

随后的大括号代表宏定义的内容。

宏定义的内容由 规则(rule) 组成,每一条规则都形如:

(Pattern) => { Exapnsion };

最后一条规则末尾的分号可以省略。

事实上,规则所用的括号可以是以下的任何一种:()[]{}。并且在调用的时候并不会对所用的括号进行检查,也就是说,你可以用 foo! {} 来调用定义为 macro_rules! foo { () => {} } 的规则。

当宏被调用时,会由上而下对每个规则进行匹配,如果某一条规则与输入 完全 匹配,则立刻进行该规则所对应的展开。考虑如下宏:

macro_rules! test {
    ( ¥( ¥e:expr )* ) => { println!("expr"); };
    ( ¥( ¥t:literal )* ) => { println!("literal"); };
}

当这个宏被调用时,即使传入的参数为字面量,都会立刻匹配到第一个规则,因为所有合法的字面量都是合法的表达式。

panic! 宏的第一条规则很好理解,当宏的参数为空时,调用 panic! 宏并传入一个 "explicit panic" 的字面量。这看起来是宏在调用自己,但实际上就是这样,这次调用会进入第二条规则。

第二条规则接收一个名为 msgexpr 类型的参数,参数的声明与使用都需要在前面加上 ¥。假如不加上,msg:expr 会被识别为一个字面量的 tt(token tree) 模式,你就只能通过以下方法调用它:

panic!(msg:expr);       // 实际上可以在 msg 的左右侧,expr 的左右侧加上空格,空格将会被忽略

第二条规则的展开内容的意思是:调用 <panic! 所在的 crate>::rt::begin_panic 函数,并传入 msg 作为参数。

第三条规则的模式和第二条规则大同小异,只是在最后面加了一个 ,。而它的展开内容其实也只是调用了 panic!,最终会匹配第二条规则。

其实第 2、3 条规则的模式可以合并为 ¥msg:expr ¥(,)?,作者猜测是因为早期 Rust 不支持 ?,所以才分为了两条规则

最后是第四条规则,它接收两个参数:名为 fmtexpr 类型参数,和名为 argtt 类型的 重复 参数(即 可变数量 参数)。

Rust 宏的参数类型

Rust 宏的参数目前有如下类型:

其中,tt 类型可以被视为 Rust 宏的 Any。

宏还对各种类型的参数捕获之后所允许的内容添加了限制,以避免语义冲突:

Rust 宏的重复

宏中定义重复的语法如下:

¥ ( ... ) sep rep

其中:

举例:

pattern \ input 1, 2, 3 1, 2, 3,
¥( ¥i:expr ),* ×
¥( ¥i:expr, )* ×
¥( ¥i:expr ),* ¥(,)?

可以通过 ¥( ¥i )* 的语法来使用重复。

其实,还可以将两个重复参数作为一个整体,再进行重复,如:

macro_rules! zip {
    ( ¥( ¥i:expr ),* ; ¥( ¥j:expr ),* ) => {
        [ ¥( (¥i, ¥j) )* ]
    };
}

不过塞到同一个整体进行重复的重复参数必须重复相同次数,否则编译器会 panic。

热身

有了上面的知识,我们就能写一个比较简单的 Vec 构造宏了:

需求:传入零个或多个相同类型的值,构造一个包含这些值(按照顺序)的 Vec。

macro_rules! build_vec {
    (
        ¥( ¥i:expr ),*                          // 重复,以支持任意数量的参数
        ¥(,)?                                   // 可选的末尾逗号
    ) => {
        {                                       // 创建一个块,以支持多条语句
            let mut vec = Vec::new();           // 构造一个 Vec,必须为 mut,否则下文无法进行 push

            ¥(                                  // 重复,将每个 ¥i 推入 vec 中
                vec.push(¥i);
            )*

            vec                                 // 返回 vec
        }
    }
}

标准库有类似的 vec! 宏,但其内部实现不与演示中的代码相同,有兴趣的可以到 这里 查看源代码

你已经学会了 Rust 宏,快来试试看吧?

Haskell 的列表生成器是个非常不错的语法糖:

[1, 3 .. 10]
-- output: [1,3,5,7,9]
[1, 3 ..]
-- output: [1,3,5..]
[x + y | x <- [1..5], y <- [1..5]]
-- output: [2,3,4,5,6,3,4,5,6,7,4,5,6,7,8,5,6,7,8,9,6,7,8,9,10]

其中,[1, 3 .. 10][1, 3 ..] 可以用 Rust 的 RangeRangeFrom 结构体加上 step_by 函数实现:

macro_rules! list {
    // 匹配 list!(first, second, ..)
    // 注意,second 后面的 逗号 必须保留,原因在上文已经提到过了
    ( ¥first:expr, ¥second:expr, ..) => {
        {
            let step = ¥second - ¥first;
            (¥first..).step_by(step)
        }
    };

    // 匹配 list!(first, second, .. end) 和 list!(first, second, .., end)
    ( ¥first:expr, ¥second:expr, .. ¥(,)? ¥end:expr ) => {
        {
            let step = ¥second - ¥first;
            (¥first..¥end + 1).step_by(step)
        }
    };
}

来实验一下:

fn main() {
    println!("{:?}", list!(1, 3, .. 11).collect::<Vec<u32>>());
    println!("{:?}", list!(1, 3, ..).take(10).collect::<Vec<u32>>());       // 由于 RangeFrom 是一个无限的迭代器,所以需要使用 take

    // outputs:
    // [1, 3, 5, 7, 9, 11]
    // [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
}

接下来是最后一个,也是我认为 Haskell 中最强大的语法糖:

对于任意的 [ generator x0 x1 x2 ... | x0 <- xs0, x1 <- xs1, x2 <- xs2 ...] 可以转化为:

xs0 >>= \x0 ->
    xs1 >>= \x1 ->
        generator x0 x1 <¥> xs2       -- 最后一个列表的迭代需要使用 map

相应的 Rust 代码是这样:

xs0.flat_map(move |x0| {    // 必须进行 move,否则在 generator 调用的时候会抱怨生命周期
    xs1.flat_map(move |x1| {
        xs2.map(move |x2| {
            generator(x0, x1, x2)
        })
    })
})

这段冗长的 Rust 代码和 Haskell 的语法糖相比差太多了,不过我们可以用宏来解决!

首先要解决的问题是,我们无法使用重复来实现嵌套的 flat_map 与最后的 map。所以我们需要编写一个辅助用的宏:

macro_rules! __rec_iter {
    (
        ¥gen:expr ;                 // 生成器,expr 类型
        ¥id:ident <- ¥it:expr,      // 当前的迭代语句
        ¥( ¥rest:tt )* ) => {       // 剩余代码
        ¥it.flat_map(move |¥id| __rec_iter!(¥gen ; ¥( ¥rest )* ) )      // 生成一层 flat_map,其内部交给下一层 __rec_iter 宏
    };

    ( 
        ¥gen:expr ;                     // 生成器
        ¥id:ident <- ¥it:expr ) => {    // 当前的迭代语句,对于整个宏来说也是最后一个
        ¥it.map(move |¥id| ¥gen)        // 生成 map,其内部由为 gen
    };
}

这个宏实际上就已经能够完成 Haskell 列表生成器的工作了,但是为了隐藏内部细节,所以再为 list 内添加一个规则,让它调用这个 __rec_iter:

macro_rules! list {
    ( ¥gen:expr ; ¥( ¥id:ident <- ¥it:expr ),* ) => {
        __rec_iter!( ¥gen ; ¥( ¥id <- ¥it ),* )
    };

    /* other rules */
}

终于写完了,让我们来测试一下:

fn main() {
    let result = list!(x + y ; x <- 1..=5, y <- 1..=5);
    println!("{:?}", result.collect::<Vec<i32>>());

    // output
    // [2, 3, 4, 5, 6, 3, 4, 5, 6, 7, 4, 5, 6, 7, 8, 5, 6, 7, 8, 9, 6, 7, 8, 9, 10]
}

正确的输出!

卫生性

卫生性也是宏的一个重要特性。考虑以下代码:

macro_rules! using_x {
    ( ¥action:expr ) => {
        {
            let x = 1;
            ¥action
        }
    }
}

fn main() {
    let two = using_x!(x + 1);
}

进行编译,编译器会抱怨 x 不在当前作用域内:

using_x!(x + 1);
         ^ not found in this scope

试着手动展开宏:

fn main() {
    let two = {
        let x = 1;
        x + 1
    }
}

看起来 好像没什么问题?为什么编译器会抱怨呢?

这是因为,宏内的 x¥actionx 处于不同的,看不见的 句法上下文 中,略微修改一下展开后的代码,让这个句法上下文 “看得见”:

fn main() {
    let two = {
        let macro_x = 1;
        outer_x + 1
    }
}

调用宏所使用的 x 属于 outer 上下文,而宏内的 x 属于 macro 上下文,只有在标识符的明面名字和句法上下文 一致的情况下,这两个标识符才能被视为相同。

我们可以通过提供一个 ident 类型的参数让两个标识符的句法上下文相同:

macro_rules! using_x {
    ( ¥id:ident, ¥action:expr ) => {
        {
            let ¥id = 1;
            ¥action
        }
    }
}

fn main() {
    let two = using_x!(x, x + 1);
}

它通过了编译,很好!

参考

宏 - 维基百科

The Little Book of Rust Macros(中文)

Macros - TRPL(中文)

Macro By Example