Rust 实战丨通过实现 json! 掌握声明宏

在 Rust 编程语言中,宏是一种强大的工具,可以用于在编译时生成代码。json! 是一个在 Rust 中广泛使用的宏,它允许我们在 Rust 代码中方便地创建 JSON 数据。

声明宏(declarative macros)是 Rust 中的一种宏,它们使用 macro_rules! 关键字定义。

本文将参考《Rust 程序设计(第二版)》,通过实现 json! 宏,深入理解声明宏的工作原理。

结论先行

本文我们将构建一个 json! 宏,它支持我们以字符串 JSON 风格的语法来编写 Json 值。如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
let students = json![
{
"name": "Hedon Wang",
"class_of": 2022,
"major": "Software engineering"
},
{
"name": "Jun Lei",
"class_of": 1991,
"major": "Computor science"
}
]

完整代码

实现 json!

定义 Json enum

首先我们需要思考一下 Json 结构是什么样子的?主要是以下 3 种模式:

1
2
3
4
5
6
7
8
{
"name": "hedon",
"age": 18,
"school": {
"name": "Wuhan University",
"address": "Hubwi Wuhan"
}
}
1
2
3
4
5
6
7
8
[
{
"name": "hedon"
},
{
"name": "john"
}
]
1
null

为此我们定义一个 Json 结构的枚举:

1
2
3
4
5
6
7
8
9
#[derive(Clone, PartialEq, Debug)]
pub enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(HashMap<String, Json>),
}

你应该可以感到非常奇妙,使用一个这么简单的枚举,居然就可以表示所有的 Json 结构了。遗憾的是,现在这个结构编写 Json 值的语法相当冗长。

1
2
3
4
5
6
7
8
9
10
11
12
let people = Json::Object(HashMap::from([
("name".to_string(), Json::String("hedon".to_string())),
("age".to_string(), Json::Number(10.0)),
("is_student".to_string(), Json::Boolean(true)),
(
"detail".to_string(),
Json::Object(HashMap::from([
("address".to_string(), Json::String("beijing".to_string())),
("phone".to_string(), Json::String("1234567890".to_string()))
]))
)
]))

我们期望可以以下面这种方式来声明 Json 变量,这看起来就清爽许多了。

1
2
3
4
5
6
7
8
9
10
11
12
let students = json!([
{
"name": "Jim Blandy",
"class_of": 1926,
"major": "Tibetan throat singing"
},
{
"name": "Jason Orendorff",
"class_of": 1702,
"major": "Knots"
}
]);

猜想 json!

我们可以预见 Json 宏内部将会有多条规则,因为 JSON 数据有多种类型:对象、数组、数值等。事实上,我们可以合理地猜测每种 JSON 类型都将有一条规则:

1
2
3
4
5
6
7
8
macro_rules! json {
(null) => { Json::Null };
([ ... ]) => { Json::Array(...) };
({ ... }) => { Json::Object(...) };
(???) => { Json::Boolean(...) };
(???) => { Json::Number(...) };
(???) => { Json::String(...) };
}

然而这不太正确,因为宏模式无法区分最后 3 种情况,稍后我们会讨论如何处理。至于前 3 种情况,显然它们是以不同的语法标记开始的,所以这几种情况比较好处理。

实现 Null

我们先从最简单的 Null 分支开始,先编写如下测试用例:

1
2
3
4
5
6
7
8
9
10
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_null_json() {
let json = json!(null);
assert_eq!(json, Json::Null);
}
}

想要通过上述测试用例非常简单,我们只需要在 macro_rules! 支持中匹配这种情况即可:

