rust宏魔法(1)

Rust宏魔法——第一辑(不知道后面还会不会更新,总之这是个系列)

注:本文默认你已经有一定的Rust基础

rust的宏的强大相信大家肯定听的不少,本篇就简单说说rust的过程宏

rust的宏有好几种,有基于模式匹配的,还有可以直接对语法树动手的,过程宏就是可以修改语法树,可以在编译器派生代码:比如有个test crate,然后它依赖于test_macro crate,而后者是一个proc-macro库,也就是里面定义了过程宏,编译时就会先编译test_macro然后用test_macro的对test的语法解析流修改再丢给编译器,所以可以实现很多非常抽象的操作。

写本文的契机是,前段时间在Rust中文社区群里看到有人问能不能用宏把json生成一个enum(json只有一层),这个需求和过程宏还是很契合的,所以就简单实现了一下,以下内容就是对实现的讲解

首先开个crate吧

1
cargo new json_to_enum

直到撰写本文时,想要定义过程宏还是必须要单独开个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
// test.json
{
"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);
}
}
// 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库

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
// 由于可以有很多个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了

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| {
// json里面是snake命名,转成驼峰式命名
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
cargo expand

展开结果:

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,
);
}
}
};
}

Welcome to my other publishing channels