cJSON_Duplicate失效无法生成预期JSON字符串问题排查求助
需要创建包含多个列表条目的JSON字符串,实际场景中通过定时器ISR填充条目(示例用for循环模拟),但调用cJSON_Duplicate()和cJSON_Print()后无法得到预期的JSON字符串,同时add_list_entry()也会在定时器ISR的临界区内调用,需排查原因并给出修复方案。
void add_list_entry(cJSON *parent, int index, uint32_t x, uint32_t z, uint32_t t_h, uint64_t t_us) { // 创建代表列表项的对象 cJSON *entry = cJSON_CreateObject(); cJSON_AddNumberToObject(entry, "X", x); cJSON_AddNumberToObject(entry, "Z", z); cJSON_AddNumberToObject(entry, "t_h", t_h); cJSON_AddNumberToObject(entry, "t_us", (double)t_us); // 创建用于存储单个条目的数组 cJSON *array = cJSON_CreateArray(); cJSON_AddItemToArray(array, entry); // 将数组添加到父对象,使用递增索引作为字符串键 char key[11]; // 足以存储最大40亿的整数 snprintf(key, sizeof(key), "%d", index); cJSON_AddItemToObject(parent, key, array); } void json_simulate(void) { cJSON *root = cJSON_CreateObject(); // 根应为对象 char *json_output; int i = 0; for (i = 0; i < 200; i++) { add_list_entry(root, // 根JSON句柄 i, // 每个条目的元素计数器 TIM1->CNT, // X编码器输入 m_curr.dist, // Z距离 m_curr.t_h, // 秒级时间 TIM5->CNT); // 微秒定时器 } taskENTER_CRITICAL(); if (m_jsonroot != NULL) { cJSON_Delete(m_jsonroot); // 确保删除之前的数据(如果存在) } m_jsonroot = cJSON_Duplicate(root, 1); // 复制到全局JSON句柄 m_tx_msg = 1; // 设置全局更新标志 json_output = cJSON_Print(m_jsonroot); taskEXIT_CRITICAL(); free(json_output); // 释放cJSON_Print()的结果 cJSON_Delete(root); // 删除本地根对象 }
核心问题点
并发访问冲突
实际场景中ISR和json_simulate()任务会同时操作cJSON对象,但当前仅在复制m_jsonroot时加了临界区,add_list_entry()在ISR中的调用以及模拟代码的for循环修改root时未做互斥保护。cJSON的内部结构并非线程安全,并发修改会破坏链表或节点结构,导致复制、打印出的JSON损坏。JSON结构设计冗余(不符合预期)
每个列表项被包裹在单独的数组中,最终JSON结构为{"0": [{"X": ...}], "1": [{"X": ...}], ...},如果预期是一个包含所有条目的数组(如[{"X": ...}, {"X": ...}, ...]),这种设计完全不符合需求,会导致输出结构与预期不符。uint64_t转double的精度丢失
uint64_t类型的最大值超过了double的53位有效精度范围,强制转换后会丢失低位数据,导致t_us字段的值不准确。复制时机的线程安全问题
调用cJSON_Duplicate()时,如果root正被ISR修改,复制过程会读取到不完整或损坏的节点数据,导致m_jsonroot结构异常。
修复方案
1. 解决并发访问问题
- 全局互斥保护:所有修改cJSON对象的操作(包括ISR中的
add_list_entry()、json_simulate()中的for循环)都必须在临界区内执行,确保同一时间只有一个上下文(任务/ISR)操作JSON结构:- 在ISR中调用
add_list_entry()前,使用taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()包裹; - 模拟代码的for循环也需要放到临界区内,避免与ISR并发修改
root。
- 在ISR中调用
- 双缓冲机制(更优):准备两个独立的cJSON根对象,ISR固定往其中一个写,
json_simulate()任务复制另一个的内容,复制完成后切换缓冲区,避免临界区长时间占用影响ISR响应。
2. 修正JSON结构
如果预期是一个包含所有条目的数组,调整代码如下:
// 修改add_list_entry,直接将条目添加到父数组 void add_list_entry(cJSON *parent, uint32_t x, uint32_t z, uint32_t t_h, uint64_t t_us) { cJSON *entry = cJSON_CreateObject(); cJSON_AddNumberToObject(entry, "X", x); cJSON_AddNumberToObject(entry, "Z", z); cJSON_AddNumberToObject(entry, "t_h", t_h); // 用字符串存储uint64_t,避免精度丢失 char t_us_str[21]; snprintf(t_us_str, sizeof(t_us_str), "%llu", t_us); cJSON_AddStringToObject(entry, "t_us", t_us_str); cJSON_AddItemToArray(parent, entry); } // 修改json_simulate的root为数组 void json_simulate(void) { cJSON *root = cJSON_CreateArray(); char *json_output; int i = 0; taskENTER_CRITICAL(); for (i = 0; i < 200; i++) { add_list_entry(root, TIM1->CNT, m_curr.dist, m_curr.t_h, TIM5->CNT); } if (m_jsonroot != NULL) { cJSON_Delete(m_jsonroot); } m_jsonroot = cJSON_Duplicate(root, 1); m_tx_msg = 1; json_output = cJSON_Print(m_jsonroot); taskEXIT_CRITICAL(); free(json_output); cJSON_Delete(root); }
3. 修复uint64_t精度问题
将uint64_t类型的t_us转换为字符串存储,避免用double类型导致的精度丢失,如上述代码中使用snprintf()将t_us转为字符串后添加到JSON对象。
4. 确保复制操作的原子性
cJSON_Duplicate()必须在临界区内执行,同时保证在复制过程中没有其他上下文修改源对象root,避免读取到不一致的节点数据。
内容的提问来源于stack exchange,提问作者stdcerr

