rust宏魔法-2

Rust宏魔法——第二辑(

又来整点奇技淫巧了

这期就玩玩用声明宏来生成宏(

总结可以直接看 👉 省流

假设有以下的奇怪情景:

你整了一个日志系统,但是其中的输出内容是预设好的(当然也应该预留一个比如 Other 的来自定义内容)

然后你想要给每一种输出内容都注册一个宏,并且有些输出是带有 “{}” 的,因此你希望能够有类似下面这个效果:

1
2
app_start_failed!(e)
// App start failed due to the error : {e}

也就是像 println! 之类的宏能够接受不定数量的参数,具体看格式(也就是你预设字符串里面有几个 “{}” )

为了方便演示,就以以下作为例子吧

1
2
app_started : "App start!"
app_start_failed : "App start failed due to the error: {}"

如果只是考虑上面这种,应该很容易写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#[macro_export]
macro_rules! generate_macros {
( $( $id:ident => $e:expr ),*$(,)? ) => {
$(
#[macro_export]
macro_rules! $id {
() => {
println!($e);
};
($tt:tt) => {
println!($e, $tt);
}
}
)*
}
}

generate_macros! {
app_started => "App start!",
app_start_failed => "App start failed due to the error: {}",
}

fn main() {
app_started!();
app_start_failed!("AAAAAAA");
}

这样自然能够正常运行,但是也不难发现问题,就是如果增加了一个有两个 “{}” 的字段,那就不得不给 generate_macros 里面新增一个规则。

当然,对于 macro 烂熟于心的你,肯定想到可以把这个里面的匹配模式统一成 $($tt:tt)* ,也就是类似下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
#[macro_export]
macro_rules! generate_macros {
( $( $id:ident => $e:expr ),*$(,)? ) => {
$(
#[macro_export]
macro_rules! $id {
($($tt:tt)*) => {
println!($e, $tt);
}
}
)*
}
}

直接自信运行!

1
2
3
4
5
error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
--> src/main.rs:9:19
|
9 | ($($tt:tt)*) => {
| ^^^^^^^

报错了,悲(

这个原因主要是像 $( #[macro_export] xxxxx )* 这样,被递归展开了,但是在 macro_rules! $id {} 中这么写,就不知道什么时候结束,因为 $tt 也是没有在 macro_rules! generate_macros 中声明(

但是这里如果是通过 proc-macro 包上一层就可以正确解析了

新建一个 proc-macro 类型的crate,写上以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, parse::{Parse, ParseBuffer}, Token, Ident, LitStr};

struct MacroInfo {
id: Ident,
msg: LitStr,
}

impl Parse for MacroInfo {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
let id = input.parse::<Ident>()?;
input.parse::<Token![=>]>()?;
let msg = input.parse::<LitStr>()?;
Ok(Self { id, msg })
}
}

#[proc_macro]
pub fn make_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as MacroInfo);

let id = &input.id;
let msg = &input.msg;

quote! {
macro_rules! #id {
($($tt:tt)*) => {
println!(#msg, $($tt)*);
}
}
}.into()
}

然后 generate_macros 这边替换一下就好了

1
2
3
4
5
6
7
8
9
10
#[macro_export]
macro_rules! generate_macros {
( $( $id:ident => $e:expr ),*$(,)? ) => {
$(
make_macro! {
$id => $e
}
)*
}
}

使用示范:

1
2
3
4
5
6
7
8
9
10
11
generate_macros! {
hello => "Hello, {}!",
goodbye => "Goodbye, {}!",
hhh => "hhh, {} and {}!",
}

fn main() {
hello!("world");
goodbye!("world");
hhh!("world", "aaa");
}

如此,写任意格式的字符串就不需要在 generate_macros 那里添加新的匹配模式了。

省流

为了避免在 macro_rules 中定义 macro_rules 时错误的解析内层声明宏的 $( xxx )* 这一类模式,需要把这部分移动到 proc-macro 中。

Welcome to my other publishing channels