在 Rust 编程语言中,宏是一种强大的工具,可以用于在编译时生成代码。json!
是一个在 Rust 中广泛使用的宏,它允许我们在 Rust 代码中方便地创建 JSON 数据。
声明宏(declarative macros)是 Rust 中的一种宏,它们使用 macro_rules!
关键字定义。
本文将参考《Rust 程序设计(第二版)》,通过实现 json!
宏,深入理解声明宏的工作原理。
结论先行
本文我们将构建一个 json!
宏,它支持我们以字符串 JSON 风格的语法来编写 Json 值。如下面这个例子:
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 种模式:
{ "name": "hedon", "age": 18, "school": { "name": "Wuhan University", "address": "Hubwi Wuhan" } }
复制
[ { "name": "hedon" }, { "name": "john" } ]
复制
null
复制
为此我们定义一个 Json 结构的枚举:
#[derive(Clone, PartialEq, Debug)] pub enum Json { Null, Boolean(bool), Number(f64), String(String), Array(Vec<Json>), Object(HashMap<String, Json>), }
复制
你应该可以感到非常奇妙,使用一个这么简单的枚举,居然就可以表示所有的 Json 结构了。遗憾的是,现在这个结构编写 Json 值的语法相当冗长。
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 变量,这看起来就清爽许多了。
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 类型都将有一条规则:
macro_rules! json { (null) => { Json::Null }; ([ ... ]) => { Json::Array(...) }; ({ ... }) => { Json::Object(...) }; (???) => { Json::Boolean(...) }; (???) => { Json::Number(...) }; (???) => { Json::String(...) }; }
复制
然而这不太正确,因为宏模式无法区分最后 3 种情况,稍后我们会讨论如何处理。至于前 3 种情况,显然它们是以不同的语法标记开始的,所以这几种情况比较好处理。
实现 Null
我们先从最简单的 Null
分支开始,先编写如下测试用例:
#[cfg(test)] mod tests { use super::*; #[test] fn test_null_json() { let json = json!(null); assert_eq!(json, Json::Null); } }
复制
想要通过上述测试用例非常简单,我们只需要在 macro_rules!
支持中匹配这种情况即可:
#[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
我们先准备如下测试用例:
#[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())); }
复制
通过观察分析,它们其实都是同一种模式:
现在需要解决的问题就是,如何将这 3 种模式进行统一,这样在 macro_rules!
中才可以统一匹配模式并进行代码生成。
这里我们其实需要做的就是将 bool
、f64
和 &str
转为对应的 Json
类型。那就需要用到标准库中的 From
trait 了。
做法很简单,我们实现如下代码:
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!
,目前的实现如下:
#[macro_export] macro_rules! json { (null) => { Json::Null }; ($value: tt) => { Json::from($value) }; }
复制
这里我们使用 $value
作 为变量来承接匹配到的元素,其类型为 tt
,表示任意的语法标记树。具体可以参考:片段类型。
这时运行上述测试用例,是没有问题的:
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
。这个实现比较简单,本文就不赘述了,代码如下:
#[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 了:
impl_from_for_primitives!(u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 isize usize);
复制
记得这个时候你要删除上面手动实现的 impl From<f64> for Json
,不然会有 impl 冲突错误。
再次运行测试,也是可以通过的。
实现 Array
准备如下测试用例:
#[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]
这个模式,笔者的分析过程如下:
- 首先是外面的两个中括号
[
和]
; - 再往里,是一个重复匹配的模式,以
,
分割,可以匹配 0 到任意多个元素,所以是$( ,*)
,具体可以参考:重复模式; - 最里面就是第 2 步要匹配的元素了,我们先用
$element
作为变量来承接每一个元素,其类型为tt
,表示任意的语法标记树。
分析完匹配的表达式后,我们就可以得到:
([ $( $element:tt ), * ]) => { /* TODO */ }
复制
我们要生成的代码长这个样子:
Json::Array(vec![ Json::Number(1.0), Json::Null, Json::String("string".to_string()), Json::Boolean(true) ])
复制
其实就是一个 vec!
,然后里面每个元素都是一个 Json
,如此递归下去。
即可以得到代码生成部分的逻辑为:
Json::Array(vec![$(json!($element)),* ])
复制
综上,我们实现的代码如下:
#[macro_export] macro_rules! json { (null) => { Json::Null }; ([ $( $element: tt),* ]) => { Json::Array(vec![ $( json!($element)), * ]) }; ($value: tt) => { Json::from($value) }; }
复制
运行测试用例:
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 带上了:
#[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!
宏内容和展开后的代码:
完善我们的 macro_rules! json
:
#[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) }; }
复制
运行测试用例:
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 中,我们匹配了这样一个模式:
([ $( $element:tt ), * ]) => { /* TODO */ }
复制
其中 $($element:tt), *)
就是一个重复模式,其可以进一步抽象为 $( ... ),*
,表示匹配 0 次或多次,以 ,
分隔。
Rust 支持以下全部重复模式:
模式 | 含义 |
---|---|
$( … ) * | 匹配 0 次或多次,没有分隔符 |
$( … ), * | 匹配 0 次或多次,以逗号分隔 |
$( … ); * | 匹配 0 次或多次,以分号分隔 |
$( … ) + | 匹配 1 次或多次,没有分隔符 |
$( … ), + | 匹配 1 次或多次,以逗号分隔 |
$( … ); + | 匹配 1 次或多次,以分号分隔 |
$( … ) ? | 匹配 0 次或 1 次,没有分隔符 |
即:
*
表示 0 次或多次+
表示 1 次或多次?
表示 0 次或 1 次- 可在上述 3 者之前加入分隔符
片段类型
在 实现 Array 中,我们匹配了这样一个模式:
([ $( $element:tt ), * ]) => { /* TODO */ }
复制
这里我们将 $element
指定为 tt
,这个 tt
就是宏中的一种片段类型。
tt
能匹配单个语法标记树,包含:
- 一对括号,如
(..)
、[..]
、或{..}
,以及位于其中的所有内容,包括嵌套的语法标记树。 - 单独的非括号语法标记,比如
1926
或Knots
。
所以为了匹配任意类型的 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\n”; 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)] | 任意 |
完整代码
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) ]) ) } }
复制