1
2
3
4
5
6
#[macro_export]
macro_rules! json {
(null) => {
Json::Null
};
}
  • #[macro_export] 注解是 Rust 中的一个属性,用于指示这个宏应该被导出到调用者的作用域中,这样其他模块也可以使用它。
  • macro_rules! 宏定义了一个自定义的宏。在这里,它创建了一个名为 json 的宏,用于生成 JSON 数据。
  • 宏定义中 (null) 是匹配模式。这意味着当你调用 json! 宏并传递 null 作为参数时,将会触发这个规则。
  • => 符号用于指示匹配模式后的代码块。在这里,它指定了当匹配 (null) 时应该生成的代码块。
  • Json::Null 是一个 JSON 类型的枚举值,表示 JSON 中的 null 值。这个宏的目的是将传入的 null 转换为 Json::Null

实现 Boolean/Number/String

我们先准备如下测试用例:

1
2
3
4
5
6
7
8
9
10
11
#[test]
fn test_boolean_number_string_json() {
let json = json!(true);
assert_eq!(json, Json::Boolean(true));

let json = json!(1.0);
assert_eq!(json, Json::Number(1.0));

let json = json!("hello");
assert_eq!(json, Json::String("hello".to_string()));
}

通过观察分析,它们其实都是同一种模式:

Boolean/Number/String 分析

现在需要解决的问题就是,如何将这 3 种模式进行统一,这样在 macro_rules! 中才可以统一匹配模式并进行代码生成。

这里我们其实需要做的就是将 boolf64&str 转为对应的 Json 类型。那就需要用到标准库中的 From trait 了。

做法很简单,我们实现如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
impl From<bool> for Json {
fn from(value: bool) -> Self {
Json::Boolean(value)
}
}

impl From<&str> for Json {
fn from(value: &str) -> Self {
Json::String(value.to_string())
}
}

impl From<f64> for Json {
fn from(value: f64) -> Self {
Json::Number(value)
}
}

然后完善我们的 json!,目前的实现如下:

1
2
3
4
5
6
7
8
9
#[macro_export]
macro_rules! json {
(null) => {
Json::Null
};
($value: tt) => {
Json::from($value)
};
}

这里我们使用 $value作 为变量来承接匹配到的元素,其类型为 tt ,表示任意的语法标记树。具体可以参考:片段类型

这时运行上述测试用例,是没有问题的:

1
2
PASS [   0.004s] json-macro tests::test_boolean_number_string_json
PASS [ 0.004s] json-macro tests::test_null_json

美中不足的是,JSON 结构中的数字类型,其实不一定是 f64,也可以是 i32、u32、f32 或其他的数字类型,如果我们要为这全部的数字类型都实现到 Json 的 From trait,那就多冗余。

这个时候我们又可以实现一个宏,用于快速生成 impl From<T> for Json 。这个实现比较简单,本文就不赘述了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! impl_from_for_primitives {
( $( $type: ty ) * ) => {
$(
impl From<$type> for Json {
fn from(value: $type) -> Self {
Json::Number(value as f64)
}
}
)*
}
}

然后我们只需要用下面这一行代码,就可以为所有的数字类型实现 From trait 了:

1
impl_from_for_primitives!(u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 isize usize);

记得这个时候你要删除上面手动实现的 impl From<f64> for Json,不然会有 impl 冲突错误。

再次运行测试,也是可以通过的。

实现 Array

准备如下测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#[test]
fn test_array_json() {
let json = json!([1, null, "string", true]);
assert_eq!(
json,
Json::Array(vec![
Json::Number(1.0),
Json::Null,
Json::String("string".to_string()),
Json::Boolean(true)
])
)
}

要匹配 [1, null, "string", true]这个模式,笔者的分析过程如下:

  1. 首先是外面的两个中括号 []
  2. 再往里,是一个重复匹配的模式,以 , 分割,可以匹配 0 到任意多个元素,所以是 $( ,*) ,具体可以参考:重复模式
  3. 最里面就是第 2 步要匹配的元素了,我们先用 $element 作为变量来承接每一个元素,其类型为 tt ,表示任意的语法标记树。

分析完匹配的表达式后,我们就可以得到:

1
([ $( $element:tt ), * ]) => { /* TODO */ }

我们要生成的代码长这个样子:

