使用 C 语言和结构体方法实现 JSON 解析

目录

标签:

想法 (Idea)

  1. 用 C 语言构建一个 JSON 解析器。
  2. 不使用独立的函数,而是将函数附加到结构体,并将它们用作方法。
  3. 使其没有 C 语言常见的问题 (段错误、内存泄漏、堆栈溢出等...)。
  4. 提供符合人体工程学的 API。

用法 (Usage)

C

 1#include "json.h"
 2#include <stdlib.h>
 3
 4int main(void) {
 5 struct json json = json_new(JSON({
 6  "object" : {},
 7  "array" : [[]],
 8  "atoms" : [ "string", 0.1, true, false, null ]
 9 }));
10 struct json_value json_value = json.parse(&json);
11 json_print_value(&json_value);
12 puts("");
13 json_free_value(&json_value);
14 return EXIT_SUCCESS;
15}

提示 - 简单编译 C 项目

不要将其作为使用 make 的指南,在我的项目中,我只是将其用作命令运行器。

编译器标志

这些标志可能特定于 gcc,我使用 gcc (GCC) 14.2.1 20250207,因此请谨慎对待。 我几乎在我启动的每个 C 项目中使用这些标志。 SH

 1gcc -std=c23 \
 2	-O2 \
 3	-Wall \
 4	-Wextra \
 5	-Werror \
 6	-fdiagnostics-color=always \
 7	-fsanitize=address,undefined \
 8	-fno-common \
 9	-Winit-self \
10	-Wfloat-equal \
11	-Wundef \
12	-Wshadow \
13	-Wpointer-arith \
14	-Wcast-align \
15	-Wstrict-prototypes \
16	-Wstrict-overflow=5 \
17	-Wwrite-strings \
18	-Waggregate-return \
19	-Wswitch-default \
20	-Wno-discarded-qualifiers \
21	-Wno-aggregate-return \
22  main.c

Flag| Description ---|--- -std=c23| 设置语言标准,我使用 ISO C23 -O2| 优化级别高于 -O1 -Wall| 启用一系列警告 -Wextra| 启用比 -Wall 更多的警告 -Werror| 将所有警告转换为错误 -fdiagnostics-color=always| 在诊断中使用颜色 -fsanitize=address,undefined| 启用 AddressSanitizer 和 UndefinedBehaviorSanitizer -fno-common| 将未初始化的全局变量放置在 BSS 段中 -Winit-self| 警告关于未初始化的变量 -Wfloat-equal| 如果在相等比较中使用浮点值,则发出警告 -Wundef| 如果评估未定义的标识符,则发出警告 -Wshadow| 每当局部变量或类型声明隐藏另一个变量、参数、类型时,发出警告 -Wpointer-arith| 警告任何依赖于函数类型或 void 的“大小”的内容 -Wcast-align| 每当强制转换指针时,发出警告,这样会增加目标所需的对齐方式。 -Wstrict-prototypes| 如果在未指定参数类型的情况下声明或定义函数,则发出警告 -Wstrict-overflow=5| 警告编译器基于签名溢出不会发生的假设进行优化的情况 -Wwrite-strings| 给字符串常量类型 const char[length],警告复制到非 const char* 中 -Wswitch-default| 每当 switch 语句没有 default 情况时,发出警告 -Wno-discarded-qualifiers| 如果丢弃了指针上的类型限定符,则不要发出警告。 -Wno-aggregate-return| 如果定义或调用了任何返回结构或联合的函数,则不要发出警告。

寻找源文件

我通常将头文件和源文件保存在与 makefile 相同的目录中,所以我使用 find 来查找它们: SHELL

1shell find . -name "*.c"

Make 和 Makefiles

我没有将 build 目标定义为 .PHONY,因为我通常没有 build 目录。 将所有内容放在一起作为一个 makefile: MAKE

 1CFLAGS := -std=c23 \
 2	-O2 \
 3	-Wall \
 4	-Wextra \
 5	-Werror \
 6	-fdiagnostics-color=always \
 7	-fsanitize=address,undefined \
 8	-fno-common \
 9	-Winit-self \
10	-Wfloat-equal \
11	-Wundef \
12	-Wshadow \
13	-Wpointer-arith \
14	-Wcast-align \
15	-Wstrict-prototypes \
16	-Wstrict-overflow=5 \
17	-Wwrite-strings \
18	-Waggregate-return \
19	-Wcast-qual \
20	-Wswitch-default \
21	-Wno-discarded-qualifiers \
22	-Wno-aggregate-return
23
24FILES := $(shell find . -name "*.c")
25
26build:
27	$(CC) $(CFLAGS) $(FILES) -o jsoninc

