Rust 宏是一种强大的元编程工具,允许开发者在编译时生成或修改源代码,从而增强代码的可复用性、简洁性和抽象能力。Rust依赖宏编程,实现了零成本抽象:抽象在和替换在编译阶段,不在运行时引入任何开销。Rust 提供了两种主要的宏类型:macro_rules! 宏(也称为“声明式宏”或“模式宏”)和过程宏。本文主要是介绍Rust声明式宏的定义和使用,以及一些宏编程基本原理,帮助大家实现Rust宏编程入门。
获取关于宏
熟悉C语言的童鞋对宏不会陌生,比如定义一个取最小值的宏,类似这样:
#define MIN(a, b) ((a) < (b) ? (a) : (b))
应用该宏,取两个字符串最小长度,类似这样:
int minLenght(char* str1, char* str2)
{
return MIN(strlen(str1), strlen(str2));
}
在C语言中,宏是在预编译阶段进行替换(展开)。可以简单的认为(不严谨),C语言中的宏是“符号”的替换过程:
(1)MIN(..) => ( (..)< (..) ? (..) : (..) )
(2)a => strlen(str1) , b => strlen(str2)
(3) MIN(strlen(str1), strlen(str2)) => ( (strlen(str1)) < (strlen(str2)) ? (strlen(str1)) : (strlen(str2)) )
上述代码经过预编译后,代码为:
int minLenght(char* str1, char* str2)
{
return ( (strlen(str1))<(strlen(str2) )? (strlen(str1)) : (strlen(str2)) );
}
Rust中,也支持宏。
Rust中的宏概念和C中的宏类似,都是面向编译器制定的一种源代码扩展、替换的方法和规则。
虽然我们之前没有系统的学习宏,但是已经多次用过它,例如在很多例子中,使用println!打印输出,而这个println!就是最常用的一个宏;可以看到它和函数最大的区别是:它在调用时多了一个 !,除此之外还有 vec! 、assert_eq! 都是相当常用的,可以说宏在Rust中无处不在。
在 Rust 中宏分为两大类:声明式宏( declarative macros ) macro_rules! 和过程宏( procedural macros )。
进一步,过程宏又分为派生宏,类属性宏,类函数宏:
#[derive],在阅读代码过程中经常见到,可以为目标结构体或枚举派生指定的代码,例如Debug特征- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
- 类函数宏(Function-like macro),看上去就像是函数调用。
Rust宏相比于C的宏,在类型安全性、模式匹配、卫生性(见下面注释)、定义与使用上都有大幅提升;自然其复杂程度也相比C提升不少。但也不必担心,接下来我们将逐个看看它们的庐山真面目。
注:宏的卫生性(Hygiene)是指在宏展开过程中,确保宏生成的代码与宏调用所在的上下文之间具有清晰的隔离,以防止宏引入的标识符(如变量名、函数名、类型名等)与上下文中的已有标识符发生意外的冲突或绑定。卫生性是现代宏系统的一个重要特性,旨在解决传统宏系统中常见的命名空间污染和未预期的绑定问题。
声明式宏 —— macro_rules!
在 Rust 中使用最广的就是声明式宏,一些场景中说:“Rust的宏”,通常指的就是声明式宏 。
声明式宏和C代码中的宏最接近:声明一个宏样式,按模式匹配的方式进行代码扩展或替换。与C中的宏替换不同的是,Rust宏展开过程中,应用了模式匹配(和rust的match模式相似):
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}
声明式宏,允许我们像使用match一样,通过对宏声明内的表达式按多个模式逐个进行匹配,一旦匹配到某个模式,则按该模式后面所定义的表达式进行宏展开(这个过程和C的宏展开类似,属于源代码级“符号”替换);一旦所有模式都不匹配,则宏展开失败。
简化版的vec!宏
在学习数组章节,一定会接触到用一个vec!宏来便捷地定义并初始化一个数组。其“便捷”在于:
1) 无需事先声明数组内元素的类型;
2) 无需事先指定长度,可以按自己的需求随意添加初始化元素个数。
列如:
let v = vec![1, 2, 3];
在Rust强类型限制、严格的初始化流程检测要求下,上面的代码看山去有些不可思议。
这正是macor_rules!的魅力所在。
我们来实现一下。
cargo new --libe imarco
在lib.rs中添加以下代码:
#[macro_export]
macro_rules! ivec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
pub fn get_default_i32vec() -> Vec<i32> {
ivec![1,2,3]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_declare_int() {
let array = ivec![1,2,3,4];
assert_eq!(array.len(), 4);
assert_eq!(array[0], 1);
assert_eq!(array[1], 2);
assert_eq!(array[2], 3);
assert_eq!(array[3], 4);
}
}
完成后,我们运行一下测试: cargo test test_declare_int

测试运行成功,我们第一个宏定义顺利通过!
对宏内容逐行解释:

