前言#
昨天我们了解到了宏里面涉及的一些概念,今天我们将学习声明宏。
声明宏#
前面我们接触过一点宏相关的知识:rust基础学习--day37 - 知乎 (zhihu.com),知道了rust中宏有两大类:声明宏和过程宏。其中声明宏 macro_rules!就是我们今天的主角。
接下来我们将通过理论和实践两方面来学习。
理论知识#
macro_rules!#
macro_rules!实际上自己就是一个宏,也就是说实际上它自己就是一个语法拓展,也就意味着它从技术方面(technically)来说并不属于rust语法的一部分。不必去追究macro_rules!的展开是啥,看不到的,因为它属于编译器内部的一部分,这也就是为什么我们可以在任意地方去调用它的原因。
它的使用格式如下:
macro_rules! $name {
$rule0 ;
$rule1 ;
// …
$ruleN ;
}至少包含一个rule,另外最后一个rule可以不写;。
每一个规则类似下面这种:
($matcher) => {$expansion}一般都是用()来包裹matcher,而{}包裹表达式,和pattern类似。
当然,对于调用的来说,可以使用[]、()又或者{},不用拘泥于(),不过一般看场景,比如类似函数的宏用()。
规则的展开这部分一般被叫做transcriber,后面一致叫做转录器。
Matching#
当macro_rules!宏被调用,它的解析器(interpreter)会根据定义的顺序一个一个的过一遍所有规则,它会尽量去匹配传入的token tree和规则的matcher。如果匹配到了符合的规则,就会将规则的展开产物当做是这个宏的展开产物。
来看个例子:
macro_rules! four { () => { 1 + 3 }; }这个例子只有一个规则,规则没有matcher。我们调用的时候可以直接four!()或者four![]又或者four!{}。
有点需要说下,matcher还可以是单纯的字面量,这就意味着必须完全匹配到这个字面量才会符合这个rule。比如:
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}有点抽象,然而这就只是个字面量:4 fn ['spang "whammo"] @_@,没什么高深的内容。
元变量#
matcher也可以包含捕获(captures)。它允许传入通过一些基础语法类别来匹配输入给宏的内容。
被匹配到的内容即元变量,它可以作为变量存在于规则中甚至作为输出展开的一部分。
格式为:$var:kind,其中$是字面量,var则是你的变量名,$var则是作为一个元变量,可以用于规则中,然后:作为这个变量和kind的分隔符。这个kind也可以被叫做fragment-specifier,它一定是下面其中一种:
block:一个块,即被{}包裹的语句或者表达式的块;expr:表达式;ident:标识符(包括keywords)item:一个项,比如函数、struct、module、impl等;lifetime:生命周期,比如'a、'static等;literal:一个字面量,比如"3.14"、"hello world"等;meta:元项,#[...]或者#![...]这俩属性的内部内容,比如#[derive("test")]里的derive("test");pat:一个模式(pattern);path:一个路径,比如foo,::std::mem::replace,transmute::<_, int>等;stmt:语句;tt:一个简单的token tree;ty:一个类型;vis:一个可能为空可见性限定符(不知道啥意思。。。原文:a possible empty visibility qualifier),比如pub,pub(in crate)等;
如果想更深入的理解,可以到这: Fragment Specifiers
另外,这里还有一个很特殊的元变量:$crate,它指向当前的crate。
来看个例子:
macro_rules! one_expression {
($e:expr) => {...};
}这就是匹配一个表达式作为变量$e,那么$e就是一个元变量。注意,这里expr一定是一个有效、完整的表达式,因为如果不是,编译器会先报错。
我们再来看个例子:
macro_rules! times_five {
($e:expr) => { 5 * $e };
}这里我们在规则里将捕获到的元变量$e和5相乘。
和宏展开类似,这里的元变量会被替换成一个完整的AST节点,这就意味着不管这里捕获到的$e是个啥tokens序列,它都会被解析成一个简单完整的表达式。
另外一个matcher中可以包含多个捕获,比如:
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
}Repetitions#
matcher也可以包含重复的内容。格式:$(...) sep rep。
$是一个字面量;(...)是一个重复的圆括号组(paren-grouped) matcher,也就是重复匹配的内容;sep是一个可选的分隔符,它不一定是一个分隔符(delimiter),也可能是一个重复内容的运算符(operators)。常见的例子:,或者;。rep是一个必要的重复运算符,它们可以是:?:指示最多一个重复内容。由于它指示最多只有一个重复内容,所以它不能搭配分隔符;*:指示0个或者多个重复内容;+:指示0个或者多个重复内容;
重复的内容中还可以套娃,包含其它符合的matcher,包括字面量token trees、元变量以及其他支持套娃的重复内容。
重复的内容在展开中使用相同的语法,重复的元变量只能在展开的重复的内容中使用。
来看个例子:
macro_rules! vec_strs {
(
$($element:expr) // 重复的内容是元变量,类型为表达式
, // 分隔符
* // 匹配0个或者多个
) => {
{
let mut v = Vec::new();
$(
v.push(format!("{}", $element));
)* // 重复性的将匹配到的$element以字符串的形式存入到vec中
v // 最终展开产物
}
};
}
fn main() {
let s = vec_strs![1, "a", true, 3.14159f32];
assert_eq!(s, $["1", "a", "true", "3.14159"]);
}这个例子中,我们匹配了0个或者多个重复的表达式,然后将它以字符串的形式存入到一个vec中,这个vec则是最终的展开产物。
当然,我们还可以在重复的内容中套娃匹配重复的元变量,比如:
macro_rules! repeat_two {
($($i:ident)*, $($i2:ident)*) => {
$(let $i: (); let $i2: ();)*
}
}
repeat_two!(a b d e f, u v w x y z);
repeat_two!(a b c d e f, x y z); // 错误用法这里匹配了两个重复内容,重复内容中分别又匹配重复的标识符,然后将它们声明。
注意,两个表达式的长度需要一致。
元变量表达式#
相关commit:rfcs#1584
功能:#![feature(macro_metavar_expr)]
转录器可以包含元变量表达式(metavariable expressions)。元变量表达式提供转录器关于元变量的一些难获取的信息。除了$$表达式之外,常用的格式为:${ op(...) }。看格式就知道,是用来处理重复内容的(不包括$$)。
${count(ident)}:$ident在最里面(inner-most)重复内容的重复次数,等同于${count(ident, 0)};${count(ident, depth)}:$ident在不同层的重复内容的重复次数;(这里的depth应该指的是嵌套的层数)${index()}:最里面的层的重复位置索引,等同于${index(0)};${index(depth)}:depth层的重复位置索引;${length()}:最里面层将重复的次数,等同于${length(0)};${length(depth)}:不多说${ignore(ident)}:绑定ident来重复,但是展开不包含indent;$$:展开为一个$,保证$不会被转录;
来看个例子:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::with_capacity(${count(x)});
$(
temp_vec.push($x);
)*
temp_vec
}
};
}这里获取了最里面一层(实际上也就一层)$x的次数,然后调用Vec::with_capacity声明一个固定长度的vec,这么做可以提前分配vec的堆内存,不需要等runtime再确定。
实践#
前面我们通过理论知识大致上了解了如何编写一个声明宏,那么现在我们来实战。
我们的目标是实现一个斐波那契数列 Fibonacci sequence ,不过有些蛋疼,现在先不说为什么蛋疼,后面会知道的。
创建环境#
就不搞workspace这么麻烦了,只搞一个文件,然后
rustc +nightly -Zunpretty=expanded xxx.rs即可。或者随便搞个文件夹,cargo init即可。
实现#
我们的目标就是将下面这种写法转换成计算
a[n] = 0, 1, ..., a[n-2] + a[n-1]通过这一串创建一个简单的斐波那契数列迭代器,那么我们的目标代码就应该是这样:
let fib = recurrence![a[n] = 0, 1, ..., a[n-2] + a[n-1]];
for e in fib.take(10) { println!("{}", e) }它应该输出斐波那契数列前10个数。
注意,这里的a[n] = 0, 1, ..., a[n-2] + a[n-1]只是单纯的一串字符串字面量,所以我们的matcher应该是匹配这个字符串,我们的matcher代码应该如下:
macro_rules! recurrence {
(a[n] = $($inits:expr),+,...,$recur:expr) => { ... };
}$inits是元变量,匹配的是数据中0, 1,而$recur则是匹配的a[n - 2] + a[n - 1]这个表达式
不过这里有个问题,就是分隔符,我们这里用的,而...前后也是用的,分隔,所以编译器匹配的时候会认为它也是$inits,那么就会有问题了:
error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other option.
--> src/main.rs:75:45
|
75 | let fib = recurrence![a[n] = 0, 1, ..., a[n-2] + a[n-1]];
|所以这里我们需要将...前后的,替换为;,即:
macro_rules! recurrence {
(a[n] = $($inits:expr),+;...;$recur:expr) => { ... };
}
fn main () {
let fib = recurrence![a[n] = 0, 1; ...; a[n-2] + a[n-1]];
for e in fib.take(10) { println!("{}", e) }
}然后我们来完成计算相关的逻辑,我们需要在规则中实现这块内容。
首先我们需要一个struct,用来存放最近两次的计算结果,以及当前n的数值,也就是n的位置:
struct Recurrence {
mem: [u64;2],
pos: usize,
}并且这货得是个迭代器,因为最终展开产物是个迭代器,所以它还需要实现迭代器的功能,而实现迭代器就需要实现next方法:
struct Recurrence {
mem: [u64;2],
pos: usize,
}
impl Iterator for Recurrence {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
// 小于2的场景单独处理
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = self.mem[self.pos - 2] + self.mem[self.pos - 1];
self.pos += 1;
Some(next_val)
}
}
}但是这段代码还有点问题,就是self.mem是没有更新的并且长度永远等于2,而n的位置则一直在递增,所以并不能跑,我们还需要拓展下,让这个mem可以更新,那么简单的总结下,我们需要解决的点:
- 数据更新问题;
- 位置偏差,我们需要一个相对的数值,而不是绝对值
n。
针对于第二点,我们可以引入Index这个trait。
由于是无关代码,所以这里直接贴代码
use std::ops::Index;
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(2);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}- Wrapping可以保证不会溢出。
index - offset + window,即相对的位置,永远不会大于2。
那么接下来我们需要处理slice数据更新的问题。
这个也简单,直接贴代码:
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in [1, 0] {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}- swap:交换两个可变数据在内存上的位置,不会初始化。
这一小段代码稍微有点绕,就是将mem[1]和swap_temp互换数据,而swap_temp变成mem[1]的数据后又和mem[0]交换数据,那么mem[1]的数据就是a[n],而mem[0]则是a[n - 1],那么数据更新为最新的了。
然后我们将这俩段代码组装进我们的宏里面:
macro_rules! recurrence {
(a[n] = $($inits:expr),+;...;$recur:expr) => {{
use std::ops::Index;
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(2);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
impl Iterator for Recurrence {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
// 小于2的场景单独处理
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let n = self.pos;
let a = IndexOffset {
slice: &self.mem,
offset: n,
};
a[n - 2] + a[n - 1]
};
self.pos += 1;
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in [1, 0] {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
Some(next_val)
}
}
}
Recurrence {
mem: [0, 1],
pos: 0,
}
}};
}
fn main() {
let fib = recurrence![a[n] = 0, 1;...;a[n - 2] + a[n - 1]];
for f in fib.take(10) {
print!("{f}\n");
}
}然后我们运行下cargo run