使用变参宏编写内联原始 JSON (Variadic macros to write inline raw JSON)

这实际上不值得单独作为一个部分,但我使用 #<expression> 来字符串化 C 语言表达式,并结合使用 __VA_ARGS__: C

1#define JSON(...) #__VA_ARGS__

启用: C

1char *raw_json = JSON({ "array" : [ [], {}] });

内联到: C

1char *raw_json = "{ \"array\" : [[]], }";

在内存中表示 JSON 值 (Representing JSON values in memory)

我需要一个结构来保存已解析的 JSON 值、它们的类型和它们的值。

JSON 值的类型 (Types of JSON values)

JSON 可以是以下之一:

  1. null
  2. true
  3. false
  4. number
  5. string
  6. array
  7. object

在 C 语言中,我使用 enum 来表示这一点: C

 1// json.h
 2enum json_type {
 3 json_number,
 4 json_string,
 5 json_boolean,
 6 json_null,
 7 json_object,
 8 json_array,
 9};
10
11extern char *json_type_map[];

我使用 json_type_map 将所有 json_type 值映射到它们的 char* 表示形式: C

1char *json_type_map[] = {
2  [json_number] = "json_number",  [json_string] = "json_string",
3  [json_boolean] = "json_boolean", [json_null] = "json_null",
4  [json_object] = "json_object",  [json_array] = "json_array",
5};

json_value 结构体 & 用于原子值、数组元素或对象值和对象键的联合体 (json_value & unions for atoms, array elements or object values and object keys)

json_value 结构体保存类型(如上定义)、一个联合体(为布尔值、字符串或数字共享内存空间)、一个 json_value 结构体列表(作为数组子项或对象值)、一个字符串列表(即对象键)以及上述三个字段的长度。 C

 1struct json_value {
 2 enum json_type type;
 3 union {
 4  bool boolean;
 5  char *string;
 6  double number;
 7 } value;
 8 struct json_value *values;
 9 char **object_keys;
10 size_t length;
11};

销毁值 (Tearing values down)

由于 json_value 中的某些字段是在堆上分配的,因此当我们不再使用它或退出进程时,我们必须销毁/释放该结构体。 json_free_value 正是做这件事的: C

 1void json_free_value(struct json_value *json_value) {
 2 switch (json_value->type) {
 3 case json_string:
 4  free(json_value->value.string);
 5  break;
 6 case json_object:
 7  for (size_t i = 0; i < json_value->length; i++) {
 8   free(json_value->object_keys[i]);
 9   json_free_value(&json_value->values[i]);
10  }
11  if (json_value->object_keys != NULL) {
12   free(json_value->object_keys);
13   json_value->object_keys = NULL;
14  }
15  if (json_value->values != NULL) {
16   free(json_value->values);
17   json_value->values = NULL;
18  }
19  break;
20 case json_array:
21  for (size_t i = 0; i < json_value->length; i++) {
22   json_free_value(&json_value->values[i]);
23  }
24  if (json_value->values != NULL) {
25   free(json_value->values);
26   json_value->values = NULL;
27  }
28  break;
29 case json_number:
30 case json_boolean:
31 case json_null:
32 default:
33  break;
34 }
35 json_value->type = json_null;
36}

就这么简单,我们忽略了堆栈分配的 JSON 值变体(例如 json_numberjson_booleanjson_null),同时为 json_string、每个 json_array 子项和 json_object 键和值释放已分配的内存空间。

打印 json_values (Printing json_values)

只有内存表示而没有检查它的方法对我们没有价值,因此我将 print_json_value 放入 main.c 中: C

 1void print_json_value(struct json_value *json_value) {
 2 switch (json_value->type) {
 3 case json_null:
 4  printf("null");
 5  break;
 6 case json_number:
 7  printf("%f", json_value->value.number);
 8  break;
 9 case json_string:
10  printf("\"%s\"", json_value->value.string);
11  break;
12 case json_boolean:
13  printf(json_value->value.boolean ? "true" : "false");
14  break;
15 case json_object:
16  printf("{");
17  for (size_t i = 0; i < json_value->length; i++) {
18   printf("\"%s\": ", json_value->object_keys[i]);
19   print_json_value(&json_value->values[i]);
20   if (i < json_value->length - 1) {
21    printf(", ");
22   }
23  }
24  printf("}");
25  break;
26 case json_array:
27  printf("[");
28  for (size_t i = 0; i < json_value->length; i++) {
29   print_json_value(&json_value->values[i]);
30   if (i < json_value->length - 1) {
31    printf(", ");
32   }
33  }
34  printf("]");
35  break;
36 default:
37  ASSERT(0, "Unimplemented json_value case");
38  break;
39 }
40}