见截图,对ivec!宏进行了逐行说明。其中关键点
1) 第3行描述了一种模式匹配:宏参数中包含多个表达式,表达式按“,”分割;表达式重复0次或多次(详见后续 模式解析 章节);
2) 6、7、8行描述了重复模式的替换方式:每一个重复的表达式,都应用一次temp_vec.push([该表达式])的展开。
总结而言,ivec!的功能是,对ivec![ item1, item2, ...] 声明的数组,执行以下过程:
1) 定义一个代码块,在该代码块内定义一个rust 数组 temp_vec;
2) 对宏参数进行匹配,按 "," 进行表达式分割;对每一个有效表达式执行一次 temp_vec.push([表达式内容])的代码扩展;
3) 代码块返回temp_vec。
以get_default_i32vec 函数为例,该函数展开后,应该是这样:
fn get_default_i32vec()->Vec<i32> {
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
}
我们在imarco工程下,用cargo expand --lib 将该工程代码进行宏展开:

其结果与我们分析的一致!
多模式匹配
针对ivec!宏,补充一组测试用例:
#[test]
fn test_empty_input() {
let array = ivec![];
assert_eq!(array.len(), 0);
}
该测试意图测试ivec!宏参数为空时,期望返回一个长度为0的数组。
实际执行时,该测试会失败:

