Skip to main content
  1. Posts/

leptjson note1

·4 mins·

leptjson note1 #

从零开始的 JSON 库教程 笔记.

JSON #

JSON (JavaScript Object Notation, JavaScript 对象表示法) , 是一种存储和交换文本信息的语法.

{
    "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 包含 6 种数据类型:

  • null: 表示为 null
  • boolean: 表示为 true 或 false
  • numebr: 一般的浮点数表示方式
  • string: 表示为 “…”
  • array: 表示为 […]
  • object: 表示为 {…}

需要实现的 JSON 库需要完成 3 个需求:

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

本单元实现 null 和 boolean 的解析.

编译环境 #

使用 CMake 进行配置.

头文件与 API 设置 #

Include 防范 #

利用宏加入 Include Guard 避免重复声明:

# ifndef LEPTJSON_H__
# define LEPTJSON_H__

/* ... */

# endif /* LEPTJSON_H__ */

宏的名字必须是唯一的, 通常习惯以 _H__ 作为后缀.

  • 如果项目有多个文件或目录结构, 可以用项目名称_目录_文件名称_H__这种命名方式.

枚举类型 #

JSON 中有 6 种数据类型,如果把 true 和 false 当作两个类型, 就是 7 种. 声明枚举类型:

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

因为 C 语言没有 C++ 的命名空间 (namespace) 功能, 一般会使用项目的简写作为标识符的前缀. 通常枚举值用全大写 (如 LEPT_NULL) , 而类型及函数则用小写 (如 lept_type) .

数据结构 #

声明 JSON 的数据结构. 最终需要实现树的数据结构, 本单元只需要存储一个 lept_type.

typedef struct {
    lept_type type;
}lept_value;

API: 解析 JSON #

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

传入的 JSON 文本为 C 字符串, 同时不应当被改动, 因此使用 const char* 类型.

返回值的枚举类型:

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

获取访问结果的函数:

lept_type lept_get_type(const lept_value* v);

JSON 语法子集 #

此单元的 JSON 语法子集:

JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = null / false / true 
null  = "null"
false = "false"
true  = "true"

其中, %xhh 表示以 16 进制表示的字符, / 是多选一, * 是零或多个, () 用于分组. 语法解释:

  • JSON 文本组成为 空白 + 值 + 空白.
  • 空白由零或多个空格符、制表符、换行符、回车符组成.
  • 值取 null, false 或 true, 它们分别有对应的字面值 (literal) .

JSON 解析器需要判断输入是否是一个合法的 JSON, 如果输入的 JSON 不合法, 需要产生相应的错误码. 此单元中, 错误码如下:

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

单元测试 #

宏编写技巧 #

测试文件 test.h 中的宏 EXPECT_EQ_BASE 如下:

#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)

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

# define M() a(); b()

if (cond)
    M();
else
    c();

/* 预处理后 */

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

如果只用 { } 也不行:

# define M() {a(); b();}

/* 预处理后 */

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

使用 do { /*...*/ } while(0) 才正确:

# define M() do { a(); b(); } while(0)

/* 预处理后 */

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

实现解析器 #

lept_context 存储参数:

typedef struct {
    const char* json;
}lept_context;

/* ... */

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) { // 解析出 null
        lept_parse_whitespace(&c); // 跳过第二段空白
        if (*c.json != '\0') // 空白之后还有其它字符, 返回状态为 LEPT_PARSE_ROOT_NOT_SINGULAR
            ret = LEPT_PARSE_ROOT_NOT_SINGULAR;
    }
    return ret;
}

这里我们实现的 JSON 解释器需要完整鉴别 JSON 文本是否符合规范.

解析函数: 由于 JSON 的语法很简单, 在跳过空白后, 只需要读取第一个字符就可以知道值的类型, 然后调用相关的分析函数.

// 如果首字符等于给定值, 指针指向后一个字符
#define EXPECT(c, ch)       do { assert(*c->json == (ch)); c->json++; } while(0) 

typedef struct {
    const char* json;
}lept_context;

/* ws = *(%x20 / %x09 / %x0A / %x0D) */
// lept_parse_whitespace 跳过 json 开头的空格
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_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 'n':  return lept_parse_null(c, v);
        case 't':  return lept_parse_true(c, v);
        case 'f': return lept_parse_false(c, v);
        case '\0': return LEPT_PARSE_EXPECT_VALUE; // 空字符串
        default:   return LEPT_PARSE_INVALID_VALUE; // 此单元中, 不合语法
    }
}

关于断言 #

断言 (assertion) 是 C 语言中常用的防御式编程方式, 用以减少编程错误. 最常用的是在函数开始的地方检测所有参数. 有时候也可以在调用函数后, 检查上下文是否正确. assert(cond) 运行时鉴定条件是否为真, 断言失败会令程序直接崩溃.

  • 如果某个错误是由于程序员错误编码所造成的 (例如传入不合法的参数) , 那么应用断言; 如果某个错误是由运行时的环境所造成的, 程序员无法避免, 就要处理运行时错误 (例如开启文件失败) .