调用此函数: C

 1int main(void) {
 2 struct json_value json_value = {
 3   .type = json_array,
 4   .length = 4,
 5   .values =
 6     (struct json_value[]){
 7       (struct json_value){.type = json_string, .value.string = "hi"},
 8       (struct json_value){.type = json_number, .value.number = 161},
 9       (struct json_value){
10         .type = json_object,
11         .length = 1,
12         .object_keys =
13           (char *[]){
14             "key",
15           },
16         .values =
17           (struct json_value[]){
18             (struct json_value){.type = json_string,
19                       .value.string = "value"},
20           },
21       },
22       (struct json_value){.type = json_null},
23     },
24 };
25 json_print_value(&json_value);
26 puts("");
27 return EXIT_SUCCESS;
28}

结果: TEXT

1["hi", 161.000000, {"key": "value"}, null]

json 解析器结构体,函数指针以及如何使用它们 (它们很糟糕) (json Parser struct, Function pointers and how to use them (they suck))

听起来可能相反,人们可以很容易地将函数附加到 C 语言中的结构体,只需将结构体的一个字段定义为函数指针,将一个函数分配给它,你就拥有了一个方法,就像在 Go 或 Rust 中一样。 C

 1struct json {
 2 char *input;
 3 size_t pos;
 4 size_t length;
 5 char (*cur)(struct json *json);
 6 bool (*is_eof)(struct json *json);
 7 void (*advance)(struct json *json);
 8 struct json_value (*atom)(struct json *json);
 9 struct json_value (*array)(struct json *json);
10 struct json_value (*object)(struct json *json);
11 struct json_value (*parse)(struct json *json);
12};

当然,你必须以 C 语言的方式定义一个函数(<return type> <name>(<list of params>);)并将其分配给你的方法字段 - 但这并不复杂: C

 1struct json json_new(char *input) {
 2 ASSERT(input != NULL, "corrupted input");
 3 struct json j = (struct json){
 4   .input = input,
 5   .length = strlen(input) - 1,
 6 };
 7
 8 j.cur = cur;
 9 j.is_eof = is_eof;
10 j.advance = advance;
11 j.parse = parse;
12 j.object = object;
13 j.array = array;
14 j.atom = atom;
15
16 return j;
17}

curis_eofadvance 是小的辅助函数: C

 1static char cur(struct json *json) {
 2 ASSERT(json != NULL, "corrupted internal state");
 3 return json->is_eof(json) ? -1 : json->input[json->pos];
 4}
 5
 6static bool is_eof(struct json *json) {
 7 ASSERT(json != NULL, "corrupted internal state");
 8 return json->pos > json->length;
 9}
10
11static void advance(struct json *json) {
12 ASSERT(json != NULL, "corrupted internal state");
13 json->pos++;
14 skip_whitespace(json);
15}

ASSERT 是一个简单的断言宏: C

1#define ASSERT(EXP, context)                          \
2 if (!(EXP)) {                                \
3  fprintf(stderr,                              \
4      "jsoninc: ASSERT(" #EXP "): `" context               \
5      "` failed at %s, line %d\n",                    \
6      __FILE__, __LINE__);                        \
7  exit(EXIT_FAILURE);                            \
8 }

例如,如果 json_new 函数的参数是一个空指针,则会失败: C

1int main(void) {
2 struct json json = json_new(NULL);
3 return EXIT_SUCCESS;
4}

即使有描述性的注释: TEXT

1jsoninc: ASSERT(input != NULL): `corrupted input` failed at ./json.c, line 16

使用方法解析 JSON (Parsing JSON with methods)

既然我们已经完成了所有的设置,我们就可以从项目的关键开始:解析 JSON。 通常我会做一个词法分析器和解析器,但为了简单起见 - 我将这些过程合并到一个单一的解析器架构中。

警告

另外,请不要考虑标准合规性 - 我真的懒得去管,请参阅 解析 JSON 是一个雷区 💣

忽略空白 (Ignoring Whitespace)

就我们而言,JSON 没有说明任何关于空白的内容 - 所以我们只使用 skip_whitespace 函数来忽略所有空白: C

