跟着github上的项目json-tutorial完成一个json解析器。本文对应该项目的第一单元。

👉原项目地址

JSON是什么

JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为ECMA-404

虽然 JSON 源至于 JavaScript 语言,但它只是一种数据格式,可用于任何编程语言。现时具类似功能的格式有 XML、YAML,当中以 JSON 的语法最为简单。

例如,一个动态网页想从服务器获得数据时,服务器从数据库查找数据,然后把数据转换成 JSON 文本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"title": "Design Patterns",
"subtitle": "Elements of Reusable Object-Oriented Software",
"author": [
"Erich Gamma",
"Richard Helm",
"Ralph Johnson",
"John Vlissides"
],
"year": 2009,
"weight": 1.8,
"hardcover": true,
"publisher": {
"Company": "Pearson Education",
"Country": "India"
},
"website": null
}

网页的脚本代码就可以把此 JSON 文本解析为内部的数据结构去使用。

从此例子可看出,JSON 是树状结构,而 JSON 只包含 6 种数据类型:

  • null: 表示为 null
  • boolean: 表示为 true 或 false
  • number: 一般的浮点数表示方式,在下一单元详细说明
  • string: 表示为 “…”
  • array: 表示为 [ … ]
  • object: 表示为 { … }

目标:简单而基本

我们要实现的 JSON 库,主要是完成 3 个需求:

  1. 把 JSON 文本解析为一个树状数据结构(parse)。
  2. 提供接口访问该数据结构(access)。
  3. 把数据结构转换成 JSON 文本(stringify)。

requirement

开始:fork项目自己做

点击json-tutorial项目的fork后,在本地clone:

1
git clone git@github.com:yourname/json-tutorial.git

image-20210710143417392

cd 进入你的工作目录,可以看待git bash命令提示符后面有(master)标识,说明此时我们处在master分支上。

用git remote -v命令可以查看该本地仓库关联的远程仓库:

image-20210710143652754

其中,origin 为远程地址的别名,后面为远程仓库的url。

我们的 JSON 库名为 leptjson,代码文件只有 3 个:

  1. leptjson.h:leptjson 的头文件(header file),含有对外的类型和 API 函数声明。
  2. leptjson.c:leptjson 的实现文件(implementation file),含有内部的类型声明和函数实现。此文件会编译成库。
  3. test.c:我们使用测试驱动开发(test driven development, TDD)。此文件包含测试程序,需要链接 leptjson 库。

一、编译环境搭建

安装CMake后,使用其cmake-gui程序:

image-20210710145207755

先在 “Where is the source code” 选择 json-tutorial/tutorial01,再在 “Where to build the binary” 键入上一个目录加上 /build:

image-20210710150346431

在选择generator的地方要选择你电脑中安装的vs版本,否则会失败,若选择错误,可以在file->delete cache中重置:

image-20210710150217094

配置完成:

image-20210710150413104

编译运行leptjson_test后若出现下面结果说明已经成功搭建好编译环境:

1
2
/路径/json-tutorial/tutorial01/test.c:56: expect: 3 actual: 0
11/12 (91.67%) passed

二、头文件与API设计

由于头文件也可以 #include 其他头文件,为避免重复声明,通常会利用宏加入 include 防范(include guard):

1
2
3
4
5
6
#ifndef LEPTJSON_H__
#define LEPTJSON_H__

/* ... */

#endif /* LEPTJSON_H__ */

如前所述,JSON 中有 6 种数据类型,如果把 true 和 false 当作两个类型就是 7 种,我们为此声明一个枚举类型(enumeration type):

1
2
3
4
5
6
7
8
9
typedef enum { 
LEPT_NULL,
LEPT_FALSE,
LEPT_TRUE,
LEPT_NUMBER,
LEPT_STRING,
LEPT_ARRAY,
LEPT_OBJECT
} lept_type;

接下来,我们声明 JSON 的数据结构。JSON 是一个树形结构,我们最终需要实现一个树的数据结构,每个节点使用 lept_value 结构体表示,我们会称它为一个 JSON 值(JSON value)。 在此单元中,我们只需要实现 null, true 和 false 的解析,因此该结构体只需要存储一个 lept_type。之后的单元会逐步加入其他数据。

