使用 C 语言和结构体方法实现 JSON 解析
使用 C 语言和结构体方法实现 JSON 解析
- 2025年2月26日
- 2940 字
- 预计阅读时间 14 分钟
目录
- 想法 (Idea)
- 使用变参宏编写内联原始 JSON (Variadic macros to write inline raw JSON)
- 在内存中表示 JSON 值 (Representing JSON values in memory)
- JSON 值的类型 (Types of JSON values)
json_value
结构体 & 用于原子值、数组元素或对象值和对象键的联合体 (json_value
& unions for atoms, array elements or object values and object keys)- 销毁值 (Tearing values down)
- 打印
json_value
(Printing json_values) json
解析器结构体,函数指针以及如何使用它们 (它们很糟糕) (json
Parser struct, Function pointers and how to use them (they suck))
- 使用方法解析 JSON (Parsing JSON with methods)
标签:
想法 (Idea)
- 用 C 语言构建一个 JSON 解析器。
- 不使用独立的函数,而是将函数附加到结构体,并将它们用作方法。
- 使其没有 C 语言常见的问题 (段错误、内存泄漏、堆栈溢出等...)。
- 提供符合人体工程学的 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 可以是以下之一:
- null
- true
- false
- number
- string
- array
- 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_number
、json_boolean
和 json_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}
cur
、is_eof
和 advance
是小的辅助函数:
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
null
、true
和 false
是唯一的原子,并且易于推理,关于常量大小和字符,因此我们可以只断言它们的字符:
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}
非常容易,只要我们在字符串内部(在 \"
,\n
和 EOF
之前),我们就前进,之后我们将其复制到一个新的切片并返回该切片(此函数对于对象键特别有用 - 这就是它是一个函数的原因)。
解析数组 (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