1static void skip_whitespace(struct json *json) {
2 while (!json->is_eof(json) &&
3     (json->cur(json) == ' ' || json->cur(json) == '\t' ||
4     json->cur(json) == '\n')) {
5  json->pos++;
6 }
7}

解析原子值 (Parsing Atoms)

由于 JSON 有五种原子值,我们需要使用 json->atom 方法将它们解析为我们的 json_value 结构体: C

 1static struct json_value atom(struct json *json) {
 2  ASSERT(json != NULL, "corrupted internal state");
 3
 4  skip_whitespace(json);
 5
 6  char cc = json->cur(json);
 7  if ((cc >= '0' && cc <= '9') || cc == '.' || cc == '-') {
 8    return number(json);
 9  }
10
11  switch (cc) {
12    // ... all of the atoms ...
13  default:
14    printf("unknown character '%c' at pos %zu\n", json->cur(json), json->pos);
15    ASSERT(false, "unknown character");
16    return (struct json_value){.type = json_null};
17  }
18}

数字 (numbers)

信息

从技术上讲,JSON 中的数字应该包括科学计数法和其他有趣的东西,但让我们记住项目的简单性和我的理智,请参阅 json.org。 C

 1static struct json_value number(struct json *json) {
 2 ASSERT(json != NULL, "corrupted internal state");
 3 size_t start = json->pos;
 4 // i don't give a fuck about scientific notation <3
 5 for (char cc = json->cur(json);
 6    ((cc >= '0' && cc <= '9') || cc == '_' || cc == '.' || cc == '-');
 7    json->advance(json), cc = json->cur(json))
 8  ;
 9
10 char *slice = malloc(sizeof(char) * json->pos - start + 1);
11 ASSERT(slice != NULL, "failed to allocate slice for number parsing")
12 memcpy(slice, json->input + start, json->pos - start);
13 slice[json->pos - start] = 0;
14 double number = strtod(slice, NULL);
15 free(slice);
16
17 return (struct json_value){.type = json_number, .value = {.number = number}};
18}

我们跟踪数字的开始位置,只要该数字仍然被认为是数字(任何 0-9 | _ | . | -),就前进。 一旦到达末尾,我们就分配一个临时字符串,从输入字符串复制包含数字的字符,并使用 \0 终止该字符串。 strtod 用于将此字符串转换为 double。 完成后,我们释放切片并将结果作为 json_value 返回。

null, true 和 false

nulltruefalse 是唯一的原子,并且易于推理,关于常量大小和字符,因此我们可以只断言它们的字符: C

 1static struct json_value atom(struct json *json) {
 2 ASSERT(json != NULL, "corrupted internal state");
 3
 4 skip_whitespace(json);
 5
 6 char cc = json->cur(json);
 7 if ((cc >= '0' && cc <= '9') || cc == '.' || cc == '-') {
 8  return number(json);
 9 }
10
11 switch (cc) {
12 case 'n': // null
13  json->pos++;
14  ASSERT(json->cur(json) == 'u', "unknown atom 'n', wanted 'null'")
15  json->pos++;
16  ASSERT(json->cur(json) == 'l', "unknown atom 'nu', wanted 'null'")
17  json->pos++;
18  ASSERT(json->cur(json) == 'l', "unknown atom 'nul', wanted 'null'")
19  json->advance(json);
20  return (struct json_value){.type = json_null};
21 case 't': // true
22  json->pos++;
23  ASSERT(json->cur(json) == 'r', "unknown atom 't', wanted 'true'")
24  json->pos++;
25  ASSERT(json->cur(json) == 'u', "unknown atom 'tr', wanted 'true'")
26  json->pos++;
27  ASSERT(json->cur(json) == 'e', "unknown atom 'tru', wanted 'true'")
28  json->advance(json);
29  return (struct json_value){.type = json_boolean,
30                .value = {.boolean = true}};
31 case 'f': // false
32  json->pos++;
33  ASSERT(json->cur(json) == 'a', "invalid atom 'f', wanted 'false'")
34  json->pos++;
35  ASSERT(json->cur(json) == 'l', "invalid atom 'fa', wanted 'false'")
36  json->pos++;
37  ASSERT(json->cur(json) == 's', "invalid atom 'fal', wanted 'false'")
38  json->pos++;
39  ASSERT(json->cur(json) == 'e', "invalid atom 'fals', wanted 'false'")
40  json->advance(json);
41  return (struct json_value){.type = json_boolean,
42                .value = {.boolean = false}};
43 // ... strings ...
44 default:
45  printf("unknown character '%c' at pos %zu\n", json->cur(json), json->pos);
46  ASSERT(false, "unknown character");
47  return (struct json_value){.type = json_null};
48 }
49}

