从零开始的JSON库教程
Q7nl1s admin

从零开始的 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
    6
    JSON-text = ws value ws
    ws = *(%x20 / %x09 / %x0A / %x0D)
    value = null / false / true
    null = "null"
    false = "false"
    true = "true"

需求:

requirement

  • 把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
    3
    typedef 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
    6
    enum {
    LEPT_PARSE_OK = 0,
    LEPT_PARSE_EXPECT_VALUE,
    LEPT_PARSE_INVALID_VALUE,
    LEPT_PARSE_ROOT_NOT_SINGULAR
    };
  • 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 进制表示的字符,/ 是多选一,* 是零或多个,() 用于分组。

    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
  • 单元测试

    在复杂项目中一般不用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
    #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"));// 解析成功返回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
    #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();

    要让实现语句的整个操作只存在一个;(分号)。

  • 实现解析器

    在实现API设计、单元测试后,就到了解析器的实现。

    为了避免函数新参多参数传递,我把这些数据都放在了一个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_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
    #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。其它的解析函数会返回错误码,传递至顶层。

  • 关于断言

    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
    24
    static 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
    18
    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_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
    17
    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;
    }

    此处相对来说简单很多,唯一要注意的是json最后停靠的地址位置,是在检测类型之后的第一个空白字符(也可为\0或异常情况)的位置。

  • 常见问答

    1.为什么把例子命名为 leptjson?

    来自于标准模型中的轻子(lepton),意为很轻量的 JSON 库。另外,建议大家为项目命名时,先 google 一下是否够独特,有很多同名的话搜寻时难以命中。

    2.为什么使用宏而不用函数或内联函数?

    因为这个测试框架使用了 __LINE__ 这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。另外,内联函数是 C99 的新增功能,本教程使用 C89。

2.解析数字

规范:

需求:

结构:

实现:

持续更新…

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View