Rust 实战丨通过实现 json! 掌握声明宏
在 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_jsonPASS [ 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)]))}
}
相关文章:
Rust 实战丨通过实现 json! 掌握声明宏
在 Rust 编程语言中,宏是一种强大的工具,可以用于在编译时生成代码。json! 是一个在 Rust 中广泛使用的宏,它允许我们在 Rust 代码中方便地创建 JSON 数据。 声明宏(declarative macros)是 Rust 中的一种宏࿰…...
vue+elementUI实现在表格中添加输入框并校验的功能
背景: vue2elmui 需求: 需要在一个table中添加若干个输入框,并且在提交时需要添加校验 思路: 当需要校验的时候可以考虑添加form表单来触发校验,因此需要在table外面套一层form表单,表单的属性就是ref…...
为国产加油:“缺芯少屏”暂缓,另一领域,也要加把劲
说起咱中国之前的“缺芯少屏”,真的是让人挺闹心的。 不过呢,为了改变这个状况,咱们的工程师们可是费了不少劲儿,辛辛苦苦努力了数十年。现在好了,咱们也迎来了柔性屏的时代。 柔性屏 说起来,在触摸屏或者…...
【Qnx】Qnx coredump解析
Qnx coredump解析 coredump文件 Qnx运行的程序崩溃时,会生成coredump文件。 默认情况下这些文件默认会保存在/var/log/*.core 文件中。 解析coredump文件,可以帮忙加快分析程序崩溃的原因,比如了解崩溃的堆栈。 通常可以使用gdb和coreinfo…...
超级签名源码/超级签/ios分发/签名端本地linux服务器完成签名
该系统完全在linux下运行,不存在使用第三方收费工具,市面上很多系统都是使用的是第三方收费系统,例如:某心签名工具,某测侠等,不开源而且需要每年交费,这种系统只是在这些工具的基础上套了一层壳…...
RocketMQ在Centos7系统上单机部署
最近因为一些信创问题,要将RabbitMQ替换为RocketMQ,因此在此分享一些RocketMQ在Centos7系统上单机部署相关过程。 优缺点 RocketMQ的优点: 性能优越:RocketMQ在处理大量消息时,性能优于RabbitMQ。当面临每秒数万到数…...
Vue37-非单文件组件
一、组件的两种编写形式: 非单文件组件;单文件组件。 二、创建一个组件 2-1、组件中的el 组件中不写el,不说为谁服务。 2-2、组件中的data 因为对象形式,多处复用的话,有引用关系,改一处,另一…...
CSS实现经典打字小游戏《生死时速》
🌻 前言 CSS 中有这样一个模块:Motion Path 运动模块,它可以使元素按照自定义的路径进行移动。本文将为你讲解这个模块属性的使用,并且利用它实现我小时候电脑课经常玩的一个打字游戏:金山打字的《生死时速》。 &…...
推箱子-小游戏
学习目标: 巩固Java基础,数据类型、二维数组、条件语句等; 效果展示:...
AI数字人的开源解决方案
目前,国内外已经涌现出一些优秀的数字人开源解决方案,这些解决方案为开发者提供了构建数字人应用的工具和基础设施。以下是一些比较知名的数字人开源解决方案。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎交流合作。 1…...
java写一个验证码
生成验证码 内容:可以是小写字母,也可以是大写字母,还可以是数字 规则 长度为5 内容中是四位字母,1位数字。 其中数字只有1位,但是可以出现在任意的位置。 package User;import java.util.ArrayList; import jav…...
【星海随笔】ELK优化
ELS 再遇到大的日志文件的时候不会自动进行清理的,我们可以通过 logrotate 转储工具进行操作。 该命令是基于 Cron 实现,由系统执行,当然也可以手动进行执行例如 logrotate -f configfile# more /etc/logrotate.confweekly // 默认每一周执行一次rotate轮转工作 r…...
SQL Auto Increment
SQL Auto Increment 在关系型数据库中,自动增量(Auto Increment)是一个常见且实用的特性。它允许数据库自动为表中插入的新行分配唯一的标识符,通常用于主键字段。本文将深入探讨SQL中的自动增量功能,包括其工作原理、…...
网络安全练气篇——PHP编程语言基础
目录 PHP基础 一、PHP简介与环境搭建 什么是PHP? PHP环境安装 代码编辑选择 二、基本语法 PHP基本语法操作 PHP变量与输出 啥是常量? PHP注释 PHP单引号双引号声明 三、PHP表单 PHP表单 四、登录界面搭建与讲解 构建登陆页面 登陆页面端 服务器端…...
LabVIEW程序内存泄漏分析与解决方案
维护他人编写的LabVIEW程序时,若发现程序运行时间越长,占用内存越大直至崩溃,通常是内存泄漏导致的。本文从多角度分析内存泄漏的可能原因,包括数组和字符串处理、未释放的资源、循环中的对象创建等,并提供具体的解决方…...
JVM垃圾回收器介绍
Serial GC: 算法: 使用的是标记-清除算法。特点: 串行执行,适用于单CPU环境或较小的堆内存配置。在新生代和老年代的回收中都是单线程执行,因此在进行垃圾回收时会暂停所有应用线程(Stop-The-World)。 Parallel GC (也称为吞吐量优…...
subprocess隐藏cmd窗口
process subprocess.Popen(command, shellTrue, stdoutsubprocess.PIPE, stderrsubprocess.PIPE,creationflagssubprocess.CREATE_NO_WINDOW) 添加参数即可不显示cmd运行窗口 creationflagssubprocess.CREATE_NO_WINDOW...
编程前端看什么书比较好:深入解析与推荐
编程前端看什么书比较好:深入解析与推荐 在编程前端的学习道路上,书籍无疑是我们最宝贵的财富。一本好的书籍,不仅可以提供系统的知识体系,还能引导我们深入探索技术的奥秘。然而,面对市面上琳琅满目的前端书籍&#…...
HarmonyOS(36) DevEco Studio 配置debug和release
在android开发中可以在build.gradle来配置realease和debug,在HarmonyOS中可以通过build-profile.json5文件中通过buildModeSet配置: 在DevEco Studio 中可以通过下面来选择运行debug还是release: 我们可以通过BuildProfile.ets里面的静态变量获取当前…...
PHP CGI Windows平台远程代码执行漏洞(CVE-2024-4577)复现
PHP语言在设计时忽略了Windows对字符编码转换的 Best-Fit 特性,导致未授权的攻击者可以通过特定字符串绕过 CVE-2012-1823 补丁,执行任意PHP代码,导致服务器失陷。 1.漏洞级别 高危 2.漏洞搜索 fofa: app"XAMPP"3.影响范围 P…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...
【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...
