← 所有文章

rust宏魔法-2

Rust宏魔法——第二辑(

又来整点奇技淫巧了

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

总结可以直接看 👉 省流

假设有以下的奇怪情景:

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

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

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

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

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

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

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

#[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)* ,也就是类似下面这种:

#[macro_export]
macro_rules! generate_macros {
    ( $( $id:ident => $e:expr ),*$(,)? ) => {
        $(
            #[macro_export]
            macro_rules! $id {
                ($($tt:tt)*) => {
                    println!($e, $tt);
                }
            }
        )*
    }
}

直接自信运行!

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,写上以下代码

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 这边替换一下就好了

#[macro_export]
macro_rules! generate_macros {
    ( $( $id:ident => $e:expr ),*$(,)? ) => {
        $(
            make_macro! {
                $id => $e
            }
        )*
    }
}

使用示范:

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 中。

· · ·

感谢阅读。愿这份记录也能帮到你。

继续探索其他文章 ↗