Rust宏魔法——第二辑(
又来整点奇技淫巧了
这期就玩玩用声明宏来生成宏(
总结可以直接看 👉 省流
假设有以下的奇怪情景:
你整了一个日志系统,但是其中的输出内容是预设好的(当然也应该预留一个比如 Other 的来自定义内容)
然后你想要给每一种输出内容都注册一个宏,并且有些输出是带有 “{}” 的,因此你希望能够有类似下面这个效果:
也就是像 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
中。