leptjson note1
Table of Contents
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)
运行时鉴定条件是否为真, 断言失败会令程序直接崩溃.
- 如果某个错误是由于程序员错误编码所造成的 (例如传入不合法的参数) , 那么应用断言; 如果某个错误是由运行时的环境所造成的, 程序员无法避免, 就要处理运行时错误 (例如开启文件失败) .