字符串 (strings)

信息

同样,与 JSON 数字类似,JSON 字符串应包括引号和其他有趣内容的转义符,但让我们再次记住项目的简单性和我的理智,请参阅 json.org。 C

 1static char *string(struct json *json) {
 2 json->advance(json);
 3 size_t start = json->pos;
 4 for (char cc = json->cur(json); cc != '\n' && cc != '"';
 5    json->advance(json), cc = json->cur(json))
 6  ;
 7
 8 char *slice = malloc(sizeof(char) * json->pos - start + 1);
 9 ASSERT(slice != NULL, "failed to allocate slice for a string")
10
11 memcpy(slice, json->input + start, json->pos - start);
12 slice[json->pos - start] = 0;
13
14 ASSERT(json->cur(json) == '"', "unterminated string");
15 json->advance(json);
16 return slice;
17}

非常容易,只要我们在字符串内部(在 \",\nEOF 之前),我们就前进,之后我们将其复制到一个新的切片并返回该切片(此函数对于对象键特别有用 - 这就是它是一个函数的原因)。

解析数组 (Parsing Arrays)

由于数组是 [] 之间和用 , 分隔的任何数量的 JSON 值,因此也很容易实现: C

 1struct json_value array(struct json *json) {
 2 ASSERT(json != NULL, "corrupted internal state");
 3 ASSERT(json->cur(json) == '[', "invalid array start");
 4 json->advance(json);
 5
 6 struct json_value json_value = {.type = json_array};
 7 json_value.values = malloc(sizeof(struct json_value));
 8
 9 while (!json->is_eof(json) && json->cur(json) != ']') {
10  if (json_value.length > 0) {
11   if (json->cur(json) != ',') {
12    json_free_value(&json_value);
13   }
14   ASSERT(json->cur(json) == ',',
15       "expected , as the separator between array members");
16   json->advance(json);
17  }
18  struct json_value member = json->parse(json);
19  json_value.values = realloc(json_value.values,
20                sizeof(json_value) * (json_value.length + 1));
21  json_value.values[json_value.length++] = member;
22 }
23
24 ASSERT(json->cur(json) == ']', "missing array end");
25 json->advance(json);
26 return json_value;
27}

我们从数组长度为 1 开始,并为我们找到的每个新子项重新分配内存。 我们还检查每个子项之间的 ,

一个不断增长的数组可能更适合最小化分配,但我们在这里编写未经优化的 C 代码 - 尽管如此,它仍然有效 :)

解析对象 (Parsing Objects)

C

 1struct json_value object(struct json *json) {
 2 ASSERT(json != NULL, "corrupted internal state");
 3 ASSERT(json->cur(json) == '{', "invalid object start");
 4 json->advance(json);
 5
 6 struct json_value json_value = {.type = json_object};
 7 json_value.object_keys = malloc(sizeof(char *));
 8 json_value.values = malloc(sizeof(struct json_value));
 9
10 while (!json->is_eof(json) && json->cur(json) != '}') {
11  if (json_value.length > 0) {
12   if (json->cur(json) != ',') {
13    json_free_value(&json_value);
14   }
15   ASSERT(json->cur(json) == ',',
16       "expected , as separator between object key value pairs");
17   json->advance(json);
18  }
19  ASSERT(json->cur(json) == '"',
20      "expected a string as the object key, did not get that")
21  char *key = string(json);
22  ASSERT(json->cur(json) == ':', "expected object key and value separator");
23  json->advance(json);
24
25  struct json_value member = json->parse(json);
26  json_value.values = realloc(json_value.values, sizeof(struct json_value) *
27                            (json_value.length + 1));
28  json_value.values[json_value.length] = member;
29  json_value.object_keys = realloc(json_value.object_keys,
30                   sizeof(char **) * (json_value.length + 1));
31  json_value.object_keys[json_value.length] = key;
32  json_value.length++;
33 }
34
35 ASSERT(json->cur(json) == '}', "missing object end");
36 json->advance(json);
37 return json_value;
38}

与数组相同,只是我们有一个字符串作为键,: 作为分隔符和一个 json_value 作为值,而不是单个原子。 每对都用 , 分隔。 2025 - xnacly