1
2
3
4
5
6
Json::Array(vec![
Json::Number(1.0),
Json::Null,
Json::String("string".to_string()),
Json::Boolean(true)
])

其实就是一个 vec!,然后里面每个元素都是一个 Json,如此递归下去。

即可以得到代码生成部分的逻辑为:

1
Json::Array(vec![$(json!($element)),* ])
Json::Array 宏分析

综上,我们实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! json {
(null) => {
Json::Null
};
([ $( $element: tt),* ]) => {
Json::Array(vec![ $( json!($element)), * ])
};
($value: tt) => {
Json::from($value)
};
}

运行测试用例:

1
2
3
PASS [   0.003s] json-macro tests::test_null_json
PASS [ 0.003s] json-macro tests::test_boolean_number_string_json
PASS [ 0.004s] json-macro tests::test_array_json

实现 Object

写好如下测试用例,这次我们顺带把 Null、Boolean、Number 和 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
#[test]
fn test_object_json() {
let json = json!({
"null": null,
"name": "hedon",
"age": 10,
"is_student": true,
"detail": {
"address": "beijing",
"phone": "1234567890"
}
});
assert_eq!(
json,
Json::Object(HashMap::from([
("name".to_string(), Json::String("hedon".to_string())),
("age".to_string(), Json::Number(10.0)),
("is_student".to_string(), Json::Boolean(true)),
(
"detail".to_string(),
Json::Object(HashMap::from([
("address".to_string(), Json::String("beijing".to_string())),
("phone".to_string(), Json::String("1234567890".to_string()))
]))
)
]))
)
}

对比预期的 json! 宏内容和展开后的代码:

Json::Object 宏分析

完善我们的 macro_rules! json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[macro_export]
macro_rules! json {
(null) => {
Json::Null
};
([ $( $element: tt),* ]) => {
Json::Array(vec![ $( json!($element)), * ])
};
({ $( $key:tt : $value:tt ),* }) => {
Json::Object(HashMap::from([
$(
( $key.to_string(), json!($value) )
), *
]))
};
($value: tt) => {
Json::from($value)
};
}

运行测试用例:

1
2
3
4
PASS [   0.004s] json-macro tests::test_object_json
PASS [ 0.005s] json-macro tests::test_array_json
PASS [ 0.004s] json-macro tests::test_null_json
PASS [ 0.005s] json-macro tests::test_boolean_number_string_json

至此,我们就完成了 json! 宏的构建了!完整源码可见:完整代码

Peace! Enjoy coding~

附录

重复模式

实现 Array 中,我们匹配了这样一个模式:

1
([ $( $element:tt ), * ]) => { /* TODO */ }

其中 $($element:tt), *) 就是一个重复模式,其可以进一步抽象为 $( ... ),* ,表示匹配 0 次或多次,以 , 分隔。

Rust 支持以下全部重复模式:

模式 含义
$( … ) * 匹配 0 次或多次,没有分隔符
$( … ), * 匹配 0 次或多次,以逗号分隔
$( … ); * 匹配 0 次或多次,以分号分隔
$( … ) + 匹配 1 次或多次,没有分隔符
$( … ), + 匹配 1 次或多次,以逗号分隔
$( … ); + 匹配 1 次或多次,以分号分隔
$( … ) ? 匹配 0 次或 1 次,没有分隔符

即:

  • * 表示 0 次或多次
  • + 表示 1 次或多次
  • ? 表示 0 次或 1 次
  • 可在上述 3 者之前加入分隔符

片段类型

实现 Array 中,我们匹配了这样一个模式:

1
([ $( $element:tt ), * ]) => { /* TODO */ }

这里我们将 $element 指定为 tt,这个 tt 就是宏中的一种片段类型。

tt 能匹配单个语法标记树,包含:

  • 一对括号,如 (..)[..]、或 {..} ,以及位于其中的所有内容,包括嵌套的语法标记树。
  • 单独的非括号语法标记,比如 1926Knots

