跟着github上的项目json-tutorial完成一个json解析器。本文对应该项目的第四单元,重点在解析json数组。
👉原项目地址
一、JSON数组 一个 JSON 数组可以包含零至多个元素,而这些元素也可以是数组类型。换句话说,我们可以表示嵌套(nested)的数据结构。先来看看 JSON 数组的语法:
1 array = %x5B ws [ value *( ws %x2C ws value ) ] ws %x5D
当中,%x5B
是左中括号 [
,%x2C
是逗号 ,
,%x5D
是右中括号 ]
,ws
是空白字符。一个数组可以包含零至多个值,以逗号分隔,例如 []
、[1,2,true]
、[[1,2],[3,4],"abc"]
都是合法的数组。但注意 JSON 不接受末端额外的逗号,例如 [1,2,]
是不合法的(许多编程语言如 C/C++、Javascript、Java、C# 都容许数组初始值包含末端逗号)。
JSON 数组的语法很简单,实现的难点不在语法上,而是怎样管理内存。
二、数据结构 首先,我们需要设计存储 JSON 数组类型的数据结构。
JSON 数组存储零至多个元素,最简单就是使用 C 语言的数组。数组最大的好处是能以 O(1) 用索引访问任意元素,次要好处是内存布局紧凑,省内存之余还有高缓存一致性(cache coherence)。但数组的缺点是不能快速插入元素,而且我们在解析 JSON 数组的时候,还不知道应该分配多大的数组才合适。
另一个选择是链表(linked list),它的最大优点是可快速地插入元素(开端、末端或中间),但需要以 O(n) 时间去经索引取得内容。如果我们只需顺序遍历,那么是没有问题的。还有一个小缺点,就是相对数组而言,链表在存储每个元素时有额外内存开销(存储下一节点的指针),而且遍历时元素所在的内存可能不连续,令缓存不命中(cache miss)的机会上升。
我见过一些 JSON 库选择了链表,而这里则选择了数组。我们将会通过之前在解析字符串时实现的堆栈,来解决解析 JSON 数组时未知数组大小的问题。
决定之后,我们在 lept_value
的 union
中加入数组的结构:
1 2 3 4 5 6 7 8 9 10 typedef struct lept_value lept_value ;struct lept_value { union { struct { lept_value* e; size_t size; }a; struct { char * s; size_t len; }s; double n; }u; lept_type type; };
由于 lept_value
内使用了自身类型的指针,我们必须前向声明(forward declare)此类型。
另外,注意这里 size
是元素的个数,不是字节单位。我们增加两个 API 去访问 JSON 数组类型的值:
1 2 3 4 5 6 7 8 9 10 size_t lept_get_array_size (const lept_value* v) { assert(v != NULL && v->type == LEPT_ARRAY); return v->u.a.size; } lept_value* lept_get_array_element (const lept_value* v, size_t index) { assert(v != NULL && v->type == LEPT_ARRAY); assert(index < v->u.a.size); return &v->u.a.e[index]; }
暂时我们不考虑增删数组元素,这些功能留待第八单元讨论。
然后,我们写一个单元测试去试用这些 API(练习需要更多测试)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #if defined(_MSC_VER) #define EXPECT_EQ_SIZE_T(expect, actual) EXPECT_EQ_BASE((expect) == (actual), (size_t)expect, (size_t)actual, "%Iu" ) #else #define EXPECT_EQ_SIZE_T(expect, actual) EXPECT_EQ_BASE((expect) == (actual), (size_t)expect, (size_t)actual, "%zu" ) #endif static void test_parse_array () { lept_value v; lept_init(&v); EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ ]" )); EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v)); EXPECT_EQ_SIZE_T(0 , lept_get_array_size(&v)); lept_free(&v); }
在之前的单元中,作者已多次重申,C 语言的数组大小应该使用 size_t
类型。因为我们要验证 lept_get_array_size()
返回值是否正确,所以再为单元测试框架添加一个宏 EXPECT_EQ_SIZE_T
。麻烦之处在于,ANSI C(C89)并没有的 size_t
打印方法,在 C99 则加入了 "%zu"
,但 VS2015 中才有,之前的 VC 版本使用非标准的 "%Iu"
。因此,上面的代码使用条件编译去区分 VC 和其他编译器。虽然这部分不跨平台也不是 ANSI C 标准,但它只在测试程序中,不太影响程序库的跨平台性。
三、解析过程 我们在解析 JSON 字符串时,因为在开始时不能知道字符串的长度,而又需要进行转义,所以需要一个临时缓冲区去存储解析后的结果。我们为此实现了一个动态增长的堆栈,可以不断压入字符,最后一次性把整个字符串弹出,复制至新分配的内存之中。
对于 JSON 数组,我们也可以用相同的方法,而且,我们可以用同一个堆栈!我们只需要把每个解析好的元素压入堆栈,解析到数组结束时,再一次性把所有元素弹出,复制至新分配的内存之中。
但和字符串有点不一样,如果把 JSON 当作一棵树的数据结构,JSON 字符串是叶节点,而 JSON 数组是中间节点。在叶节点的解析函数中,我们怎样使用那个堆栈也可以,只要最后还原就好了。但对于数组这样的中间节点,共用这个堆栈没问题么?
答案是:只要在解析函数结束时还原堆栈的状态,就没有问题。为了直观地了解这个解析过程,我们用连环图去展示 ["abc",[1,2],3]
的解析过程。
首先,我们遇到 [
,进入 lept_parse_array()
:
生成一个临时的 lept_value
,用于存储之后的元素。我们再调用 lept_parse_value()
去解析这个元素值,因为遇到 "
进入 lept_parse_string()
:
在 lept_parse_string()
中,不断解析字符直至遇到 "
,过程中把每个字符压栈:
最后在 lept_parse_string()
中,把栈上 3 个字符弹出,分配内存,生成字符串值:
返回上一层 lept_parse_array()
,把临时元素压栈:
然后我们再遇到 [
,进入另一个 lept_parse_array()
。它发现第一个元素是数字类型,所认调用 lept_parse_number()
,生成一个临时的元素值:
之后把该临时的元素值压栈:
接着再解析第二个元素。我们遇到了 ]
,从栈上弹出 2 个元素,分配内存,生成数组(虚线代表是连续的内存):
那个数组是上层数组的元素,我们把它压栈。现时栈内已有两个元素,我们再继续解析下一个元素:
最后,遇到了 ]
,可以弹出栈内 3 个元素,分配内存,生成数组:
四、实现 经过这个详细的图解,实现 lept_parse_array()
应该没有难度。以下是半制成品:
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 lept_parse_value (lept_context* c, lept_value* v) ; static int lept_parse_array (lept_context* c, lept_value* v) { size_t size = 0 ; int ret; EXPECT(c, '[' ); if (*c->json == ']' ) { c->json++; v->type = LEPT_ARRAY; v->u.a.size = 0 ; v->u.a.e = NULL ; return LEPT_PARSE_OK; } for (;;) { lept_value e; lept_init(&e); if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) return ret; memcpy (lept_context_push(c, sizeof (lept_value)), &e, sizeof (lept_value)); size++; if (*c->json == ',' ) c->json++; else if (*c->json == ']' ) { c->json++; v->type = LEPT_ARRAY; v->u.a.size = size; size *= sizeof (lept_value); memcpy (v->u.a.e = (lept_value*)malloc (size), lept_context_pop(c, size), size); return LEPT_PARSE_OK; } else return LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET; } } static int lept_parse_value (lept_context* c, lept_value* v) { switch (*c->json) { case '[' : return lept_parse_array(c, v); } }
简单说明的话,就是在循环中建立一个临时值(lept_value e
),然后调用 lept_parse_value()
去把元素解析至这个临时值,完成后把临时值压栈。当遇到 ]
,把栈内的元素弹出,分配内存,生成数组值。
注意到,lept_parse_value()
会调用 lept_parse_array()
,而 lept_parse_array()
又会调用 lept_parse_value()
,这是互相引用,所以必须要加入函数前向声明 。
最后,我想告诉同学,实现这个函数时,我曾经制造一个不明显的 bug。这个函数有两个 memcpy()
,第一个「似乎」是可以避免的,先压栈取得元素的指针,给 lept_parse_value
:
1 2 3 4 5 6 7 8 9 for (;;) { lept_value* e = lept_context_push(c, sizeof (lept_value)); lept_init(e); size++; if ((ret = lept_parse_value(c, e)) != LEPT_PARSE_OK) return ret; }
这种写法为什么会有 bug?这是第 5 条练习题。
五、总结与练习答案
编写 test_parse_array()
单元测试,解析以下 2 个 JSON。由于数组是复合的类型,不能使用一个宏去测试结果,请使用各个 API 检查解析后的内容。
1 2 [ null , false , true , 123 , "abc" ] [ [ ] , [ 0 ] , [ 0 , 1 ] , [ 0 , 1 , 2 ] ]
这个练习纯粹为了熟习数组的访问 API。新增的第一个 JSON 只需平凡的检测。第二个 JSON 有特定模式,第 i 个子数组的长度为 i,每个子数组的第 j 个元素是数字值 j,所以可用两层 for 循环测试。
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 static void test_parse_array () { size_t i, j; lept_value v; lept_init(&v); EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ null , false , true , 123 , \"abc\" ]" )); EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v)); EXPECT_EQ_SIZE_T(5 , lept_get_array_size(&v)); EXPECT_EQ_INT(LEPT_NULL, lept_get_type(lept_get_array_element(&v, 0 ))); EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(lept_get_array_element(&v, 1 ))); EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(lept_get_array_element(&v, 2 ))); EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(lept_get_array_element(&v, 3 ))); EXPECT_EQ_INT(LEPT_STRING, lept_get_type(lept_get_array_element(&v, 4 ))); EXPECT_EQ_DOUBLE(123.0 , lept_get_number(lept_get_array_element(&v, 3 ))); EXPECT_EQ_STRING("abc" , lept_get_string(lept_get_array_element(&v, 4 )), lept_get_string_length(lept_get_array_element(&v, 4 ))); lept_free(&v); lept_init(&v); EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ [ ] , [ 0 ] , [ 0 , 1 ] , [ 0 , 1 , 2 ] ]" )); EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v)); EXPECT_EQ_SIZE_T(4 , lept_get_array_size(&v)); for (i = 0 ; i < 4 ; i++) { lept_value* a = lept_get_array_element(&v, i); EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(a)); EXPECT_EQ_SIZE_T(i, lept_get_array_size(a)); for (j = 0 ; j < i; j++) { lept_value* e = lept_get_array_element(a, j); EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(e)); EXPECT_EQ_DOUBLE((double )j, lept_get_number(e)); } } lept_free(&v); }
现时的测试结果应该是失败的,因为 lept_parse_array()
里没有处理空白字符,加进合适的 lept_parse_whitespace()
令测试通过。
按现时的 lept_parse_array()
的编写方式,需要加入 3 个 lept_parse_whitespace()
调用,分别是解析 [
之后,元素之后,以及 ,
之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static int lept_parse_array (lept_context* c, lept_value* v) { EXPECT(c, '[' ); lept_parse_whitespace(c); for (;;) { if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) return ret; lept_parse_whitespace(c); if (*c->json == ',' ) { c->json++; lept_parse_whitespace(c); } } }
使用第三单元解答篇 介绍的检测内存泄漏工具,会发现测试中有内存泄漏。很明显在 lept_parse_array()
中使用到 malloc()
分配内存,但却没有对应的 free()
。应该在哪里释放内存?修改代码,使工具不再检测到相关的内存泄漏。
成功测试那 3 个 JSON 后,使用内存泄漏检测工具会发现 lept_parse_array()
用 malloc()
分配的内存没有被释放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ==154 == 124 (120 direct, 4 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 4 ==154 == at 0 x4C28C20: malloc (vg_replace_malloc.c:296 ) ==154 == by 0 x409D82: lept_parse_array (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x409E91: lept_parse_value (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x409F14: lept_parse (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x405261: test_parse_array (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x408C72: test_parse (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x40916A: main (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == ==154 == 240 (96 direct, 144 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4 ==154 == at 0 x4C28C20: malloc (vg_replace_malloc.c:296 ) ==154 == by 0 x409D82: lept_parse_array (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x409E91: lept_parse_value (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x409F14: lept_parse (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x40582C: test_parse_array (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x408C72: test_parse (in /json-tutorial/ tutorial05/build/ leptjson_test) ==154 == by 0 x40916A: main (in /json-tutorial/ tutorial05/build/ leptjson_test)
很明显,有 malloc()
就要有对应的 free()
。正确的释放位置应该放置在 lept_free()
,当值被释放时,该值拥有的内存也在那里释放。之前字符串的释放也是放在这里:
1 2 3 4 5 6 void lept_free (lept_value* v) { assert(v != NULL ); if (v->type == LEPT_STRING) free (v->u.s.s); v->type = LEPT_NULL; }
但对于数组,我们应该先把数组内的元素通过递归调用 lept_free()
释放,然后才释放本身的 v->u.a.e
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void lept_free (lept_value* v) { size_t i; assert(v != NULL ); switch (v->type) { case LEPT_STRING: free (v->u.s.s); break ; case LEPT_ARRAY: for (i = 0 ; i < v->u.a.size; i++) lept_free(&v->u.a.e[i]); free (v->u.a.e); break ; default : break ; } v->type = LEPT_NULL; }
修改之后,再运行内存泄漏检测工具,确保问题已被修正。
开启 test.c 中两处被 #if 0 ... #endif
关闭的测试,本来 lept_parse_array()
已经能处理这些测试。然而,运行时会发现 Assertion failed: (c.top == 0)
断言失败。这是由于,当错误发生时,仍然有一些临时值在堆栈里,既没有放进数组,也没有被释放。修改 lept_parse_array()
,当遇到错误时,从堆栈中弹出并释放那些临时值,然后才返回错误码。
遇到解析错误时,我们可能在之前已压入了一些值在自定义堆栈上。如果没有处理,最后会在 lept_parse()
中发现堆栈上还有一些值,做成断言失败。所以,遇到解析错误时,我们必须弹出并释放那些值。
在 lept_parse_array
中,原本遇到解析失败时,会直接返回错误码。我们把它改为 break
离开循环,在循环结束后的地方用 lept_free()
释放从堆栈弹出的值,然后才返回错误码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static int lept_parse_array (lept_context* c, lept_value* v) { for (;;) { if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) break ; if (*c->json == ',' ) { } else if (*c->json == ']' ) { } else { ret = LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET; break ; } } for (i = 0 ; i < size; i++) lept_free((lept_value*)lept_context_pop(c, sizeof (lept_value))); return ret; }
第 4 节那段代码为什么会有 bug?
这个 bug 源于压栈时,会获得一个指针 e
,指向从堆栈分配到的空间:
1 2 3 4 5 6 7 8 9 for (;;) { lept_value* e = lept_context_push(c, sizeof (lept_value)); lept_init(e); size++; if ((ret = lept_parse_value(c, e)) != LEPT_PARSE_OK) return ret; }
然后,我们把这个指针调用 lept_parse_value(c, e)
,这里会出现问题,因为 lept_parse_value()
及之下的函数都需要调用 lept_context_push()
,而 lept_context_push()
在发现栈满了的时候会用 realloc()
扩容。这时候,我们上层的 e
就会失效,变成一个悬挂指针(dangling pointer),而且 lept_parse_value(c, e)
会通过这个指针写入解析结果,造成非法访问。
在使用 C++ 容器时,也会遇到类似的问题。从容器中取得的迭代器(iterator)后,如果改动容器内容,之前的迭代器会失效。这里的悬挂指针问题也是相同的。
但这种 bug 有时可能在简单测试中不能自动发现,因为问题只有堆栈满了才会出现。从测试的角度看,我们需要一些压力测试(stress test),测试更大更复杂的数据。但从编程的角度看,我们要谨慎考虑变量的生命周期,尽量从编程阶段避免出现问题。例如把 lept_context_push()
的 API 改为:
1 static void lept_context_push (lept_context* c, const void * data, size_t size) ;
这样就确把数据压入栈内,避免了返回指针的生命周期问题。但我们之后会发现,原来的 API 设计在一些情况会更方便一些,例如在把字符串值转化(stringify)为 JSON 时,我们可以预先在堆栈分配字符串所需的最大空间,而当时是未有数据填充进去的。
无论如何,我们编程时都要考虑清楚变量的生命周期,特别是指针的生命周期。
经过对数组的解析,我们了解到如何利用递归处理复合型的数据类型解析。与一些用链表或自动扩展的动态数组的实现比较,我们利用了自定义堆栈作为缓冲区,能分配最紧凑的数组作存储之用,会比其他实现更省内存。我们完成了数组类型后,只余下对象类型了。