• 从零开始的 JSON 库教程(六):解析对象解答篇
    • 1. 重构 lept_parse_string()
    • 2. 实现 lept_parse_object()
    • 3. 释放内存
    • 4. 总结

    从零开始的 JSON 库教程(六):解析对象解答篇

    • Milo Yip
    • 2016/11/15

    本文是《从零开始的 JSON 库教程》的第六个单元解答篇。解答代码位于 json-tutorial/tutorial06_answer。

    1. 重构 lept_parse_string()

    这个「提取方法」重构练习很简单,只需要把原来调用 lept_set_string 的地方,改为写入参数变量。因此,原来的 lept_parse_string() 和 答案中的 lept_parse_string_raw() 的 diff 仅是两处:

    1. 130,131c130,131
    2. < static int lept_parse_string(lept_context* c, lept_value* v) {
    3. < size_t head = c->top, len;
    4. ---
    5. > static int lept_parse_string_raw(lept_context* c, char** str, size_t* len) {
    6. > size_t head = c->top;
    7. 140,141c140,141
    8. < len = c->top - head;
    9. < lept_set_string(v, (const char*)lept_context_pop(c, len), len);
    10. ---
    11. > *len = c->top - head;
    12. > *str = lept_context_pop(c, *len);

    以 TDD 方式开发软件,因为有单元测试确保软件的正确性,面对新需求可以安心重构,改善软件架构。

    2. 实现 lept_parse_object()

    有了 lept_parse_array() 的经验,实现 lept_parse_object() 几乎是一样的,分别只是每个迭代要多处理一个键和冒号。我们把这个实现过程分为 5 步曲。

    第 1 步是利用刚才重构出来的 lept_parse_string_raw() 去解析键的字符串。由于 lept_parse_string_raw() 假设第一个字符为 ",我们要先作校检,失败则要返回 LEPT_PARSE_MISS_KEY 错误。若字符串解析成功,它会把结果存储在我们的栈之中,需要把结果写入临时 lept_memberkklen 字段中:

    1. static int lept_parse_object(lept_context* c, lept_value* v) {
    2. size_t i, size;
    3. lept_member m;
    4. int ret;
    5. /* ... */
    6. m.k = NULL;
    7. size = 0;
    8. for (;;) {
    9. char* str;
    10. lept_init(&m.v);
    11. /* 1. parse key */
    12. if (*c->json != '"') {
    13. ret = LEPT_PARSE_MISS_KEY;
    14. break;
    15. }
    16. if ((ret = lept_parse_string_raw(c, &str, &m.klen)) != LEPT_PARSE_OK)
    17. break;
    18. memcpy(m.k = (char*)malloc(m.klen + 1), str, m.klen);
    19. m.k[m.klen] = '\0';
    20. /* 2. parse ws colon ws */
    21. /* ... */
    22. }
    23. /* 5. Pop and free members on the stack */
    24. /* ... */
    25. }

    第 2 步是解析冒号,冒号前后可有空白字符:

    1. /* 2. parse ws colon ws */
    2. lept_parse_whitespace(c);
    3. if (*c->json != ':') {
    4. ret = LEPT_PARSE_MISS_COLON;
    5. break;
    6. }
    7. c->json++;
    8. lept_parse_whitespace(c);

    第 3 步是解析任意的 JSON 值。这部分与解析数组一样,递归调用 lept_parse_value(),把结果写入临时 lept_memberv 字段,然后把整个 lept_member 压入栈:

    1. /* 3. parse value */
    2. if ((ret = lept_parse_value(c, &m.v)) != LEPT_PARSE_OK)
    3. break;
    4. memcpy(lept_context_push(c, sizeof(lept_member)), &m, sizeof(lept_member));
    5. size++;
    6. m.k = NULL; /* ownership is transferred to member on stack */

    但有一点要注意,如果之前缺乏冒号,或是这里解析值失败,在函数返回前我们要释放 m.k。如果我们成功地解析整个成员,那么就要把 m.k 设为空指针,其意义是说明该键的字符串的拥有权已转移至栈,之后如遇到错误,我们不会重覆释放栈里成员的键和这个临时成员的键。

    第 4 步,解析逗号或右花括号。遇上右花括号的话,当前的 JSON 对象就解析完结了,我们把栈上的成员复制至结果,并直接返回:

    1. /* 4. parse ws [comma | right-curly-brace] ws */
    2. lept_parse_whitespace(c);
    3. if (*c->json == ',') {
    4. c->json++;
    5. lept_parse_whitespace(c);
    6. }
    7. else if (*c->json == '}') {
    8. size_t s = sizeof(lept_member) * size;
    9. c->json++;
    10. v->type = LEPT_OBJECT;
    11. v->u.o.size = size;
    12. memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);
    13. return LEPT_PARSE_OK;
    14. }
    15. else {
    16. ret = LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET;
    17. break;
    18. }

    最后,当 for (;;) 中遇到任何错误便会到达这第 5 步,要释放临时的 key 字符串及栈上的成员:

    1. /* 5. Pop and free members on the stack */
    2. free(m.k);
    3. for (i = 0; i < size; i++) {
    4. lept_member* m = (lept_member*)lept_context_pop(c, sizeof(lept_member));
    5. free(m->k);
    6. lept_free(&m->v);
    7. }
    8. v->type = LEPT_NULL;
    9. return ret;

    注意我们不需要先检查 m.k != NULL,因为 free(NULL) 是完全合法的。

    3. 释放内存

    使用工具检测内存泄漏时,我们会发现以下这行代码造成内存泄漏:

    1. memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);

    类似数组,我们也需要在 lept_free() 释放 JSON 对象的成员(包括键及值):

    1. void lept_free(lept_value* v) {
    2. size_t i;
    3. assert(v != NULL);
    4. switch (v->type) {
    5. /* ... */
    6. case LEPT_OBJECT:
    7. for (i = 0; i < v->u.o.size; i++) {
    8. free(v->u.o.m[i].k);
    9. lept_free(&v->u.o.m[i].v);
    10. }
    11. free(v->u.o.m);
    12. break;
    13. default: break;
    14. }
    15. v->type = LEPT_NULL;
    16. }

    4. 总结

    至此,你已实现一个完整的 JSON 解析器,可解析任何合法的 JSON。统计一下,不计算空行及注释,现时 leptjson.c 只有 405 行代码,lept_json.h 54 行,test.c 309 行。

    另一方面,一些程序也需要生成 JSON。也许最初读者会以为生成 JSON 只需直接 sprintf()/fprintf() 就可以,但深入了解 JSON 的语法之后,我们应该知道 JSON 语法还是需做一些处理,例如字符串的转义、数字的格式等。然而,实现生成器还是要比解析器容易得多。而且,假设我们有一个正确的解析器,可以简单使用 roundtrip 方式实现测试。请期待下回分解。

    如果你遇到问题,有不理解的地方,或是有建议,都欢迎在评论或 issue 中提出,让所有人一起讨论。