1
2
3
typedef struct {
lept_type type;
}lept_value;

先定义两个API:

1
2
int lept_parse(lept_value* v, const char* json);
lept_type lept_get_type(const lept_value* v);

前一个函数用来解析json,参数json是不应该被改动的,所以使用const char* 类型。返回值是以下这些枚举值,无错误会返回 LEPT_PARSE_OK,其他值在后面解释。

1
2
3
4
5
6
enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
};

后一个函数用来获取该lept_value节点的数据类型。

三、JSON 语法子集

下面是此单元的 JSON 语法子集,使用 RFC7159 中的 ABNF 表示:

1
2
3
4
5
6
JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = null / false / true
null = "null"
false = "false"
true = "true"

当中 %xhh 表示以 16 进制表示的字符,/ 是多选一,* 是零或多个,() 用于分组。

那么第一行的意思是,JSON 文本由 3 部分组成,首先是空白(whitespace),接着是一个值,最后是空白。

第二行告诉我们,所谓空白,是由零或多个空格符(space U+0020)、制表符(tab U+0009)、换行符(LF U+000A)、回车符(CR U+000D)所组成。

第三行是说,我们现时的值只可以是 null、false 或 true,它们分别有对应的字面值(literal)。

我们的解析器应能判断输入是否一个合法的 JSON。如果输入的 JSON 不合符这个语法,我们要产生对应的错误码,方便使用者追查问题。

在这个 JSON 语法子集下,我们定义 3 种错误码:

  • 若一个 JSON 只含有空白,传回 LEPT_PARSE_EXPECT_VALUE
  • 若一个值之后,在空白之后还有其他字符,传回 LEPT_PARSE_ROOT_NOT_SINGULAR
  • 若值不是那三种字面值,传回 LEPT_PARSE_INVALID_VALUE

四、单元测试

为了简单起见,编写一个极简单的单元测试方式。无论我们是采用 TDD(test-driven development),或是先实现后测试,都应尽量加入足够覆盖率的单元测试。

回到 leptjson 项目,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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "leptjson.h"

static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;

#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)

#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")

static void test_parse_null() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

/* ... */

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

现时只提供了一个 EXPECT_EQ_INT(expect, actual) 的宏,每次使用这个宏时,如果 expect != actual(预期值不等于实际值),便会输出错误信息。 若按照 TDD 的步骤,我们先写一个测试,如上面的 test_parse_null(),而 lept_parse() 只返回 LEPT_PARSE_OK

1
2
/路径/json-tutorial/tutorial01/test.c:27: expect: 0 actual: 1
1/2 (50.00%) passed

第一个返回 LEPT_PARSE_OK,所以是通过的。第二个测试因为 lept_parse() 没有把 v.type 改成 LEPT_NULL,造成失败。我们再实现 lept_parse() 令到它能通过测试。

宏的编写技巧: EXPECT_EQ_BASE解析

反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句,否则会有如下的问题:

1
2
3
4
5
6
7
8
9
10
11
12
#define M() a(); b()
if (cond)
M();
else
c();

/* 预处理后 */

if (cond)
a(); b(); /* b(); 在 if 之外 */
else /* <- else 缺乏对应 if */
c();

只用 { } 也不行:

1
2
3
4
5
6
7
8
#define M() { a(); b(); }

/* 预处理后 */

if (cond)
{ a(); b(); }; /* 最后的分号代表 if 语句结束 */
else /* else 缺乏对应 if */
c();

用 do while 就行了:

1
2
3
4
5
6
7
8
#define M() do { a(); b(); } while(0)

/* 预处理后 */

if (cond)
do { a(); b(); } while(0);
else
c();

五、实现解析器

首先为了减少解析函数之间传递多个参数,我们把这些数据都放进一个 lept_context 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef 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_parse() 失败,会把 v 设为 null 类型,所以这里先把它设为 null,让 lept_parse_value() 写入解析出来的根值。