所以为了匹配任意类型的 Json ,我们选择了 tt 作为 $element 的片段类型。

macro_rules! 支持的片段类型如下所示:

片段类型 匹配(带例子) 后面可以跟 ······
expr 表达式:2 + 2, "udon", x.len() =>,;
stmt 表达式或声明,不包括任何尾随分号(很难用,请尝试使用 expr 或 block) =>,;
ty 类型:String, Vec, (&str, bool), dyn Read + Send =>,; =
path 路径:ferns, ::std::sync::mpsc =>,; =
pat 模式:_, Some(ref x) =>,=
item 语法项:struct Point { x: f64, y: f64 }, mod ferns; 任意
block 块:{ s += "ok"; true } 任意
meta 属性的主体:inline, derive(Copy, Clone), doc="3D models." 任意
literal 字面量值:1024, "Hello, world!", 1_000_000f64 任意
lifetime 生命周期:'a, 'item, 'static 任意
vis 可见性说明符:pub, pub(crate), pub(in module::submodule) 任意
ident 标识符:std, Json, longish_variable_name 任意
tt 语法标记树:;, >=, {}, [0 1 (+ 0 1)] 任意

完整代码

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq)]
#[allow(unused)]
enum Json {
Null,
Boolean(bool),
String(String),
Number(f64),
Array(Vec<Json>),
Object(HashMap<String, Json>),
}

impl From<bool> for Json {
fn from(value: bool) -> Self {
Json::Boolean(value)
}
}

impl From<&str> for Json {
fn from(value: &str) -> Self {
Json::String(value.to_string())
}
}

impl From<String> for Json {
fn from(value: String) -> Self {
Json::String(value)
}
}

#[macro_export]
macro_rules! impl_from_for_primitives {
( $( $type: ty ) * ) => {
$(
impl From<$type> for Json {
fn from(value: $type) -> Self {
Json::Number(value as f64)
}
}
)*
}
}

impl_from_for_primitives!(u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 isize usize);

#[macro_export]
macro_rules! json {
(null) => {
Json::Null
};
([ $( $element: tt),* ]) => {
Json::Array(vec![ $( json!($element)), * ])
};
({ $( $key:tt : $value:tt ),* }) => {
Json::Object(HashMap::from([
$(
( $key.to_string(), json!($value) )
), *
]))
};
($value: tt) => {
Json::from($value)
};
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_null_json() {
let json = json!(null);
assert_eq!(json, Json::Null);
}

#[test]
fn test_boolean_number_string_json() {
let json = json!(true);
assert_eq!(json, Json::Boolean(true));

let json = json!(1.0);
assert_eq!(json, Json::Number(1.0));

let json = json!("hello");
assert_eq!(json, Json::String("hello".to_string()));
}

#[test]
fn test_object_json() {
let json = json!({
"null": null,
"name": "hedon",
"age": 10,
"is_student": true,
"detail": {
"address": "beijing",
"phone": "1234567890"
}
});
assert_eq!(
json,
Json::Object(HashMap::from([
("null".to_string(), Json::Null),
("name".to_string(), Json::String("hedon".to_string())),
("age".to_string(), Json::Number(10.0)),
("is_student".to_string(), Json::Boolean(true)),
(
"detail".to_string(),
Json::Object(HashMap::from([
("address".to_string(), Json::String("beijing".to_string())),
("phone".to_string(), Json::String("1234567890".to_string()))
]))
)
]))
)
}

#[test]
fn test_array_json() {
let json = json!([1, null, "string", true]);
assert_eq!(
json,
Json::Array(vec![
Json::Number(1.0),
Json::Null,
Json::String("string".to_string()),
Json::Boolean(true)
])
)
}
}

Rust 实战丨通过实现 json! 掌握声明宏
https://hedon.top/2024/05/28/rust-action-macro-json/
Author
Hedon Wang
Posted on
2024-05-28
Licensed under