Rust宏魔法——第一辑(不知道后面还会不会更新,总之这是个系列)
注:本文默认你已经有一定的Rust基础
rust的宏的强大相信大家肯定听的不少,本篇就简单说说rust的过程宏
rust的宏有好几种,有基于模式匹配的,还有可以直接对语法树动手的,过程宏就是可以修改语法树,可以在编译器派生代码:比如有个test crate,然后它依赖于test_macro crate,而后者是一个proc-macro库,也就是里面定义了过程宏,编译时就会先编译test_macro然后用test_macro的对test的语法解析流修改再丢给编译器,所以可以实现很多非常抽象的操作。
写本文的契机是,前段时间在Rust中文社区群里看到有人问能不能用宏把json生成一个enum(json只有一层),这个需求和过程宏还是很契合的,所以就简单实现了一下,以下内容就是对实现的讲解
首先开个crate吧
直到撰写本文时,想要定义过程宏还是必须要单独开个crate,并且在cargo.toml
里面设置 lib.proc-macro = true
至于原因是rust的编译单元是crate,而前面也说要拿过程宏的库编译好对其他库使用的,所以就不可避免要分割,单独开个crate。
然后创建过程宏库,这里我一般喜欢把依赖的其他自己写的库放在src下
1 2 3
| cd src cargo new --lib json_to_enum_macro cd ..
|
然后创建一个json文件用于测试,内容比如:
1 2 3 4 5 6 7
| { "app_name_not_set": "应用名称未设置", "app_name_too_long": "应用名称过长", "app_name_invalid": "应用名称无效", "app_name_already_exists": "应用名称已存在" }
|
现在的文件结构应该是:
1 2 3 4 5 6 7
| . ├─src │ ├─main.rs │ └─json_to_enum_macro │ └─src │ └─lib.rs └─test.json
|
emm,先展示一下调用的效果吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| use json_to_enum_macro::json_to_enum;
json_to_enum! { #[from_json("test.json")] pub enum Test {} }
fn main() { for i in 0..4 { let t = unsafe { std::mem::transmute::<u8, Test>(i) }; println!("{:?} {}", t, t); } }
|
json_to_enum
宏里面的匹配规则是一看到这个需求时的想法,所以别问我为什么要这样写()
里面塞一个空的enum也是方便指定这个解析出的enum的名字而已,最终的效果是会替换掉enum的内容,所以里面写什么内容都没有用
好了,现在开始正式编写!
开始编写你的proc-macro
先在json_to_enum_macro的cargo.toml
里面设置 lib.proc-macro = true
然后导入proc-macro的依赖 quote,syn,以及用于解析json文件的serde_json库
1
| cargo add quote syn serde_json
|
现在文件应该是这样
1 2 3 4 5 6 7 8 9 10 11 12
| [package] name = "json_to_enum_macro" version = "0.1.0" edition = "2021"
[lib] proc-macro = true
[dependencies] quote = "1.0.36" serde_json = "1.0.116" syn = "2.0.59"
|
把这个库添加到json_to_enum的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [workspace] members = [ "src/json_to_enum_macro" ]
[package] name = "json_to_enum" version = "0.1.0" edition = "2021"
[dependencies]
[dependencies.json_to_enum_macro] path = "src/json_to_enum_macro"
|
接下来rust-analyser就会一起解析你的macro库了
打开lib.rs
文件导入依赖
1 2 3 4
| extern crate proc_macro; use proc_macro::{Span, TokenStream}; use quote::quote; use syn::{parse_macro_input, DeriveInput, Ident, Lit};
|
一般来说基本都会用上这几个:proc_macro::TokenStream, quote::quote, syn::{parse_macro_input, DeriveInput}
proc macro也有好几种,比如 proc_macro_derive
可以给enum,struct啥的实现某个特征, 使用方法比如: #[derive(Debug)] struct DebugStruct;
, 比如 proc_macro_attribute
可以扩展函数,enum字段等的功能。
而这里由于我们连enum都要派生,上面说的两者都要依托于某个对象,所以我们这里也就采用proc_macro
1 2 3 4 5 6 7 8 9
| #[proc_macro] pub fn json_to_enum(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let gen = quote! { }; gen.into() }
|
我们要先解析一个attribute,得到json文件的路径,比如这个attribute是 #[from_json("test.json")]
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
| let attribute = &input.attrs;
let json_name = attribute .iter() .find_map(|attr| { if attr.path().is_ident("from_json") { let lit = match attr.parse_args::<Lit>() { Ok(lit) => lit, Err(e) => panic!("{:?}", e), }; if let Lit::Str(lit) = lit { Some(lit.value()) } else { None } } else { None } }) .unwrap();
let json_content = std::fs::read_to_string(json_name).expect("file not found");
let json: serde_json::Value = serde_json::from_str(&json_content).expect("json parse error");
|
然后获取你传入的enum的名字(标识符),和json的keys就可以派生enum了
1 2 3 4 5 6 7 8 9 10 11 12
| let name = &input.ident; let variants = json .as_object() .expect("json should be object") .keys() .map(|k| { let ident = Ident::new(&snake_to_camel(k), Span::call_site().into()); quote! { #ident, } });
|
这时候就可以像这样派生一个enum
是不是感觉和写macro_rules!
很像呢
1 2 3 4 5
| quote! { pub enum #name { *(#variants)* } }
|
然后还要实现一下Display,需求说的是要json字段对应的值(也就是那些中文),这也显然只要获取json的values即可
1 2 3 4 5 6
| let msgs = json.as_object().unwrap().values().map(|v| { let msg = v.as_str().expect("value should be string"); quote! { #msg } });
|
后面实现Display,(我也顺便实现了Debug)也就跟上面一样
1 2 3 4 5 6 7 8 9
| impl std::fmt::Display for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { #( #name::#keys => write!(f, "{}", stringify!(#msgs)), )* } } }
|
到这里就已经完成了,如果你不知道后面这部分怎么弄就好好理解一下前面的代码,我故意没有全都写出来()
实现完后记得测试一下
1 2 3 4 5 6 7 8 9 10 11
| use json_to_enum_macro::json_to_enum;
json_to_enum! { #[from_json("test.json")] pub enum Test {} }
fn main() { let a = Test::AppNameNotSet; assert_eq!(a.to_string(), "应用名称未设置"); }
|
能正常跑就基本没啥问题了,还担心就展开看看
展开结果:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; use parser_macro::json_to_enum; pub enum Test { AppNameAlreadyExists, AppNameInvalid, AppNameNotSet, AppNameTooLong, } impl std::fmt::Display for Test { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Test::AppNameAlreadyExists => { f.write_fmt( format_args!( "{0}", "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{5df2}\u{5b58}\u{5728}\"", ), ) } Test::AppNameInvalid => { f.write_fmt( format_args!( "{0}", "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{65e0}\u{6548}\"", ), ) } Test::AppNameNotSet => { f.write_fmt( format_args!( "{0}", "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{672a}\u{8bbe}\u{7f6e}\"", ), ) } Test::AppNameTooLong => { f.write_fmt( format_args!( "{0}", "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{8fc7}\u{957f}\"", ), ) } } } } impl std::fmt::Debug for Test { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Test::AppNameAlreadyExists => { f.write_fmt(format_args!("{0}", "AppNameAlreadyExists")) } Test::AppNameInvalid => f.write_fmt(format_args!("{0}", "AppNameInvalid")), Test::AppNameNotSet => f.write_fmt(format_args!("{0}", "AppNameNotSet")), Test::AppNameTooLong => f.write_fmt(format_args!("{0}", "AppNameTooLong")), } } } fn main() { let a = Test::AppNameNotSet; match (&a.to_string(), &"应用名称未设置") { (left_val, right_val) => { if !(*left_val == *right_val) { let kind = ::core::panicking::AssertKind::Eq; ::core::panicking::assert_failed( kind, &*left_val, &*right_val, ::core::option::Option::None, ); } } }; }
|