Tutorial01

1. 头文件与 API 设计

C 语言有头文件的概念,需要使用 #include去引入头文件中的类型声明和函数声明。但由于头文件也可以 #include 其他头文件,为避免重复声明,通常会利用宏加入 include 防范(include guard):

  1. #ifndef LEPTJSON_H__
  2. #define LEPTJSON_H__
  3. /* ... */
  4. #endif /* LEPTJSON_H__ */

宏的名字必须是唯一的,通常习惯以 _H__ 作为后缀。由于 leptjson 只有一个头文件,可以简单命名为 LEPTJSON_H__。如果项目有多个文件或目录结构,可以用 项目名称_目录_文件名称_H__ 这种命名方式。

如前所述,JSON 中有 6 种数据类型,如果把 true 和 false 当作两个类型就是 7 种,我们为此声明一个枚举类型(enumeration type):

  1. 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 的数据结构。JSON 是一个树形结构,我们最终需要实现一个树的数据结构,每个节点使用 lept_value 结构体表示,我们会称它为一个 JSON 值(JSON value)。
在此单元中,我们只需要实现 null, true 和 false 的解析,因此该结构体只需要存储一个 lept_type。之后的单元会逐步加入其他数据。

  1. typedef struct {
  2. lept_type type;
  3. }lept_value;

C 语言的结构体是以 struct X {} 形式声明的,定义变量时也要写成 struct X x;。为方便使用,上面的代码使用了 typedef

然后,我们现在只需要两个 API 函数,一个是解析 JSON:

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

传入的 JSON 文本是一个 C 字符串(空结尾字符串/null-terminated string),由于我们不应该改动这个输入字符串,所以使用 const char* 类型。
另一注意点是,传入的根节点指针 v 是由使用方负责分配的,所以一般用法是:

  1. lept_value v;
  2. const char json[] = ...;
  3. int ret = lept_parse(&v, json);

返回值是以下这些枚举值,无错误会返回 LEPT_PARSE_OK,其他值在下节解释。

  1. enum {
  2. LEPT_PARSE_OK = 0,
  3. LEPT_PARSE_EXPECT_VALUE,
  4. LEPT_PARSE_INVALID_VALUE,
  5. LEPT_PARSE_ROOT_NOT_SINGULAR
  6. };

现时我们只需要一个访问结果的函数,就是获取其类型:

  1. lept_type lept_get_type(const lept_value* v);

2.TDD

一般我们会采用自动的测试方式,例如单元测试(unit testing)。单元测试也能确保其他人修改代码后,原来的功能维持正确(这称为回归测试/regression testing)。
常用的单元测试框架有 xUnit 系列,如 C++ 的 Google Test、C# 的 NUnit。我们为了简单起见,会编写一个极简单的单元测试方式。
一般来说,软件开发是以周期进行的。例如,加入一个功能,再写关于该功能的单元测试。但也有另一种软件开发方法论,称为测试驱动开发(test-driven development, TDD),它的主要循环步骤是:

  1. 加入一个测试。

  2. 运行所有测试,新的测试应该会失败。

  3. 编写实现代码。

  4. 运行所有测试,若有测试失败回到3。

  5. 重构代码。

  6. 回到 1。

TDD 是先写测试,再实现功能。好处是实现只会刚好满足测试,而不会写了一些不需要的代码,或是没有被测试的代码。
但无论我们是采用 TDD,或是先实现后测试,都应尽量加入足够覆盖率的单元测试。

  1. static int main_ret = 0;
  2. static int test_count = 0;
  3. static int test_pass = 0;
  4. #define EXPECT_EQ_BASE(equality, expect, actual, format) \
  5. do \
  6. { \
  7. test_count++; \
  8. if (equality) \
  9. test_pass++; \
  10. else \
  11. { \
  12. fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual); \
  13. main_ret = 1; \
  14. } \
  15. } while (0)
  16. /*__FILE__:当前文件路径 __LINE__:编译器提供的宏,代表编译时该行的行号*/
  17. #define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")
  18. #define EXPECT_EQ_DOUBLE(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%.17g")
  19. static void test_parse_null() {
  20. lept_value v;
  21. v.type = LEPT_TRUE;
  22. EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
  23. EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
  24. }
  25. /* ... */
  26. static void test_parse() {
  27. test_parse_null();
  28. /* ... */
  29. }
  30. int main() {
  31. test_parse();
  32. printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
  33. return main_ret;
  34. }

现时只提供了一个 EXPECT_EQ_INT(expect, actual) 的宏,每次使用这个宏时,如果 expect != actual(预期值不等于实际值),便会输出错误信息。类似如下

  1. /Users/miloyip/github/json-tutorial/tutorial01/test.c:27: expect: 0 actual: 1
  2. 1/2 (50.00%) passed

3. 宏的编写技巧——do while

有些同学可能不了解 EXPECT_EQ_BASE 宏的编写技巧,简单说明一下。反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句,否则会有如下的问题:

  1. #define M() a(); b()
  2. if (cond)
  3. M();
  4. else
  5. c();
  6. /* 预处理后 */
  7. if (cond)
  8. a(); b(); /* b(); 在 if 之外 */
  9. else /* <- else 缺乏对应 if */
  10. c();

只用 { } 也不行:

  1. #define M() { a(); b(); }
  2. /* 预处理后 */
  3. if (cond)
  4. { a(); b(); }; /* 最后的分号代表 if 语句结束 */
  5. else /* else 缺乏对应 if */
  6. c();

