
简介JsonCpp源码非编译库集成包面向需要在C项目中直接引入JsonCpp、又不希望链接预编译库的开发者解决源码级集成与快速验证的实际需求特别适合Visual Studio工程内直接使用。资源共38个文件压缩包约1.11MB以14个h头文件和5个cpp源文件为主体头文件对应json目录下的接口声明cpp实现JSON解析、写入等核心逻辑同时包含sln解决方案、vcproj测试工程与ReadMe说明其余为示例编译产物目录结构清晰便于按需提取所需文件。已有958人学习下载可满足配置解析、对象序列化、网络协议数据组装等常见场景适合有一定C基础、希望避免外部依赖的开发者。包内附带JsonCppTest测试项目展示了核心读写与值类型模块在实际工程中的组织方式能够帮助读者更快理解源码布局并接入自有项目省去自行整理文件的麻烦。1. 为什么有人宁可啃源码也不想引入编译库先说结论JsonCpp这库本身不大但围绕“怎么把它弄进工程”这件事圈子里一直分成两派。一派是直接下载编译好的动态库或静态库include进去、链上lib就完事。另一派就是标题说的这条路——把源码文件直接放进项目里参与编译不单独生成库文件。我见过很多从嵌入式转过来的工程师或者做代码审计、需要固化第三方依赖版本的项目组几乎清一色选源码直编。原因很实在。编译库方式的两大痛点第一是交叉编译环境下的ABI兼容问题。你拿x86的工具链编出来的.a或者.so放到ARM板子上根本跑不起来。而嵌入式设备上最常用的就是交叉编译工具链目标平台的库版本、编译选项、甚至libstdc的版本都可能和你本机不一致。一旦库是用不同编译选项编出来的链接阶段可能不报错运行阶段直接段错误或者崩溃排查起来能把人折磨疯。第二是平台部署的碎片化问题。有些项目部署到客户现场对方机器上缺动态库运行环境或者操作系统的glibc版本太老你编出来的库依赖的最低GLIBC版本比目标系统还新直接加载失败。这种问题在工控机、老旧服务器、定制Linux系统上特别常见。而源码直编是和你的主程序一起用同一套编译参数编出来的天然规避了运行时依赖。源码直编真正解决的问题是把第三方依赖的“不可控”变成“可控”。源码拿过来你可以审计每一行代码JsonCpp早期版本有过安全漏洞代码审查在安全要求高的项目里是硬需求你可以修改内部实现比如替换内存分配器、关闭异常支持以减小体积你可以固定版本并且做本地Patch团队其他成员clone仓库后不需要额外安装任何东西直接编译整个工程。再加上JsonCpp这库本身也就那么几个cpp文件整个库的源码量不大直接加入到工程里编译时间增加不到一秒对构建系统几乎零负担。这种情况下再单独搞一个库文件出来反而是多此一举。2. JsonCpp源码目录拆解哪些文件必须保留哪些可以扔掉既然决定源码直编第一步不是复制粘贴而是搞清楚JsonCpp源码包里哪些文件必须进工程哪些只是给编译库流程服务的辅助文件。很多新手直接把整个仓库拖进项目结果一堆用不上的.cmake、.pl脚本和测试代码也跟着编编译报错后一脸懵。2.1 真正必须的文件其实只有这几个我以目前最常用的JsonCpp 1.9.x版本为例这个版本也是目前最稳定的强烈建议新项目直接用1.9.5之后的小版本核心源码分两部分头文件目录include/json/下面有json.h、json-forwards.h、value.h、reader.h、writer.h、allocator.h、assertions.h、config.h等。源码目录src/lib_json/下面有json_reader.cpp、json_value.cpp、json_writer.cpp、json_tool.h。就这么简单。其中json.h是统一入口实际使用中你只需要在业务代码里#include json/json.h其他的头文件会被它内部嵌套引用。这里要注意一个文件容易被忽略——json_valueiterator.h。它一般也在include/json/目录下但在某些旧版本里它被放在了src/lib_json/内部或者根本没有独立头文件而是内联在value.h里。如果你发现编译的时候报ValueIterator相关符号找不到优先检查这个文件是不是在include路径里。2.2 可以扔掉的部分和原因下面这些文件和目录源码直编时完全不需要加入工程内容位置能不能扔原因CMakeLists.txt和*.cmake根目录、src目录扔这些是构建库文件用的源码直编绕过了CMake的库构建流程version.h.ininclude/json扔但要注意这是CMake configure时自动生成的模板源码直编时需要手动处理版本宏后面会细说makefiles/目录根目录扔针对Unix平台的Makefile支持test/目录根目录扔测试代码和你的项目无关doxybuild.py、amalgamate.py根目录扔文档生成和合并工具脚本pkg-config/目录根目录扔给系统库管理用的src/jsonsrc/旧版本中存在扔旧版残留目录1.9.x已移除.github/、doc/根目录扔CI和文档有一个特殊情况——json_config.h。某些版本里这个文件是json_config.h.in或者json_config.h.cmake本质是CMake根据平台宏定义生成的文件。源码直编时这个文件不存在但json.h内部会#include它通常在config.h里间接引用。所以你必须手动创建一个json_config.h内容大致是基础平台宏的开关。以Linux和Windows最常见的场景为例// json_config.h 手动创建 #ifndef JSON_CONFIG_H #define JSON_CONFIG_H // 如果你的编译器支持C11保持这个定义开启 #define JSON_HAS_RVALUE_REFERENCES 1 // 如果是MSVC当不使用预编译头时建议关闭 // #define JSON_NOEXCEPTION 1 // 根据需要决定 // #define JSON_USE_EXCEPTION 0 // 同上 #endif如果你不创建这个文件编译时会遇到大量关于JSON_HAS_RVALUE_REFERENCES、JSONCPP_DLL等宏未定义的报错。2.3 版本差异的坑JsonCpp有两个大版本分支0.10.x和1.x。两个分支的json.h接口略有差异最明显的是1.x把很多内部实现从*.h文件里挪到了*.cpp里并且增加了JSONCPP_STRING、Json::String这些类型别名。如果用0.10.x的头文件配1.x的源码或者反过来链接时会出现一堆Json::Reader::parse之类的符号找不到或者类型不匹配的诡异编译错误。所以务必确认你整个工程里只存在一个版本的JsonCpp源码。我踩过最离谱的坑是一个老项目的公共第三方库目录里已经放了一份0.10.x的json.h而新模块又引入了1.9.x的源码结果头文件冲突编译出了几百个重复定义错误排查到怀疑人生。如果你也是接手老项目先全局搜一下json.h和json_value.cpp确保没有重复。3. 源码直编的完整实操步骤从下载到编译通过理论说清楚了下面直接上操作。我默认你的场景是手头有个项目可能是Linux下的Makefile工程也可能是CMake工程或者Windows下的Visual Studio工程你想把JsonCpp源码融进去。3.1 获取源码与版本锁定源码直编的前提是先锁定一个固定版本不要用master分支。原因很简单——master可能随时有变动过两天clone下来的代码和你上周的代码不一样构建不稳定。推荐做法是去GitHub Releases页面下载指定tag的源码包比如1.9.5。下载后解压把include/json整个目录和src/lib_json整个目录复制到你的工程目录下。我习惯这样组织目录结构your_project/ ├── third_party/ │ └── jsoncpp/ │ ├── include/ │ │ └── json/ │ │ ├── json.h │ │ ├── json-forwards.h │ │ └── ... │ └── src/ │ └── lib_json/ │ ├── json_reader.cpp │ ├── json_value.cpp │ ├── json_writer.cpp │ └── json_tool.h ├── src/ │ ├── main.cpp │ └── ... └── CMakeLists.txt这样单独建一个third_party/jsoncpp目录的好处是将来要升级版本时直接替换这个目录不影响其他代码而且代码审查时扫第三方依赖也方便。3.2 头文件引用路径和命名空间使用时在你的业务代码里#include json/json.h注意是带json/目录前缀的因为json.h内部会#include json/reader.h之类如果include搜索路径只指向include/目录那么带前缀的写法才能正确解析内部引用。然后设置编译器头文件搜索路径Makefile:CPPFLAGS -I$(PROJECT_ROOT)/third_party/jsoncpp/includeCMake:include_directories(${PROJECT_SOURCE_DIR}/third_party/jsoncpp/include)Visual Studio: 在“C/C - 常规 - 附加包含目录”里添加上面的include路径所有JsonCpp的类都在Json命名空间下使用时Json::Value、Json::Reader、Json::FastWriter这些。3.3 把源码加进编译队列的三种方式方式一CMake推荐最省心如果你项目本来就用CMake直接在CMakeLists.txt里追加set(JSONCPP_SOURCES ${PROJECT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_reader.cpp ${PROJECT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_value.cpp ${PROJECT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_writer.cpp ) add_executable(your_target src/main.cpp ${JSONCPP_SOURCES} )这样三个cpp文件就和你的业务代码一起编译。不需要额外链接任何库因为JsonCpp的实现代码全在这里面了。方式二Makefile在Makefile里把三个源文件加进SRCS变量SRCS src/main.cpp \ third_party/jsoncpp/src/lib_json/json_reader.cpp \ third_party/jsoncpp/src/lib_json/json_value.cpp \ third_party/jsoncpp/src/lib_json/json_writer.cpp CPPFLAGS -Ithird_party/jsoncpp/include CXXFLAGS -stdc11 all: main g -o main $(SRCS) $(CPPFLAGS) $(CXXFLAGS)方式三Visual StudioVS里右键项目 - 添加 - 现有项选中三个cpp文件加入项目即可。注意VS默认会把文件按“编译单元”处理直接编译就行。3.4 C标准版本的选择JsonCpp 1.9.x要求C11及以上建议直接开C11或C14不要开C98。原因不只是JsonCpp内部用了move构造、auto等C11特性还因为从1.7.x开始JsonCpp默认就是用JSON_HAS_RVALUE_REFERENCES来开启移动语义支持不开C11这个宏就没意义。如果项目里因某些原因被锁定在C98老嵌入式工具链上挺常见那就必须用旧版JsonCpp 0.10.x并且要手动处理一堆宏不推荐除非有外设驱动的硬约束。编译前我在自己工程里加了一段验证编译环境的话图个安心#if __cplusplus 201103L #error JsonCpp requires C11 or higher. Please enable C11 mode. #endif3.5 验证是否编译成功的Demo放一个最小可用示例。假设main.cpp里做一次JSON解析和序列化#include json/json.h #include iostream #include string int main() { // 构造一个JSON字符串 std::string rawJson R({name:JsonCpp,version:1.9,features:[direct,source]}); // 解析 Json::CharReaderBuilder builder; Json::Value root; Json::String errs; std::istringstream stream(rawJson); bool ok Json::parseFromStream(builder, stream, root, errs); if (!ok) { std::cerr parse error: errs std::endl; return -1; } // 读取字段 std::cout name: root[name].asString() std::endl; std::cout version: root[version].asDouble() std::endl; // 序列化 Json::StreamWriterBuilder writerBuilder; std::string output Json::writeString(writerBuilder, root); std::cout serialized: output std::endl; return 0; }如果这段代码能编译通过、运行正常说明源码直编的环境已经OK。注意parseFromStream和CharReaderBuilder是1.x版本推荐的解析方式老代码里常见的Json::Reader reader; reader.parse(...)在1.9里仍然可用但官方标了deprecated新代码不建议写。4. 嵌入式场景下的裁剪与内存优化聊源码直编绕不开嵌入式这个典型场景。我最初选择这个方案就是因为手头的嵌入式Linux项目交叉编译环境太折腾。热搜词里也有“嵌入式内核源码”估计不少朋友是冲着这个方向来的。4.1 关闭异常处理缩小体积很多嵌入式交叉编译工具链默认关闭了异常支持-fno-exceptions或者为了减小镜像体积选择不开RTTI。JsonCpp默认使用异常来报告错误但这部分可以裁剪。在json_config.h或者编译宏里做如下设置#define JSON_NOEXCEPTION 1 // 关闭异常抛出 #define JSON_USE_EXCEPTION 0 // 不再使用try/catch这种情况下JsonCpp内部出错时不会抛异常而是通过返回值或errs字符串报告错误。你的业务代码就要改成检查返回值的方式不要依赖try/catch。典型改造如下// 不推荐依赖异常 // try { // Json::Value root reader.parse(rawJson); // } catch(...) { ... } // 推荐返回错误信息 Json::CharReaderBuilder builder; Json::Value root; Json::String errs; if (!Json::parseFromStream(builder, stream, root, errs)) { // 错误处理 return -1; }关闭异常之后库的代码体积能减少约10%~15%在flash紧张的MCU或者精简Linux系统里这个优化挺客观。4.2 定制内存分配器JsonCpp内部使用Json::Value节点来存储所有数据每个节点都是一棵树。对于大JSON或者高频率解析场景节点会频繁创建和销毁默认走malloc/free在长时间运行的系统里可能导致内存碎片。JsonCpp提供了Value::setAllocator接口让你注入自定义分配器。实际编程里我在一个长期运行的采集程序里用了一个简单的内存池#include json/allocator.h class PoolAllocator : public Json::Allocator { public: static void* allocate(size_t size) { // 从预分配的内存池中分配 return pool_malloc(size); } static void deallocate(void* p) { // 归还到内存池 pool_free(p); } }; // 初始化时调用全局生效 Json::Value::setAllocator(PoolAllocator::allocate, PoolAllocator::deallocate);这里要特别提醒setAllocator是全局静态设置不是线程安全的必须在程序入口处设置一次之后再设置就是未定义行为。另外设置的allocator必须和deallocator配对使用否则会出现释放错误。4.3 源码直编后的大小实测以ARM Cortex-A7交叉编译环境为例我用arm-linux-gnueabihf-g编译-Os优化级别关闭异常后的JsonCpp三个cpp文件加起来编出来的代码段大约在80~100KB左右数据段不到10KB。这个体量对于嵌入式Linux设备完全能接受比引入整个Boost.JSON或RapidJSONRapidJSON是header-only编译依赖模板膨胀严重要轻量不少。如果还想再缩小可以裁剪掉json_writer.cpp里的几个Writer实现比如StyledStreamWriter、CompressedStreamWriter这些只用其中一两个。删之前确认你的业务代码没有用到对应的类删完测试一次序列化路径。不过大多数情况下这三个cpp全量编进去也就几百KB没必要过度优化。5. 源码直编最常见的六个坑与排查方法最后这部分我从自己源码直编的实际经历出发整理了六个高频坑。这些坑在编译库方式下很少出现偏偏在源码直编时集体爆发。5.1 坑一重复定义或链接错误表现链接阶段报multiple definition of Json::Value::Value()...或者undefined reference to Json::Value::...。原因重复定义通常是你工程里其他地方已经有一个JsonCpp的.cpp或.a被链接进来了。我遇到过最典型的是——公共库目录下还有一个json_value.cpp被Makefile通配符匹配进来了。undefined reference通常是源码文件没被加进编译列表或者版本头文件与源码不匹配。排查链路全局搜索整个工程用find . -name json_*.cpp列出来确认只有一份。检查构建系统里json_reader.cpp、json_value.cpp、json_writer.cpp是否作为编译单元参与了链接。检查json.h头文件版本与三个cpp文件版本是否一致——看文件开头的注释1.9.x开头会标注版本号。5.2 坑二宏定义不一致导致编译报错表现编译时出现类似JSON_DLL_BUILD_IN_LIBRARY未定义、JSON_API为空或者class JSON_API Value报错。原因JsonCpp的导入导出宏JSON_API在Windows DLL场景下才有意义静态编译时需要置空。源码直编通常直接编译源码不生成DLL所以必须保证JSON_API被定义为空字符串。解决方式在编译宏里加入-DJSON_DLL0Linux下通常不需要因为默认就是空Windows下更稳妥的做法是在json_config.h里手动定义// Windows下源码直编生成静态链接 #define JSON_DLL 0 #define JSON_API这里的逻辑是JSON_DLL0告诉JsonCpp当前不是作为动态库编的JSON_API就不会被展开成__declspec(dllexport)或dllimport避免跨模块符号导入导出的各种怪问题。5.3 坑三C11 vs C98 混编表现error: nullptr was not declared in this scope或者std::move找不到。原因JsonCpp 1.9.x的头文件如果遇到C98编译器会退回到旧接口但源码里的json_value.cpp里有大量C11代码新旧不匹配直接编译错误。排查方式确认编译标准统一不要出现头文件用C98的编译选项、源码用C11的编译选项这种怪事。gcc编译时检查__cplusplus宏echo | gcc -dM -E -stdc11 - | grep __cplusplus # 输出#define __cplusplus 201103L如果输出是199711L说明编译器没开C11直接改编译参数。5.4 坑四字符串与JSON编码问题表现root[name].asString()返回中文乱码或者解析时遇到非UTF-8编码的字节直接报错。原因JsonCpp对字符串底层支持的是UTF-8不负责编码转换。如果你的数据源是GBK编码直接往里塞就会乱码。源码直编和编译库方式在这个问题上表现一致但源码直编方便你修改源码做处理这也是一个优势。经验方案我试过两种做法——一是数据进入JsonCpp之前先转成UTF-8推荐用iconv或自己写个转换函数二是解析时用Json::CharReaderBuilder的strictMode关闭但乱码的本质问题没解决。转码是正路别在库内部打主意。另外Json::StreamWriterBuilder默认输出时不会对非ASCII字符做转义它会原样输出UTF-8字节测试时留意终端编码。5.5 坑五多线程程序里的Json::Value共享表现多线程同时读写同一个Json::Value对象程序偶尔崩溃或者数据错乱。原因JsonCpp的Json::Value不是线程安全的一个Value对象被多个线程同时读写内部引用计数和树节点会乱掉。源码直编时这个坑尤其隐蔽因为编译库方式通常会让你想到“链接了带线程模型的库”而源码直编容易默认“所有代码都是我自己的应该安全”。正确处理方式每个线程维护自己的Json::Value不共享如果需要共享加读写锁std::shared_mutex或者平台锁全局配置类只读共享不变更。具体到我写的采集程序里解析线程只负责生成Json::Value序列化线程拿到的是std::string不直接共享Json::Value对象从根上避开这个坑。5.6 坑六源码直编后无法升级版本这个算流程问题不算技术问题但特别影响长期维护。很多团队源码直编之后直接把整个third_party/jsoncpp目录塞进git之后再也不管过两年想升级发现没有记录当时改了哪些宏、加过哪些Patch。推荐做法在third_party/jsoncpp/目录下放一个README.md记录当前版本号如1.9.5获取地址GitHub tag URL本地改动的所有Patch比如修改了json_config.h、裁剪了哪些代码为什么选择这个版本例如因为某个已知bug在1.9.5修复了这样下次升级时你的改动清单一目了然不会出现升级后“哎呀之前修的问题又冒出来了”的情况。6. 源码直编后的一些调试技巧和心得体会前面的内容覆盖了从选型到部署的完整链路最后分享几个我在源码直编实战中沉淀的调试技巧。技巧一加日志锁点定位JsonCpp内部错误JsonCpp源码直编的最大优势是你可以直接往源码里加日志。比如解析失败时想定位到底卡在哪个token可以在json_reader.cpp的readToken函数里临时加一段fprintf(stderr, ...)。编译库方式下你根本没机会看到内部状态源码直编后放几个锁点再删掉排查效率翻倍。技巧二用-DJSON_USE_EXCEPTION0作为开发阶段的双保险前面提到了嵌入式裁剪时关异常其实非嵌入式开发阶段我也建议开着这个宏跑一遍全部单测——它让你发现自己代码里哪些地方错误处理不完善因为之前依赖异常跳出去了关了异常后这些错误会通过各种奇怪的方式暴露出来。提前发现问题总比上线后崩在用户机器上强。技巧三内存分配器与Valgrind结合检查连接了自定义内存分配器之后常规的Valgrind内存泄漏检测工具可能失效或者误报因为内存池的释放时机和默认的free不同。调试阶段可以先不启自定义分配器用默认的malloc跑Valgrind确定无泄漏和越界后再开启内存池。反向操作会让你陷入“内存池本身泄漏”的假象。最后提一点个人的切身体会源码直编这条路第一次搭建的过程可能比直接链接编译库多花半天到一天时间但后续的每次交叉编译、每次部署到陌生环境、每次安全审计这半天时间都会加倍省回来。尤其是如果你经常在嵌入式Linux、工控机和普通x86服务器之间切换目标平台源码直编的收益会非常明显。不妨按上面这套步骤试一次走通一遍之后你大概率不会再想回到编译库的方式。本文还有配套的精品资源点击获取