给下面内容做的名词解释:

  1. 递归下降解析器(recursive descent parser)

    一种自上而下的解析器(Top-down parsing),由一组相互递归的程序(或等价的非递归程序)构建而成,其中每个程序都实现了文法中的一个非终结符

  2. 分词器(tokenizer)

    按照规则将文本切分为单词的工具。

    分词器流程图

leptjson 是一个手写的递归下降解析器。由于 JSON 语法特别简单,我们不需要写分词器,只需检测下一个字符,便可以知道它是哪种类型的值,然后调用相关的分析函数。对于完整的 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
#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)

/* 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。其它的解析函数会返回错误码,传递至顶层。

六、本单元文件完整内容

本单元结束项目三个文件的内容:

leptjson.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef LEPTJSON_H__
#define LEPTJSON_H__

typedef enum { LEPT_NULL, LEPT_FALSE, LEPT_TRUE, LEPT_NUMBER, LEPT_STRING, LEPT_ARRAY, LEPT_OBJECT } lept_type;

typedef struct {
lept_type type;
}lept_value;

enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
};

int lept_parse(lept_value* v, const char* json);

lept_type lept_get_type(const lept_value* v);

#endif /* LEPTJSON_H__ */

leptjson.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
42
43
44
45
46
47
#include "leptjson.h"
#include <assert.h> /* assert() */
#include <stdlib.h> /* NULL */

#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)

typedef struct {
const char* json;
}lept_context;

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

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

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

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

lept_type lept_get_type(const lept_value* v) {
assert(v != NULL);
return v->type;
}

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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "leptjson.h"

static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;

#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)

#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")

static void test_parse_null() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

static void test_parse_expect_value() {
lept_value v;

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, ""));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, " "));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

static void test_parse_invalid_value() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "nul"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));

v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "?"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}

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_null();
test_parse_expect_value();
test_parse_invalid_value();
test_parse_root_not_singular();
}

int main() {
test_parse();
printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
return main_ret;
}

七、总结与练习答案

本文介绍了如何配置一个编程环境,单元测试的重要性,和一个 JSON 解析器的子集实现。

  1. 补充leptjson.c的内容,使关于 LEPT_PARSE_ROOT_NOT_SINGULAR 的单元测试成功,若 json 在一个值之后,空白之后还有其它字符,则要返回 LEPT_PARSE_ROOT_NOT_SINGULAR

单元测试失败的是这一行:

1
EXPECT_EQ_INT(LEPT_PARSE_ROOT_NOT_SINGULAR, lept_parse(&v, "null x"));

我们从 JSON 语法发现,JSON 文本应该有 3 部分:

1
JSON-text = ws value ws

但原来的 lept_parse() 只处理了前两部分。我们只需要加入第三部分,解析空白,然后检查 JSON 文本是否完结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
  1. 参考 test_parse_null(),加入 test_parse_true()test_parse_false() 单元测试。参考 lept_parse_null() 的实现和调用方,解析 true 和 false 值。

只需参考 test_parse_null() 加入两个测试函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static 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_null();
test_parse_true();
test_parse_false();
test_parse_expect_value();
test_parse_invalid_value();
test_parse_root_not_singular();
}

但要记得在上一级的测试函数 test_parse() 调用这函数,否则会不起作用。还好如果我们记得用 static 修饰这两个函数,编译器会发出警告:

1
2
3
test.c:30:13: warning: unused function 'test_parse_true' [-Wunused-function]
static void test_parse_true() {
^

参考 lept_parse_null(),再写两个函数,然后在 lept_parse_value 按首字符分派。

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
static 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;
}

static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_true(c, v);
case 'f': return lept_parse_false(c, v);
case 'n': return lept_parse_null(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}

其实这 3 种类型都是解析字面量,可以使用单一个函数实现,例如用这种方式调用:

1
case 'n': return lept_parse_literal(c, v, "null", LEPT_NULL);

这样可以减少一些重复代码,不过可能有少许额外性能开销。

留言

⬆︎TOP