do while 便可

  1. #define M() do { a(); b(); } while(0)
  2. /* 预处理后 */
  3. if (cond)
  4. do { a(); b(); } while(0);
  5. else
  6. c();

4. 断言

断言(assertion)是 C 语言中常用的防御式编程方式,减少编程错误最常用的是在函数开始的地方,检测所有参数。有时候也可以在调用函数后,检查上下文是否正确。
**
C 语言的标准库含有 assert() 这个宏(需 #include <assert.h>),提供断言功能。当程序以 release 配置编译时(定义了 NDEBUG 宏),assert() 不会做检测;而当在 debug 配置时(没定义 NDEBUG 宏),则会在运行时检测 assert(cond) 中的条件是否为真(非 0),断言失败会直接令程序崩溃。

例如上面的 lept_parse_null() 开始时,当前字符应该是 'n',所以我们使用一个宏 EXPECT(c, ch) 进行断言,并跳到下一字符。
初使用断言的同学,可能会错误地把含副作用)的代码放在 assert() 中:

  1. assert(x++ == 0); /* 这是错误的! */
  2. //若在执行前x!=0,出错,++就不会执行

这样会导致 debug 和 release 版的行为不一样。

另一个问题是,初学者可能会难于分辨何时使用断言,何时处理运行时错误(如返回错误值或在 C++ 中抛出异常)。简单的答案是,如果那个错误是由于程序员错误编码所造成的(例如传入不合法的参数),那么应用断言;如果那个错误是程序员无法避免,而是由运行时的环境所造成的,就要处理运行时错误(例如开启文件失败)。

Tutorial02

Double边界值测试

  1. //创建宏简化测试代码
  2. #define TEST_NUMBER(expect, json) \
  3. do \
  4. { \
  5. lept_value v; \
  6. EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, json)); \
  7. EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(&v)); \
  8. EXPECT_EQ_DOUBLE(expect, lept_get_number(&v)); \
  9. } while (0)
  10. //边界测试如下
  11. TEST_NUMBER(1.0000000000000002, "1.0000000000000002"); /* the smallest number > 1 */
  12. TEST_NUMBER( 4.9406564584124654e-324, "4.9406564584124654e-324"); /* minimum denormal */
  13. TEST_NUMBER(-4.9406564584124654e-324, "-4.9406564584124654e-324");
  14. TEST_NUMBER( 2.2250738585072009e-308, "2.2250738585072009e-308"); /* Max subnormal double */
  15. TEST_NUMBER(-2.2250738585072009e-308, "-2.2250738585072009e-308");
  16. TEST_NUMBER( 2.2250738585072014e-308, "2.2250738585072014e-308"); /* Min normal positive double */
  17. TEST_NUMBER(-2.2250738585072014e-308, "-2.2250738585072014e-308");
  18. TEST_NUMBER( 1.7976931348623157e+308, "1.7976931348623157e+308"); /* Max double */
  19. TEST_NUMBER(-1.7976931348623157e+308, "-1.7976931348623157e+308");

lept_strtod()编写

有一些 JSON 解析器不使用 strtod() 而自行转换,例如在校验的同时,记录负号、尾数(整数和小数)和指数,然后 naive 地计算


  1. int negative = 0;
  2. int64_t mantissa = 0;
  3. int exp = 0;
  4. /* 解析... 并存储 negative, mantissa, exp */
  5. v->n = (negative ? -mantissa : mantissa) * pow(10.0, exp);

RapidJSON 就内部实现了三种算法(使用 kParseFullPrecision 选项开启),最后一种算法用到了大整数(高精度计算)。有兴趣的同学也可以先尝试做一个 naive 版本,不使用 strtod()。之后可再参考 Google 的 double-conversion 开源项目及相关论文。

校验数字——将JSON语法书写为校验规则

JSON中数字语法如下: number = [ "-" ] int [ frac ] [ exp ] int = "0" / digit1-9 *digit frac = "." 1*digit exp = ("e" / "E") ["-" / "+"] 1*digit

算法——static int lept_parse_number(lept_context* c, lept_value* v)

  1. 通过指针p来表示当前解析字符的位置,好处如下:
    • 代码简洁
    • 由于不确定c是否会改变,从而每次更改c->json都需要一次间接访问

**

  1. static int lept_parse_number(lept_context* c, lept_value* v) {
  2. const char* p = c->json;
  3. /* 负号 */
  4. if (*p == '-') p++;
  5. /* 整数 */
  6. if (*p == '0') p++;
  7. else {
  8. if (!ISDIGIT1TO9(*p)) return LEPT_PARSE_INVALID_VALUE;
  9. for (p++; ISDIGIT(*p); p++);
  10. }
  11. /* 小数 */
  12. if (*p == '.') {
  13. p++;
  14. if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
  15. for (p++; ISDIGIT(*p); p++);
  16. }
  17. /* 指数 */
  18. if (*p == 'e' || *p == 'E') {
  19. p++;
  20. if (*p == '+' || *p == '-') p++;
  21. if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
  22. for (p++; ISDIGIT(*p); p++);
  23. }
  24. /* 数值过大 */
  25. errno = 0;
  26. v->u.n = strtod(c->json, NULL);
  27. if (errno == ERANGE && (v->u.n == HUGE_VAL || v->u.n == -HUGE_VAL))
  28. return LEPT_PARSE_NUMBER_TOO_BIG;
  29. v->type = LEPT_NUMBER;
  30. c->json = p;
  31. return LEPT_PARSE_OK;
  32. }