rust宏魔法(1)
Rust宏魔法——第一辑(不知道后面还会不会更新,总之这是个系列)
注:本文默认你已经有一定的Rust基础
rust的宏的强大相信大家肯定听的不少,本篇就简单说说rust的过程宏
rust的宏有好几种,有基于模式匹配的,还有可以直接对语法树动手的,过程宏就是可以修改语法树,可以在编译器派生代码:比如有个test crate,然后它依赖于test_macro crate,而后者是一个proc-macro库,也就是里面定义了过程宏,编译时就会先编译test_macro然后用test_macro的对test的语法解析流修改再丢给编译器,所以可以实现很多非常抽象的操作。
写本文的契机是,前段时间在Rust中文社区群里看到有人问能不能用宏把json生成一个enum(json只有一层),这个需求和过程宏还是很契合的,所以就简单实现了一下,以下内容就是对实现的讲解
首先开个crate吧
cargo new json_to_enum
直到撰写本文时,想要定义过程宏还是必须要单独开个crate,并且在
cargo.toml里面设置lib.proc-macro = true至于原因是rust的编译单元是crate,而前面也说要拿过程宏的库编译好对其他库使用的,所以就不可避免要分割,单独开个crate。
然后创建过程宏库,这里我一般喜欢把依赖的其他自己写的库放在src下
cd src
cargo new --lib json_to_enum_macro
cd ..
然后创建一个json文件用于测试,内容比如:
// test.json
{
"app_name_not_set": "应用名称未设置",
"app_name_too_long": "应用名称过长",
"app_name_invalid": "应用名称无效",
"app_name_already_exists": "应用名称已存在"
}
现在的文件结构应该是:
.
├─src
│ ├─main.rs
│ └─json_to_enum_macro
│ └─src
│ └─lib.rs
└─test.json
emm,先展示一下调用的效果吧
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);
}
}
// output
/*
AppNameAlreadyExists "应用名称已存在"
AppNameInvalid "应用名称无效"
AppNameNotSet "应用名称未设置"
AppNameTooLong "应用名称过长"
*/
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库
cargo add quote syn serde_json
现在文件应该是这样
[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的依赖
[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文件导入依赖
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
#[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")]
// 由于可以有很多个attribute,所以这里返回的是一个Vec
let attribute = &input.attrs;
// 解析出attr里提供的json文件路径
let json_name = attribute
.iter()
.find_map(|attr| {
if attr.path().is_ident("from_json") {
// 尝试把attr解析成字面量(literal)
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了
let name = &input.ident;
let variants = json
.as_object()
.expect("json should be object")
.keys()
.map(|k| {
// json里面是snake命名,转成驼峰式命名
let ident = Ident::new(&snake_to_camel(k), Span::call_site().into());
quote! {
#ident,
}
});
这时候就可以像这样派生一个enum
是不是感觉和写macro_rules!很像呢
quote! {
pub enum #name {
*(#variants)*
}
}
然后还要实现一下Display,需求说的是要json字段对应的值(也就是那些中文),这也显然只要获取json的values即可
let msgs = json.as_object().unwrap().values().map(|v| {
let msg = v.as_str().expect("value should be string");
quote! {
#msg
}
});
后面实现Display,(我也顺便实现了Debug)也就跟上面一样
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)),
)*
}
}
}
到这里就已经完成了,如果你不知道后面这部分怎么弄就好好理解一下前面的代码,我故意没有全都写出来()
实现完后记得测试一下
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(), "应用名称未设置");
}
能正常跑就基本没啥问题了,还担心就展开看看
cargo expand
展开结果:
#![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,
);
}
}
};
}