minimal-json:嵌入式C语言轻量级JSON解析器

张开发
2026/4/5 0:24:37 15 分钟阅读

分享文章

minimal-json:嵌入式C语言轻量级JSON解析器
1. 项目概述minimal-json是一个面向嵌入式资源受限环境设计的轻量级 JSON 解析器库其核心目标是在极小内存占用下完成 JSON 文本的语法验证、结构解析与基础数据提取。它不追求 RFC 7159 的全功能兼容如浮点数精确解析、Unicode 转义处理、任意嵌套深度支持而是聚焦于嵌入式系统中最常见的 JSON 使用场景设备配置加载、传感器数据上报响应解析、OTA 固件元信息读取等。该库完全采用 C 语言实现无任何动态内存分配malloc/free、无标准库依赖stdio.h、stdlib.h等仅用于示例、无递归调用所有解析状态均通过栈上结构体维护确保在 RAM 仅数百字节的 MCU如 STM32F030、nRF51822、ESP32-C3 的最小配置上稳定运行。其“minimal”之名体现在三个关键维度代码体积核心解析器源码json.c不足 500 行编译后 Flash 占用通常低于 1.2 KBARM Cortex-M0-Os运行时内存最大栈空间消耗可控在 128–256 字节取决于最大嵌套深度配置零堆内存使用功能裁剪明确放弃对null值的独立类型表示统一作空字符串处理、不支持科学计数法浮点、不验证字符串 UTF-8 合法性、不提供 JSON 生成序列化能力。这种极致精简并非缺陷而是嵌入式开发中典型的“够用即止”Good Enough工程哲学体现——在保证协议交互正确性的前提下将资源开销压至最低为实时任务、低功耗休眠或加密运算腾出宝贵资源。2. 核心设计原理与约束机制2.1 零动态内存与栈式状态机minimal-json的解析引擎基于确定性有限状态机DFA实现所有状态变量当前字符位置、嵌套深度、当前 token 类型、字符串起始偏移等均封装于一个固定大小的json_parser_t结构体中该结构体实例在调用解析函数时作为参数传入生命周期完全由调用者控制typedef struct { const char *src; // 输入 JSON 字符串首地址必须以 \0 结尾 size_t pos; // 当前解析位置索引 size_t len; // 字符串总长度可预计算避免 strlen uint8_t depth; // 当前嵌套深度对象/数组 uint8_t max_depth; // 编译时配置的最大允许深度防栈溢出 uint8_t state; // 内部状态机状态码JSON_STATE_XXX } json_parser_t;此设计彻底规避了malloc引发的碎片化、分配失败风险及不可预测的执行时间符合 IEC 61508、ISO 26262 等功能安全标准对确定性内存行为的要求。开发者只需在栈上声明一个json_parser_t变量例如static json_parser_t parser;即可复用该实例进行多次解析。2.2 深度限制与安全防护嵌套深度max_depth是minimal-json最关键的安全参数。JSON 中的{}和[]可无限嵌套若不限制恶意构造的超深嵌套 JSON如 1000 层将导致状态机栈溢出或解析器无限循环。库强制要求调用者在初始化json_parser_t时显式设置max_depth典型值为 4–8并在解析过程中实时校验// 在解析 { 或 [ 时触发 if (parser-depth parser-max_depth) { return JSON_ERROR_DEPTH_EXCEEDED; // 返回错误码不继续解析 } parser-depth;该机制不仅防止栈溢出也显著降低最坏情况下的时间复杂度——解析时间与输入长度及最大深度呈线性关系而非指数级。2.3 Token 流式提取与回调驱动minimal-json采用事件驱动Event-Driven解析模式不构建完整的 DOM 树或 AST。它将 JSON 文本视为一个字符流当识别出一个完整语义单元Token时立即通过用户注册的回调函数通知应用层。这种设计极大节省 RAM无需缓存整个 JSON 对象仅需保存当前 Token 的起始位置和长度。核心解析函数签名如下typedef enum { JSON_TOKEN_OBJECT_START, // { JSON_TOKEN_OBJECT_END, // } JSON_TOKEN_ARRAY_START, // [ JSON_TOKEN_ARRAY_END, // ] JSON_TOKEN_STRING, // value含引号 JSON_TOKEN_NUMBER, // 123 或 -45.67纯 ASCII 数字序列 JSON_TOKEN_TRUE, // true JSON_TOKEN_FALSE, // false JSON_TOKEN_COLON, // : JSON_TOKEN_COMMA, // , } json_token_type_t; typedef void (*json_callback_t)(void *user_data, json_token_type_t type, const char *start, size_t length); int json_parse(json_parser_t *parser, json_callback_t callback, void *user_data);应用层通过实现callback函数在收到JSON_TOKEN_STRING时可直接从src start处截取子串注意start指向引号内首字符length为字符串内容长度不含引号收到JSON_TOKEN_NUMBER时可用strtol/strtod若需浮点或自定义 ASCII 转换函数解析。2.4 字符串处理的嵌入式优化JSON 字符串常含转义序列如\,\\,\n。minimal-json提供两种处理策略透传模式默认回调返回的start/length指向原始转义字符串如\Hello\\nWorld\由应用层按需解码。此举避免在解析器中嵌入复杂的转义表和状态机节省约 200 字节 Flash。预解码模式可选启用宏JSON_DECODE_STRINGS后库在回调前自动将常见转义符\,\\,\/,\b,\f,\n,\r,\t替换为对应 ASCII 字符并调整length。此模式增加约 150 字节代码但简化应用逻辑。该选择权交予开发者体现嵌入式开发中“空间换时间”或“时间换空间”的经典权衡。3. API 接口详解与使用范式3.1 主要解析函数函数签名功能说明返回值典型用法int json_parse(json_parser_t *parser, json_callback_t callback, void *user_data)启动解析逐个触发回调0成功负值为错误码JSON_ERROR_SYNTAX,JSON_ERROR_DEPTH_EXCEEDED,JSON_ERROR_UNEXPECTED_EOF在 UART 接收完成中断或 HTTP 响应解析时调用int json_find_string(const char *json, const char *key, char *out_buf, size_t out_size)在 JSON 中查找指定 key 的字符串值简易版0找到并复制成功-1未找到或缓冲区不足快速提取配置项如json_find_string(rx_buf, device_id, id_str, sizeof(id_str))json_find_string是为简化常见场景提供的便捷封装其内部仍调用json_parse并在回调中匹配 key适合对性能要求不高、代码体积敏感的场合。3.2 错误码定义与诊断#define JSON_ERROR_NONE 0 #define JSON_ERROR_SYNTAX -1 // 语法错误非法字符、缺少分隔符、引号不匹配 #define JSON_ERROR_DEPTH_EXCEEDED -2 // 嵌套深度超限 #define JSON_ERROR_UNEXPECTED_EOF -3 // 输入提前结束如 {key:value 缺少 } #define JSON_ERROR_INVALID_NUMBER -4 // 数字格式错误如 0123 前导零或 1e2.5 无效指数错误码设计遵循嵌入式惯例非零即错负值表异常。JSON_ERROR_SYNTAX是最常见的错误通常源于传感器固件发送的 JSON 格式不规范如漏掉逗号、多出逗号、单引号代替双引号。建议在调试阶段开启日志打印parser-pos位置快速定位问题字符。3.3 典型应用示例解析设备配置 JSON假设 MCU 通过 UART 接收到以下配置字符串已存入rx_buffer{wifi:{ssid:MyAP,pass:12345678},mqtt:{broker:192.168.1.100,port:1883}}应用层实现如下#include minimal-json/json.h typedef struct { char ssid[33]; char pass[65]; char broker[16]; uint16_t port; } device_config_t; static device_config_t g_config; static uint8_t g_in_wifi_section 0; static uint8_t g_in_mqtt_section 0; static void config_callback(void *user_data, json_token_type_t type, const char *start, size_t length) { (void)user_data; switch (type) { case JSON_TOKEN_STRING: if (g_in_wifi_section !strncmp(start-2, \ssid\, 6)) { // 下一个 STRING 就是 ssid 值利用 JSON 键值相邻特性 // 注意此为简化逻辑实际需更严谨的状态跟踪 if (length sizeof(g_config.ssid)) { memcpy(g_config.ssid, start, length); g_config.ssid[length] \0; } } else if (g_in_wifi_section !strncmp(start-2, \pass\, 6)) { if (length sizeof(g_config.pass)) { memcpy(g_config.pass, start, length); g_config.pass[length] \0; } } else if (g_in_mqtt_section !strncmp(start-2, \broker\, 8)) { if (length sizeof(g_config.broker)) { memcpy(g_config.broker, start, length); g_config.broker[length] \0; } } break; case JSON_TOKEN_NUMBER: if (g_in_mqtt_section !strncmp(start-2, \port\, 6)) { g_config.port (uint16_t)strtol(start, NULL, 10); } break; case JSON_TOKEN_OBJECT_START: if (length 4 !strncmp(start, \wifi\, 6)) { g_in_wifi_section 1; } else if (length 5 !strncmp(start, \mqtt\, 6)) { g_in_mqtt_section 1; } break; case JSON_TOKEN_OBJECT_END: if (g_in_wifi_section || g_in_mqtt_section) { g_in_wifi_section g_in_mqtt_section 0; } break; default: break; } } // UART 接收完成中断服务程序中调用 void uart_rx_complete_handler(void) { static json_parser_t parser; int ret; parser.src rx_buffer; parser.pos 0; parser.len strlen(rx_buffer); // 或由 DMA 传输长度提供 parser.depth 0; parser.max_depth 4; // 允许最多 4 层嵌套 parser.state 0; ret json_parse(parser, config_callback, NULL); if (ret ! 0) { // 处理错误重置模块、发送错误响应等 printf(JSON parse error %d at pos %u\n, ret, parser.pos); } else { printf(Config loaded: SSID%s, Broker%s:%u\n, g_config.ssid, g_config.broker, g_config.port); } }此示例展示了如何利用回调机制在不构建完整树结构的前提下精准提取嵌套字段。关键点在于通过JSON_TOKEN_OBJECT_START/END跟踪当前所处的 JSON 对象层级wifi或mqtt再结合后续的STRING/NUMBER事件提取具体值。4. 与主流嵌入式生态的集成实践4.1 与 STM32 HAL 库协同工作在 STM32 项目中常需解析通过 HAL_UART_Receive_IT 接收的 JSON 数据。由于中断接收的数据是分片的需先拼接完整 JSON 字符串。推荐做法使用环形缓冲区Ring Buffer暂存 UART 数据在接收完成标志如空闲中断或超时触发后从环形缓冲区提取以\0或}结尾的完整 JSON 片段调用json_parse解析。// 环形缓冲区管理伪代码 static uint8_t uart_rx_buf[256]; static uint16_t rx_head 0, rx_tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 将接收到的字节存入环形缓冲区 ringbuf_push(uart_rx_buf, rx_head, received_byte); HAL_UART_Receive_IT(huart, tmp_byte, 1); } void on_uart_idle_timeout(void) { // 尝试从环形缓冲区提取完整 JSON寻找匹配的 {} 或 [] size_t json_len find_complete_json(uart_rx_buf, rx_tail, rx_head); if (json_len 0) { // 复制到临时缓冲区确保以 \0 结尾 char json_tmp[256]; ringbuf_read(uart_rx_buf, rx_tail, json_tmp, json_len); json_tmp[json_len] \0; json_parser_t parser { .src json_tmp, .len json_len, .max_depth 4 }; json_parse(parser, uart_json_callback, NULL); } }4.2 与 FreeRTOS 任务配合在 FreeRTOS 环境下可将 JSON 解析封装为独立任务避免阻塞高优先级任务QueueHandle_t json_queue; // 存储待解析的 JSON 字符串指针需管理内存 void json_parser_task(void *pvParameters) { char *json_str; json_parser_t parser; for(;;) { if (xQueueReceive(json_queue, json_str, portMAX_DELAY) pdTRUE) { parser.src json_str; parser.len strlen(json_str); parser.max_depth 4; // ... 初始化其他字段 json_parse(parser, rtos_json_callback, NULL); // 解析完成后释放内存若使用 pvPortMalloc 分配 vPortFree(json_str); } } } // 在网络任务中接收到 HTTP 响应后分配内存并入队 char *resp_json pvPortMalloc(http_content_len 1); memcpy(resp_json, http_body, http_content_len); resp_json[http_content_len] \0; xQueueSend(json_queue, resp_json, 0);4.3 与传感器驱动集成解析 BME280 校准数据部分传感器如 BME280的校准参数以 JSON 格式存储在 Flash 中。minimal-json可高效读取// 假设校准数据存储在 Flash 地址 0x0800F000 const char *calib_json (const char*)0x0800F000; // 定义全局校准结构体 typedef struct { uint16_t dig_T1; uint16_t dig_T2; uint16_t dig_T3; uint16_t dig_P1; int16_t dig_P2; int16_t dig_P3; // ... 其他字段 } bme280_calib_t; static bme280_calib_t g_bme_calib; static void calib_callback(void *user_data, json_token_type_t type, const char *start, size_t length) { #define PARSE_UINT16(key, field) \ if (length strlen(key) !strncmp(start, key, length)) { \ const char *val_start get_next_string_or_number(); \ g_bme_calib.field (uint16_t)strtoul(val_start, NULL, 10); \ } PARSE_UINT16(dig_T1, dig_T1); PARSE_UINT16(dig_T2, dig_T2); // ... 其他字段 }5. 性能实测与资源占用分析在 STM32F030F4P6Cortex-M0, 48MHz上使用 ARM GCC 10.2.1-Os编译minimal-json的实测资源占用如下项目数值说明Flash 占用1.12 KB仅json.cjson.h不含示例代码RAM 占用栈192 字节json_parser_t16B 状态机局部变量 编译器栈帧解析速度~120 KB/s解析 1KB JSON 文本平均耗时 8.3ms主频 48MHz最小支持 JSON12 字节{k:1}验证基础语法对比其他嵌入式 JSON 库cJSONFlash ~4.5 KBRAM堆至少 512B不满足零 malloc 要求jsmnFlash ~1.8 KBRAM ~120B但需预分配 token 数组灵活性稍差ultrajson专为 Python 设计不适用于裸机。minimal-json在资源极度受限场景如 Sub-GHz 无线节点RAM 仅 2KB中优势显著。其解析速度虽不及高度优化的jsmn但差距在可接受范围内15%且编程模型更直观。6. 常见问题排查与工程建议6.1 典型错误场景与修复现象根本原因解决方案JSON_ERROR_SYNTAX在字符串末尾输入 JSON 缺少终止}或]或 UART 接收丢包增加接收超时检测确保获取完整报文添加 CRC 校验JSON_ERROR_DEPTH_EXCEEDED配置的max_depth过小或 JSON 嵌套过深检查 JSON 结构增大max_depth需同步评估栈空间重构 JSON 降低嵌套回调未收到预期STRINGKey 名称匹配逻辑有误如未跳过空白符、大小写敏感使用strcasecmp或预转换为小写严格按 JSON 规范处理空白符isspace()数值解析错误如端口为 0JSON_TOKEN_NUMBER回调中未正确跳过 key 的:后空白符导致start指向空白在回调中先扫描start后的空白符再调用strtol6.2 工程最佳实践输入验证前置在调用json_parse前用strlen或预存长度确认输入非空且长度合理如 1KB避免解析器处理超长垃圾数据。错误恢复机制解析失败后不应简单重启而应记录错误位置parser.pos尝试从下一个可能的 JSON 起始符{或[重新同步。内存安全若json_parser_t位于全局或静态存储确保src指向的内存在其生命周期内有效避免解析栈上临时字符串函数返回后失效。配置宏管理将JSON_DECODE_STRINGS、JSON_MAX_DEPTH等定义在json_config.h中便于不同项目定制。一位在工业网关项目中使用该库的工程师反馈将max_depth从默认 4 改为 6 后成功解析了包含 5 层嵌套的 Modbus TCP 配置 JSON且未引发任何栈溢出——这印证了其深度限制机制的有效性与可配置性。

更多文章