原因是按照宏展开,该代码为:
#[test]
fn test_empty_input() {
let array = {
let mut temp_vec = Vec::new(); // new 一个数组,但目前尚不清楚类型,需要在使用前进行一次数据绑定(设置)以明确数据类型
temp_vec
}
assert_eq!(array.len(), 0); // 此处已经开始使用数组,但尚未明确类型,因而编译报错
}
对ivec!宏增加一个使用限制为:当不传递参数时,默认初始化一个i32数据类型的数组。
修改ivec!增加一个match分支,匹配空参数,执行初始化一个i32 数组并返回。代码如下:
#[macro_export]
macro_rules! ivec {
() => {
{
let temp_vec:Vec<i32> = Vec::new();
temp_vec
}
};
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
再运行测试则可以顺利通过!
注意:我们定义一个空分支的处理方式只是为了演示多模式匹配,而不是vec!宏真实的需求(实际上宏参数为空时,就应该返回一个未绑定数据类型的空数组,让后续使用时进行数据绑定)。
上述代码中,我们对宏增加了一个match分支,以对宏参数为空时进行处理;在实际实现中,可能会有更多场景需要细化考虑,因此可能有更多的分支。以下是标准库中vec!宏实现
该宏实现了三中模式匹配:
1) vec![]:创建一个空的数组(未绑定数据类型)。
2) vec![$elem; $n]:创建一个包含 $n 个相同元素 $elem 的数组。
3) vec![$x, $y, $z, ...]:创建一个包含给定元素的数组。
标准库中的vec! 实现要比ivec!实现复杂,原因是它还处理空间预分配等事宜,并且对多个元素的初始化采用了指针数组的分配方式,提高效率。
该宏的进一步理解,就留给大家作为课后作业啦!
模式解析
回过头来,我们再来捋一捋 ivec!关键模式匹配 ( $( $x:expr ),* ) 的含义。
- 首先,
(...)=>表示一组模式匹配分支,其写法是固定的。 - 一个匹配分支内,
$()描述一个特定的模式(pattern);与该pattern相匹配的宏参数,会被捕获,然后用于代码扩展或替换;在这里$($x:expr)会匹配任何Rust有效表达式,并捕获该内容记录到$x中。其中,$x称之为元变量,expr是该元变量的类型(expr为表达式,block为块,ty为数据类型等等,后续会单独说明)。 - 紧跟
$( $x:expr )之后的 "," 是模式重复分割标识,表示重复的pattern之间用“,”进行分割。 - 在模式重复标识后面,是重复次数的正则规则;
*代表可能重复0次或多次,+代表可能重复1次或多次,?代表可能重复0次或1次(实际上是不会重复的,因此?前面不能出现重复分割标识)。
当我们使用 ivec![1, 2, 3] 来调用该宏时,$x 模式将被匹配三次,分别是 1、2、3。为了帮助大家巩固,我们再来手动展开一次:
$()中包含的是模式$x:expr,该模式中的expr表示会匹配任何 Rust 表达式,并给予该模式一个名称$x- 因此
$x模式可以跟整数1进行匹配,也可以跟字符串 "hello" 进行匹配:ivec!["hello", "world"] $()之后的逗号,意味着1和2之间可以使用逗号进行分割,也意味着3既可以没有逗号,也可以有逗号:vec![1, 2, 3,]*说明之前的模式可以出现零次也可以任意次,这里出现了三次。
接下来,我们再来看看与模式相关联、在 => 之后的代码:
{
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
$(...)* 描述了每次模式匹配命中后的行为:在源码中添加代码temp_vec.push($x) ,其中$x 替换为本次模式匹配后捕获到的入参内容。
ivec![1,2,3];
展开为
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。
元变量类型
block
block 类型符只匹配 block 表达式。
块 (block) 由 { 开始,接着是一些语句,最后是可选的表达式(返回值表达式),然后以 } 结束。 块的类型要么是最后的值表达式类型,要么是 () 类型。
ident
ident 分类符用于匹配任何形式的标识符 (identifier) 或者关键字。
示例:
macro_rules! define_fn {
($func_name:ident, $block:block) => {
fn $func_name(s: &str) -> String {
let mut out = String::from(s);
let inner = $block;
out.push_str(inner.into());
out
}
};
}
define_fn!(comand_appender, { "command-appender" });
#[test]
fn test_dynamic_fn_command_appender() {
let x =comand_appender("hello");
assert_eq!(x, "hellocommand-appender");
}
define_fn! 宏可以动态定义函数。示例中,定义了一个comand_appender函数,函数名和部分函数实现是由宏实现的;其中,func_name就是ident 类型,而block是 block类型。用cargo expand 展开该函数为:

item
item 分类符只匹配 Rust 的 item 的 定义 (definitions) , 不会匹配指向 item 的标识符 (identifiers)。
例子:
macro_rules! items {
($($item:item)*) => ();
}
items! {
struct Foo;
enum Bar {
Baz
}
impl Foo {}
}
item是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:
modulesextern cratedeclarationsusedeclarationsfunction definitionstype definitionsstruct definitionsenumeration definitionsunion definitionsconstant itemsstatic itemstrait definitionsimplementationsexternblocks
ty
ty 类型用于匹配任何形式的类型表达式 (type expression)。类型表达式是在 Rust 中指代类型的语法。
示例:
macro_rules! types {
($($type:ty)*) => ();
}
types! {
foo::bar
bool
[u8]
impl IntoIterator<Item = u32>
}
path
path 类型符用于匹配类型中的路径 (TypePath)。这包括函数式的 trait 形式。
示例:
macro_rules! paths {
($($path:path)*) => ();
}
paths! {
ASimplePath
::A::B::C::D
G::<eneri>::C
FnMut(u32) -> ()
}
lifetime
lifetime 类型符用于匹配生命周期注解或者标签 (lifetime or label)。 它与 ident 很像,但是 lifetime 会匹配到前缀 "'" 。
示例:
macro_rules! lifetimes {
($($lifetime:lifetime)*) => ();
}
lifetimes! {
'static
'shiv
'_
}
others
除了上面几种较为常见的元类型,还有一些类型如pat,meta,literal,stmt,tt等,感兴趣的可以阅读 《Rust 宏小册》获取更多信息。
实战
在开发中遇到一个需求:
有一个命令调度器被实现为结构体 engine,engine中有一个Command 的指针数组,记录着所有注册的指令(每个指令需要实现Command 的接口)。在初始化engine时,需要将builtin的所有指令注册到engine中(约150条)。
其中, 指令注册的关键代码为(示例注册Ls指令,Ls为结构体,实现了Command):
engine.add_decl(Box::new(Ls));
我们设计一个宏,register_commands!,支持多个参数,其中第一个参数固定为engine的实例,之后都是各种指令的机构体类型,当我们调用该宏时,就可以实现向engine实例中注册所罗列的指令:
register_commands! (engine, Ls, Cd, Echo, Evl, Ps, Find, Grep...);
分析:
(1) 核心模式匹配中,一个需要匹配两类参数:一个事engine实例,一个是指令类型;其中engine示例必须有一条,而指令类型可能有多个;
(2) 边界问题:如果指令类型为0个,则什么事情也不做,也符合预期。
因此,我们给出如下宏实现:
macro_rules! register_commands {
($target:expr, $($cmd_type:expr),*) => {
{
$(
$target.add_decl(Box::new($cmd_type));
)*
}
};
}
$target为表达式,直接匹配,后面跟一个 ","。
$cmd_type 为表达式,可以重复,重复分隔符为","; 可以重复0次或多次。
对于重复的匹配,每次匹配都执行一次 $target.add_decl(Box::new($cmd_type)); 的代码替换和扩展。
实际使用:

小结
要深入理解Rust宏,还需要掌握一些Rust编译的基础知识:包括标记(token)解析,语法解析,AST抽象语法树,标记树(token tree)等等。而抽象语法树和标记树在后续过程宏中将会接触到。本文尽量避开这些比较底层的知识,给大家简述了一下rust的 声明宏,希望能够对Rust的宏编程(元编程)有个初步认识;之后大家再逐步深入掌握吧......
本篇就此“杀割”,下一篇我们将继续探讨Rust的过程宏。
下回分解了老铁....