可以看到是正常打印出来了。
但是我们的元变量完全没用上啊!
所以我们还需要调整下代码让我们的代码中用上捕获的元变量。
首先是前面两个$inits,实际上它不一定是0和1,它理论上可以是一大串一大串的数字集合,所以我们的代码得改成动态长度,而不是固定的2。
这里我们就可以用上元变量表达式里的count
const MAX_SIZE: u64 = ${count(inits, 0)};但是并不能跑通,即使是切换至nightly也不行,那就不理了,反正是个nightly中的方案,我们这里用其它方案实现,比如下面这样:
macro_rules! count_exprs {
() => (0);
($head:expr) => (1);
($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
}
const MAX_SIZE: u64 = count_exprs![$($inits),+];
再写一个宏,用来计算到底有几个数字。当然,这种递归行为实际上如果超出默认128容易爆掉,不过一般没有人会手动写这么多个数字,也可以通过属性去调整,不多说。
然后调整下有字面量2的地方即可,这里就不贴代码了,不过有个点需要说下,初始化Recurrence实例的地方需要调整下:
Recurrence {
mem: [$($inits),+],
pos: 0,
}接着我们来用上$recur,这个就简单了,是不能变的,所以直接调整let a 那边即可:
let next_val = {
let n = self.pos;
let a = IndexOffset {
slice: &self.mem,
offset: n,
};
$recur
};但是这样写会遇到如下的报错:

这个时候我们可以切nightly去看下展开的产物

看着好像正常?实际上还是得回到卫生这个概念,rust中宏引用的标识符默认是卫生的,所以这里两个不同的上下文自然没有办法运行。
我们来看个简单的例子:
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);它的产物自然是
let four = { let a 42; a / 10 };但是会报错,因为是两个不同的上下文作用域。
那怎么办呢?简单,让它也存在于展开前的上下文即可:
macro_rules! using_a {
($a:ident, $e:expr) => } {
{
let $a = 42;
$e
}
}
}有点反直觉。。后面熟悉了就好。
那么我们的代码也需要调整下,既然a和n都不存在,那么我们matcher中给他俩变成标识符不就好了,我们需要调整下我们的matcher
macro_rules! recurrence {
($seq:ident[$ind:ident] = $($inits:expr),+;...;$recur:expr) => {
...
let next_val = {
let $ind = self.pos;
let $seq = IndexOffset {
slice: &self.mem,
offset: n,
};
$recur
};
...
}
}这样就行了
这样就用上了我们定义的两个元变量
然后我们还可以拓展下a[n]的类型,作为每个元素的类型,代码就不贴了。
直接贴最终代码:
macro_rules! count_exprs {
() => (0);
($head:expr) => (1);
($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
}
macro_rules! recurrence {
($seq:ident[$ind:ident]: $ety:ty = $($inits:expr),+;...;$recur:expr) => {{
use std::ops::Index;
const MAX_SIZE: usize = count_exprs![$($inits),+];
struct IndexOffset<'a> {
slice: &'a [$ety; MAX_SIZE],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = $ety;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b $ety {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(MAX_SIZE);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
struct Recurrence {
mem: [$ety; MAX_SIZE],
pos: usize,
}
impl Iterator for Recurrence {
type Item = $ety;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
// 小于2的场景单独处理
if self.pos < MAX_SIZE {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let $ind = self.pos;
let $seq = IndexOffset {
slice: &self.mem,
offset: $ind,
};
$recur
};
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in (0..MAX_SIZE).rev() {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
self.pos += 1;
Some(next_val)
}
}
}
Recurrence {
mem: [$($inits),+],
pos: 0,
}
}};
}
fn main() {
let fib = recurrence![a[n]: u64 = 0, 1;...;a[n - 2] + a[n - 1]];
for f in fib.take(10) {
print!("{f}\n");
}
}
最终结果也表现正常:

那么我们的这个实践例子就完成了
是不是有些蛋疼,因为一般人写不出这种样子的宏。。
总结#
今天我们学习了如何编写一个稍微有点复杂的声明宏。
