从零开始的 JSON 库教程
1.启程
规范 :
宏名:宏名唯一,通常以_H__作为后缀,命名结构:
项目名称_目录_文件名称_H__
typedef
在定义变量时使用,如:struct、enum 的定义。C语言没有命名空间(namespace)功能,一般使用项目的简写作为标识符前缀。通常枚举值用大写(如
LEPT_NULL
),而类型及函数则用小写 (如lept_type
)使用CMake这款软件配置工具来实现跨平台开发。代码使用Visual Studio实现。
通过断言(assertion)防御式编程方式,减少编程错误。
利用宏的编写技巧:反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用
do { /*...*/ } while(0)
包裹成单个语句。单元测试使用测试驱动开发(test-driven development, TDD):先写测试,再实现功能
1.加入一个测试。
2.运行所有测试,新的测试应该会失败。
3.编写实现代码。
4.运行所有测试,若有测试失败回到3。
5.重构代码。
6.回到 1。
JSON 语法子集,使用 RFC7159 中的 ABNF 表示:
1
2
3
4
5
6JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = null / false / true
null = "null"
false = "false"
true = "true"
需求:
- 把JSON文本解析为一个树状数据结构(parse)
- 提供接口访问该数据结构(access)
- 把数据结构转换成JSON文本(stringify)
结构:
leptjson.h
:leptjson的头文件(header file),含有对外的类型和API函数声明。leptjson.c
:leptjson的实现文件(implementation file),含有内部的类型声明和函数实现。此文件会编译成库。test.c
:我们使用测试驱动开发(test driven development,TDD)。此文件包含测试程序,需要链接leptjson库。
实现:
搭建编译环境
CMake,下载后使用其cmake-gui程序:
1.Where is the source code
选择json-tutorial/tutorial01
2.
Where to build the binary
键入上一个目录加上/build
。3.按 Configure,选择编译器
4.再按Generate便会生成 Visual Studio 的 .sln 和 .vcproj 等文件。此处的build目录都是生成文件,可以随时删除,也不要上传到仓库。
头文件与API设计
1.宏名参见规范
2.JSON中有6种数据类型,如果把true和false当作两个类型就是七种,为此声明一个枚举类型:
1
typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;
3.接着声明JSON的数据结构:树形结构。每个节点使用
lept_value
结构体表示,我们会称它为一个JSON值(JSON value)。此单元只实现null,true和flase的解析,因此结构体只需要存储一个lept_type
。之后单元逐步加入其他数据。1
2
3typedef struct {
lept_type type;
}lept_value;4.两个API函数
1
2
3
4/*解析JSON*/
int lept_parse(lept_value* v, const char* json);
/*访问结果的函数*/
lept_type lept_get_type(const lept_value* v);对于
lept_parse()
返回值是以下这些枚举值,无错误会返回LEPT_PARSE_OK
,其他值在下节解释。1
2
3
4
5
6enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
};JSON语法子集
1
2
3
4
5
6JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = null / false / true
null = "null"
false = "false"
true = "true"当中
%xhh
表示以 16 进制表示的字符,/
是多选一,*
是零或多个,()
用于分组。1.第一行:JSON文本由三部分组成,首先为空白ws(whitespace),接着是一个值,最后是空白。
2.第二行:所谓空白是由零个或者多个空格符(space U+0020)、制表符(tab U+0009)、换行符(LF U+000A)、回车符(CR U+000D)所组成。
3.第三行:value只可以为null、false或true,它们分别由对应的自变量(literal)。
我们的解释器应能判断输入是否为一个合法的JSON不符合这个语法,我们要对应的错误码,方便使用者追查问题。
在这个JSON语法子集下,我们定义3种错误码:
- 若一个JSON只含有空白,传回
LEPT_PARSE_EXPECT_VALUE
。 - 若一个值之后,在空白之后还有其它字符,传回
LEPT_PARSE_ROOT_NOT_SINGULAR
。 - 若值不是那三种字面量,传回
LEPT_PARSE_INVALID_VALUE
。
- 若一个JSON只含有空白,传回
单元测试
在复杂项目中一般不用
printf/count
来判断结果是否符合预期,相应的,我们一般会使用自动的测试方法,例如:单元测试(unit testing)。单元测试也能确保其他人修改代码之后,原来的功能维持正确(回归测试/regression testing)。我们为了简单起见,会编写一个极简单的单元测试方式。
两种软件开发方法论:
1.周期性开发:加入一个功能,再写关于该功能的单元测试。
2.测试驱动开发(test-driven development,TDD),它的主要循环步骤是:
1
2
3
4
5
6
7
8/*
a.加入一个测试。
b.运行所有测试,新的测试应该会失败。
c.编写实现代码。
d.运行所有测试,若有测试失败回到c。
e.重构代码。
f.回到 a。
*/TDD先写测试,在进行实现。能够刚好满足测试,避免不必要的代码,或者是没有被测试的代码。
test.c
中的单元测试框架如下: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
static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;
static void test_parse_null() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));// 解析成功返回LEPT_PARSE_OK
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));// v的type为LEPT_TRUE两者不相等,报错
}
/* ... */
static void test_parse() {
test_parse_null();
/* ... */
}
int main() {
test_parse();
printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
return main_ret;
}宏的编写技巧
反斜杠表示该行未结束,会串接下一行。如果宏内有多过一个语句(statement),就需要用
do{/*...*/}
while(0)
包裹成单个语句,否则会有如下的问题:1
2
3
4
5
6
7
8
9
10
11
12
if (cond)
M();
else
c();
/* 预处理后 */
if (cond)
a(); b(); /* b(); 在 if 之外 */
else /* <- else 缺乏对应 if */
c();只用
{ }
也不行1
2
3
4
5
6
7
8
/* 预处理后 */
if (cond)
{ a(); b(); }; /* 最后的分号代表 if 语句结束 */
else /* else 缺乏对应 if */
c();用 do while 就行了:
1
2
3
4
5
6
7
8
/* 预处理后 */
if (cond)
do { a(); b(); } while(0);
else
c();要让实现语句的整个操作只存在一个
;
(分号)。实现解析器
在实现API设计、单元测试后,就到了解析器的实现。
为了避免函数新参多参数传递,我把这些数据都放在了一个
lept_context
结构体内。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16typedef struct {
const char* json;
}lept_context;
/* ... */
/* 提示:这里应该是 JSON-text = ws value ws */
/* 以下实现没处理最后的 ws 和 LEPT_PARSE_ROOT_NOT_SINGULAR */
int lept_parse(lept_value* v, const char* json) {
lept_context c;
assert(v != NULL);
c.json = json;
v->type = LEPT_NULL;
lept_parse_whitespace(&c);
return lept_parse_value(&c, v);
}我暂时只存储json当前位置,之后加入更多内容。
若
lept_json
失败,会把v设置成null
类型,所以先把它设为null
,让lept_prase_value
写入解析出来的根值。leptjson是一个手写的递归下降解析器(recursive descent parser)。因其语法特别简单我们不需要写分词器(tokenizer),只需要检测下一个字符,就可以知道它是那种类型的值,然后调用相关的分析函数。对于完整的JSON语法,跳过空白后,只需要检测当前字符:
- n ➔ null
- t ➔ true
- f ➔ false
- “ ➔ string
- 0-9/- ➔ number
- [ ➔ array
- { ➔ object
按照JSON语法异界的EBNF简单翻译成解析函数:
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
/* ws = *(%x20 / %x09 / %x0A / %x0D) */
static void lept_parse_whitespace(lept_context* c) {
const char *p = c->json;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
c->json = p;
}
/* null = "null" */
static int lept_parse_null(lept_context* c, lept_value* v) {
EXPECT(c, 'n');
if (c->json[0] != 'u' || c->json[1] != 'l' || c->json[2] != 'l')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_NULL;
return LEPT_PARSE_OK;
}
/* value = null / false / true */
/* 提示:下面代码没处理 false / true,将会是练习之一 */
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 'n': return lept_parse_null(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}lept_parse_whitespace()
不会出现错误,返回类型为void
。其它的解析函数会返回错误码,传递至顶层。关于断言
C语言标准库中含有
assert()
的宏定义,(在<assert.h>中),提供断言功能。当程序以 release 配置编译时(定义了NDEBUG
宏),assert()
不会做检测;而当在 debug 配置时(没定义NDEBUG
宏),则会在运行时检测assert(cond)
中的条件是否为真(非 0),断言失败会直接令程序崩溃。断言环境:错误是由于程序员错误编码所造成的(例如传入不合法的参数)。
运行时错误环境:错误是程序员无法避免,是由运行时的环境造成的(例如开启文件失败)。
总结与练习
总结
本文介绍了如何配置一个编程环境,单元测试的重要性,以至一个JSON解释器的子集实现。
练习
1.修正关于
LEPT_PARSE_ROOT_NOT_SINGULAR
的单元测试,若 json 在一个值之后,空白之后还有其它字符,则要返回LEPT_PARSE_ROOT_NOT_SINGULAR
。1
2
3
4
5
6
7
8
9
10
11//test.c 简单的单元测试
static void test_parse_root_not_singular() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_ROOT_NOT_SINGULAR, lept_parse(&v, "null x"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
static void test_parse() {
test_parse_root_not_singular();
}在调用
lept_parse(&v, "null x")
之后v的类型会被写入解析出来的根值LEPT_NULL
。并且要注意的是,第四行初始定义的v的type不能和第六行代码要检测的type(上述为LEPT_NULL
)相同,否则该单元测试就没有意义了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 'n': return lept_parse_null(c, v);
case 'f': return lept_parse_false(c, v);
case 't': return lept_parse_true(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}
int lept_parse(lept_value* v, const char* json) {
lept_context c;
int ret;
assert(v != NULL);
c.json = json;
v->type = LEPT_NULL;
lept_parse_whitespace(&c);
if ((ret = lept_parse_value(&c,v)) == LEPT_PARSE_OK){
lept_parse_whitespace(&c);
if(*c.json != '\0')
ret = LEPT_PARSE_ROOT_NOT_SINGULAR;
}
return ret;
}回看对
LEPT_PARSE_ROOT_NOT_SINGULAR
的解析,因为它是唯一一个要对进行两次lept_parse_whitespace(&c);
的情况,所以通过一个if
语句来判断两次解析空白字符之后,指针所停靠的位置是否是\0
,不是,则代表空白之后还有其它字符。2.参考
test_parse_null()
,加入test_parse_true()
、test_parse_false()
单元测试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static void test_parse_true() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "true"));
EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(&v));
}
static void test_parse_false() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "false"));
EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(&v));
}
static void test_parse() {
test_parse_true();
test_parse_false();
}相应的,根据
test_parse_null()
照搬照抄即可。3.参考
lept_parse_null()
的实现和调用方,解析 true 和 false 值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static int lept_parse_true(lept_context* c, lept_value* v) {
EXPECT(c, 't');
if (c->json[0] != 'r' || c->json[1] != 'u' || c->json[2] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_TRUE;
return LEPT_PARSE_OK;
}
static int lept_parse_false(lept_context* c, lept_value* v) {
EXPECT(c, 'f');
if (c->json[0] != 'a' || c->json[1] != 'l' || c->json[2] != 's' || c->json[3] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 4;
v->type = LEPT_FALSE;
return LEPT_PARSE_OK;
}此处相对来说简单很多,唯一要注意的是
json
最后停靠的地址位置,是在检测类型之后的第一个空白字符(也可为\0
或异常情况)的位置。常见问答
1.为什么把例子命名为 leptjson?
来自于标准模型中的轻子(lepton),意为很轻量的 JSON 库。另外,建议大家为项目命名时,先 google 一下是否够独特,有很多同名的话搜寻时难以命中。
2.为什么使用宏而不用函数或内联函数?
因为这个测试框架使用了
__LINE__
这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。另外,内联函数是 C99 的新增功能,本教程使用 C89。
2.解析数字
规范:
需求:
结构:
实现:
持续更新…