乐于分享
好东西不私藏

嵌入式学习之插件-CJSON库

嵌入式学习之插件-CJSON库

嵌入式JSON封神库!cJSON从入门到精通,告别手写状态机的痛苦

做嵌入式联网、物联网、车载开发的工程师,几乎都踩过同一个坑:

设备上云对接MQTT,订阅主题收到云端下发的JSON——嵌套对象里套数组,数组里又塞对象,还掺着几个带\"\\的转义字符串。

常规的做法: 打开Keil撸了个switch-case状态机,改到第三版,转义字符的边界条件还是崩了,调试到半夜怀疑人生。

直到某天打开GitHub才发现,圈里早就有个“人手一份”的老库,作者2009年就开始维护,如今已经12.6k Star、3.5k Fork、49个Release,接进工程只需要复制两个文件——cJSON.ccJSON.h

它就是cJSON,嵌入式JSON领域的“缺省答案”,今天就从入门到精通,把它讲透,让你再也不用手写JSON解析器!

Part.01

嵌入式JSON的“轻量王者”

cJSON是一个用ANSI C(C89)编写的超轻量级JSON解析器,作者Dave Gamble给它的定位很“佛系”:“能把活干完的最笨的那种解析器”。

这句话看似谦虚,实则是最精准的工程哲学——不追花哨特性,不搞复杂抽象,接口就那几个,但每一个都扎实能用,完美适配嵌入式的资源限制。

cJSON核心信息一览(必记)

  • 作者:Dave Gamble(原作者),Max Bruckner、Alan Wang(当前维护者)

  • 代码量:1个.c文件 + 1个.h文件,无多余依赖,可直接复制使用

  • 语言标准:ANSI C(C89),兼容Keil、IAR、STM32CubeIDE等所有嵌入式编译器

  • 许可证:MIT协议,商用零负担,不用担心版权问题

  • 最新版本:v1.7.19(2025年9月)

  • 仓库地址:https://github.com/DaveGamble/cJSON(建议star收藏)

为什么嵌入式圈对它情有独钟?

如今JSON在嵌入式里早已不是“可选项”——MQTT消息载荷、HTTP REST对接、云平台上报、设备配置下发、本地OTA配置、Modbus扩展协议,到处都是JSON的身影。

而cJSON的优势恰好戳中嵌入式工程师的痛点:可移植、可审计、可替换内存分配器、可裁剪,哪怕是资源不算富裕的MCU,也能轻松跑起来。

Part.02

设计思路:为什么它能只用两个文件?

打开cJSON的源码,你会发现一个核心逻辑:把复杂度往数据结构里藏。整个库的所有功能,都围绕一个极简的结构体展开:

typedef struct cJSON {    struct cJSON *next;      // 双向链表:下一个节点    struct cJSON *prev;      // 双向链表:上一个节点    struct cJSON *child;     // 子节点:用于嵌套(对象/数组)    int type;                // 类型标志(bit-flag),区分7种JSON类型    char *valuestring;       // 字符串值(string类型用)    int valueint;            // 整数值(已废弃,用cJSON_SetNumberValue替代)    double valuedouble;      // 数值(number类型用)    char *string;            // 键名(对象的key用)} cJSON;

很多人会疑惑:JSON有null、true、false、number、string、array、object七种类型,为什么这里只用一个结构体?

答案藏在type字段里——它是位标志(bit-flag)。同一个结构体,通过type的不同位区分七种JSON类型;再通过next/prev组成双向链表,用来表示数组和对象的成员;通过child指针形成树状结构,实现JSON的嵌套。

这种设计有一个小代价:按索引访问数组是O(n²)复杂度(因为是链表遍历),所以官方专门提供了cJSON_ArrayForEach宏,实现O(n)高效遍历。

但对嵌入式场景来说,这个取舍非常值:

  • 不需要动态数组、哈希表,内存占用可控

  • 内存分配次数少,适合嵌入式资源紧张的场景

  • 代码量极小,全部源码可人工审计,调试方便

  • 逻辑简单,任何嵌入式工程师读10分钟源码,就能看懂它的运行原理

Part.03

核心能力:四类API,覆盖所有日常场景

cJSON的API设计极其简洁,核心就四类:解析、构建、遍历、类型判断,学会这四类,就能应对嵌入式里99%的JSON场景。

3.1 解析:一行代码,搞定JSON字符串解析

最常用的场景:收到MQTT消息、HTTP响应等JSON字符串,把它转换成可访问的数据结构。

// 核心解析函数:将JSON字符串转为cJSON对象树cJSON *json = cJSON_Parse(string);// 解析失败判断(必做,避免空指针崩溃)if (json == NULL) {    const char *err = cJSON_GetErrorPtr(); // 获取解析错误位置    if (err != NULL) {        printf("解析错误位置: %s\n", err);    }    return -1;}// ... 这里使用解析后的json对象 ...cJSON_Delete(json); // 用完必须释放,避免内存泄漏

补充说明:

  • 如果字符串不一定是零终止(比如从socket直接接收的buffer),用cJSON_ParseWithLength(string, len)更安全,指定字符串长度。

  • 多线程场景下,cJSON_GetErrorPtr()有竞态风险,推荐用cJSON_ParseWithOpts(value, &return_parse_end, 1),通过return_parse_end返回错误位置,线程安全。

3.2 构建:先Create,再Add,轻松生成JSON

反向场景:设备上报数据、下发指令,需要构建JSON字符串发送给云端或其他设备。核心逻辑:先创建根对象,再往里面添加字段、数组、子对象。

// 1. 创建根对象(JSON对象)cJSON *root = cJSON_CreateObject();// 2. 往根对象添加字段(字符串、数字、布尔值)cJSON_AddStringToObject(root, "device""STM32F407");    // 字符串cJSON_AddNumberToObject(root, "temperature"25.6);     // 数字cJSON_AddBoolToObject(root, "online"1);               // 布尔值(1=true,0=false)// 3. 添加数组(比如设备传感器列表)cJSON *sensors = cJSON_AddArrayToObject(root, "sensors");// 往数组添加元素cJSON_AddItemToArray(sensors, cJSON_CreateNumber(1));cJSON_AddItemToArray(sensors, cJSON_CreateNumber(2));cJSON_AddItemToArray(sensors, cJSON_CreateNumber(3));// 4. 生成JSON字符串(紧凑格式,适合网络传输)char *out = cJSON_PrintUnformatted(root);printf("%s\n"out);// 5. 释放资源(必须做,避免内存泄漏)free(out);              // 生成的字符串需要手动释放cJSON_Delete(root);     // 根对象释放,整个对象树都会被释放

极其容易踩的坑(必看)

一个cJSON*节点,一旦被AddItemToObjectAddItemToArray加到父节点,所有权就交给父节点了,你不能再对它单独调用cJSON_Delete,否则会导致double free(双重释放)。只要删除根节点,整棵对象树都会被一起释放。

3.3 遍历:ArrayForEach宏,高效遍历数组/对象

前面提到,数组是链表结构,按索引访问效率低(O(n²)),官方提供了cJSON_ArrayForEach宏,实现O(n)高效遍历,同时适配数组和对象(对象内部也是链表存储)。

// 1. 获取数组(假设root里有一个叫"sensors"的数组)cJSON *array = cJSON_GetObjectItemCaseSensitive(root, "sensors");// 2. 遍历数组cJSON *item = NULL;cJSON_ArrayForEach(item, array) {    // 判断元素类型,获取值    if(cJSON_IsNumber(item)) {        printf("传感器ID: %f\n", item->valuedouble);    }}

如果是遍历对象,逻辑完全一样,把数组换成对象即可:

cJSON *obj = cJSON_GetObjectItemCaseSensitive(root, "device_info");cJSON *item = NULL;cJSON_ArrayForEach(item, obj) {    // item->string 是键名,item->valuestring/valuedouble 是值    printf("%s: %s\n", item->string, item->valuestring);}

3.4 类型判断:永远用Is系列函数,避免踩坑

type == cJSON_Number重点提醒:不要直接比较

因为type是位标志,可能叠加cJSON_IsReference等标志位,直接比较会判断错误。官方提供的cJSON_IsXXX系列函数,会自动做NULL检查 + 类型检查,安全又省心。

// 正确写法:获取键值并判断类型cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "device");if (cJSON_IsString(name) && (name->valuestring != NULL)) {    printf("设备名称: %s\n", name->valuestring);}

这个写法还有一个好处:cJSON_GetObjectItemCaseSensitive允许NULL输入并安全返回NULL,cJSON_IsString收到NULL也会直接返回0,不需要层层写NULL判断,代码更简洁。

Part.04

上手最小示例:3分钟跑起来,零门槛集成

cJSON的集成难度几乎为零,最暴力的方式:复制cJSON.ccJSON.h两个文件到工程,编译通过就能用,兼容Keil、IAR、STM32CubeIDE、PlatformIO等所有嵌入式开发环境。

下面是一个完整的最小示例,新建json_test.c,直接复制就能运行:

#include<stdio.h>#include<stdlib.h>#include"cJSON.h"intmain(void){    // 模拟MQTT收到的JSON payload    const char *payload =        "{"        "  \"device\":\"mcu-01\","        "  \"temp\":26.5,"        "  \"alerts\":[\"high_temp\",\"low_battery\"]"        "}";    // 1. 解析JSON    cJSON *root = cJSON_Parse(payload);    if (!root) {        return -1// 解析失败直接返回    }    // 2. 获取各个字段并打印    cJSON *device = cJSON_GetObjectItemCaseSensitive(root, "device");    cJSON *temp   = cJSON_GetObjectItemCaseSensitive(root, "temp");    cJSON *alerts = cJSON_GetObjectItemCaseSensitive(root, "alerts");    if (cJSON_IsString(device)) {        printf("设备ID: %s\n", device->valuestring);    }    if (cJSON_IsNumber(temp)) {        printf("当前温度: %.1f\n", temp->valuedouble);    }    // 3. 遍历alerts数组    cJSON *alert;    cJSON_ArrayForEach(alert, alerts) {        if (cJSON_IsString(alert)) {            printf("告警信息: %s\n", alert->valuestring);        }    }    // 4. 释放资源    cJSON_Delete(root);    return 0;}

编译运行(Linux/macOS):

gcc -std=c89 cJSON.c json_test.c -o json_test./json_test

输出结果:

设备ID: mcu-01当前温度: 26.5告警信息: high_temp告警信息: low_battery

Keil集成更简单:把两个文件加到工程分组,配置好头文件路径,直接#include "cJSON.h"就能用,不需要额外配置。

补充优化:如果想节省一次malloc(嵌入式内存宝贵),可以用cJSON_PrintPreallocated(item, buffer, length, format),把JSON输出到自己预留的静态缓冲区,完全绕过动态分配,适合MCU场景。

Part.05

嵌入式重点:自定义内存分配器

嵌入式开发中,很多场景不用标准库的mallocfree,比如FreeRTOS用pvPortMalloc、RT-Thread用rt_malloc,或者自己实现内存池。

cJSON贴心地留了全局Hook(钩子),可以轻松替换内存分配器,让cJSON的内存操作和你的工程保持一致。

// 1. 定义自己的内存分配和释放函数(示例:FreeRTOS)void *my_malloc(size_t size) {    return pvPortMalloc(size);}void my_free(void *ptr) {    vPortFree(ptr);}// 2. 初始化cJSON的内存Hook(必须在所有cJSON调用之前)void cjson_init(void) {    cJSON_Hooks hooks;    hooks.malloc_fn = my_malloc;  // 自定义分配函数    hooks.free_fn   = my_free;    // 自定义释放函数    cJSON_InitHooks(&hooks);}

注意事项

cJSON_InitHooks必须在所有cJSON函数调用之前设置好,之后不能再修改,否则会导致线程不安全。通常建议放在main函数开头,初始化一次即可。

这个机制的价值:让cJSON全部走RTOS的内存池,避免和libc的堆冲突;也可以接到自己的内存池里,方便做内存使用统计和泄漏监控,嵌入式工程里这个能力非常重要。

Part.06

同类选型:什么时候选cJSON,什么时候不选?

JSON在C语言生态里不止cJSON一个选择,不同场景适合不同的库,这里做一个清晰的选型参考,避免用错工具:

应用场景

推荐库

选择原因

资源正常的MCU(Cortex-M3/M4/M7),做MQTT/HTTP对接

cJSON

API友好、构建/输出方便、调试简单,兼顾轻量和实用性

资源极紧(Cortex-M0,RAM不到8KB)

jsmn

不建对象树,只返回token偏移,内存占用极低(几KB)

Linux/RTOS上做复杂业务,需要严格类型检查

Jansson / json-c

功能更全、错误处理更完善,适合复杂场景

只读JSON、不生成JSON

parson / jsmn

比cJSON更轻,专注解析功能,资源占用更少

需要JSON Schema、JSON Patch功能

cJSON + cJSON_Utils

cJSON官方附带的cJSON_Utils库,支持JSON Pointer和Patch

一句话总结:

如果你在做ESP32、STM32F4/F7/H7这类中高端MCU的联网项目,cJSON几乎是默认答案;如果你在Cortex-M0里用16KB RAM挣扎,优先考虑jsmn。

Part.07

局限性:别把cJSON神化,这些坑要避开

cJSON虽好,但不是万能的。官方文档里坦率列出了它的局限性,工程上踩过坑的都懂,提前了解能少走很多弯路:

  1. \0不支持字符串内部的字符:因为cJSON的字符串是零终止的,一旦遇到\0就会认为字符串结束。如果协议里要传二进制数据,不能直接塞进JSON的string字段,必须先做Base64编码。

  2. 仅支持UTF-8编码:大部分情况下不会主动拒绝非法UTF-8,只会原样透传。如果输入的JSON是GBK编码,解析会出问题,需要提前转成UTF-8。

  3. 浮点数仅保证IEEE754双精度:其他浮点数实现可能能跑,但不承诺正确。另外,浮点字面量的最大长度是63字符,超过会解析失败。

  4. 嵌套深度限制1000层:默认通过CJSON_NESTING_LIMIT限制嵌套深度为1000,防止递归解析导致栈溢出(嵌入式栈本来就小),可在编译时修改。

  5. 默认不线程安全:只有满足3个条件才线程安全:不使用cJSON_GetErrorPtrcJSON_InitHooks只在主线程启动前调用、所有cJSON调用期间不调用setlocale。多线程场景建议加互斥锁。

  6. 默认不区分大小写:历史原因,原始API不区分键名的大小写(比如“Device”和“device”会被认为是同一个键)。严格遵循JSON标准,一定要用带CaseSensitive后缀的函数(如cJSON_GetObjectItemCaseSensitive)。

  7. 按索引访问数组效率低(O(n²)):链表结构的固有缺陷,几百个元素的数组遍历,务必用cJSON_ArrayForEach宏。

  8. 重复key只返回第一个:JSON标准允许对象有重复成员,cJSON解析不会报错,但GetObjectItem只会返回第一个key的值。如果协议要求key唯一,需要在应用层自己校验。

这些局限性在嵌入式日常场景(如设备上云、简单数据上报)中大多无伤大雅,但如果用它做网关、中间件、跨语言协议层,一定要提前规避这些问题。

Part.08

总结:嵌入式工程师的必备工具

cJSON之所以能在嵌入式圈“人手一份”,核心在于它的“务实”——不追求完美,却能精准解决嵌入式JSON解析/构建的核心痛点:轻量、可移植、易用、商用无负担。

对于大多数嵌入式联网项目来说,cJSON的功能足够用,而且学习成本极低,花1小时学会核心API,就能告别手写状态机的痛苦,把时间花在更核心的业务逻辑上。

最后提醒:收藏cJSON的GitHub仓库,遇到问题看官方文档(最权威);集成时记得复制两个文件,用完释放资源,避开文中提到的坑,就能轻松上手。

到此,cJSON库的相关知识将讲解完毕了,后续需要大家多多训练

如果这篇教程对你有帮助,记得点赞 + 收藏!关注我,STM32 开发少走弯路

👇 关注「嵌入式老司机」,获取更多嵌入式干货为便于技术交流,创建了嵌入式技术交流群,可尽情探讨ADC、DMA、USART等常用接口及外设技术,后台回复“加群”即可