
简介在C项目中直接使用JsonCpp源码而不额外编译链接库是许多开发者在轻量集成或代码改造时更倾向的接入方式。资源包正是为此整理压缩包内共38个文件整体大小仅1.11兆字节便于保存和分发。核心部分包含14个头文件和5个C实现文件覆盖了JsonCpp的读写解析功能此外还有Visual Studio的解决方案与工程配置以及编译调试时生成的中间文件能够在示例工程中直接查看源码调用关系。资源以直接嵌入项目为目标避免了配置静态库或动态库的环节适合希望快速引入JSON处理能力、或需要修改JsonCpp内部逻辑的开发者。附带的JsonCppTest工程直观展示了json_reader、json_value、json_writer等模块的协作方式可作为入门JsonCpp源码结构的辅助参考。目前已有958人学习下载对于正在选择JsonCpp接入方式的C开发者来说是一份轻量且实用的参考资源。 JsonCpp 这个 C 的 JSON 解析库很多人拿到手第一反应是找现成的 .so / .a / .dll或者用包管理器装一份再通过链接参数把库接进来。但我这几年做项目越来越喜欢直接抓 JsonCpp 源码塞进自己工程里不编译成独立库也不跑安装脚本直接把 .cpp 文件加到构建里就开干。这么做在嵌入式环境、跨平台交付和老代码维护里的收益非常明显。这篇文章就把源码直引的完整流程、文件依赖、CMake/Makefile/单文件三种集成方式以及编译宏和踩坑记录都列出来给同样有JsonCpp 源码直用需求的同学一份可以直接抄的作业。1. 为什么用源码直引而不是编译库1.1 什么样的场景我才会选源码直引先说清楚不是所有情况都适合源码直引。如果你的项目已经有统一的包管理器或者团队有持续集成环境那直接用编译库完全没问题。但当你遇到下面这几种情况编译库方案往往比源码直引麻烦得多目标平台没有现成的预编译包交叉编译工具链又老又特殊产品需要交付给客户二次开发客户那边的编译环境和你的不完全一致老项目的构建体系很脆弱多一个动态库就多一份链接和部署成本嵌入式环境里磁盘和内存都紧张不想引入一个额外的 .so只是需要一个轻量 JSON 读写功能不想为此引入一大堆依赖。我做过的一个嵌入式网关项目就是这样。系统是 ARM 平台rootfs 裁剪得很厉害没有现成的 json 库可用交叉编译工具链版本也比较旧。如果走编译库路线我得先交叉编译 JsonCpp再把产物部署到板子上还要担心 ABI 兼容。最后干脆把 JsonCpp 源码直接放到 third_party 目录下和主工程一起编译。一次配置之后在哪个环境构建都一样反而少了很多烦恼。1.2 编译库和源码直引差异其实很大拿一张表来说明两者的核心差异这样比较直观对比维度预编译/编译库方式源码直引方式安装配置需要下载、安装、配置路径拷贝源码目录即可链接关系需要处理动态库/静态库链接顺序直接编译进目标无额外链接平台可移植性每个目标平台都要单独出库随项目源码一起编译版本控制平台库版本可能与需求不一致版本完全由自己锁定排错难度符号、链接、加载阶段的错误较难定位编译错误直接可查部署体积动态库方式多一个产物体积编译器可裁剪体积更可控二次交付客户拿到代码还得自己编库源码在手开箱即编JsonCpp 本身代码量不大源码直引的成本主要是第一次配置时那十几分钟。好处是之后每次构建都少一道编译库的工序依赖关系也简单。而且源码直引还有个隐藏优势你可以把 JsonCpp 的代码和你的业务代码放在同一个构建图里编译时采用统一的优化选项甚至通过链接时的垃圾回收裁掉没用到的函数。2. 先搞清楚 JsonCpp 源码里哪几个文件在干活2.1 完整源码目录长什么样不管是 1.8.x 还是 1.9.xJsonCpp 的源码结构基本一致。核心文件就集中在两个地方头文件在 include/json 目录下实现文件在 src/lib_json 目录下。jsoncpp/ ├── include/ │ └── json/ │ ├── json.h // 统一入口日常 include 它就够了 │ ├── value.h // Json::Value 类型定义 │ ├── reader.h // 解析器接口 │ ├── writer.h // 输出器接口 │ ├── features.h // 解析特性控制 │ ├── config.h // 编译宏与平台兼容配置 │ └── version.h // 版本号部分源码包需要 CMake 生成 └── src/ └── lib_json/ ├── json_reader.cpp // 字符串/流 - Json::Value ├── json_value.cpp // Json::Value 核心实现 ├── json_writer.cpp // Json::Value - 字符串 └── json_tool.h // reader/writer 共用的内部工具头1.9.x 版本里还会多出 allocator.h、assertions.h、memorystream.h 等头文件但实现文件基本还是那三个 cpp。理解这个目录结构是后面一切操作的前提。2.2 最小文件集直引需要哪几个文件源码直引理论上只需要把上面提到的三个 cpp 文件和 include/json 整个目录加进工程再确保 include path 指向 include 目录就行。有一个容易忽略的文件叫 json_tool.h它没有放在 include 目录而是和 cpp 文件放在一起主要被 reader 和 writer 内部引用。源码直引时一定要保证编译器能找得到它。如果你用 CMake 或 Makefile 把所有 cpp 源文件直接加入编译那它在当前源码目录下编译不会出问题。如果你做的是单文件打包方案后面会讲到需要小心处理这个头文件的 include 路径。2.3 版本选择C11 还是老版本直接影响引入方式JsonCpp 1.9.x 开始要求 C11 及以上。如果你的项目还在用老的 C98 环境或者交叉编译工具链对 C11 支持不完善建议直接用 1.8.4。这个版本在 C98 下能编译接口也基本一致只是少了一些新特性。版本差异还会影响一个常见问题直接 clone GitHub 仓库时include/json/version.h 可能不存在。因为新版本里 version.h 需要在 CMake configure 阶段从 version.h.in 生成。如果你直接把 clone 下来的源码塞进工程会报version.h not found。解决办法有三种下载官方发布版的 tar 包发布包里已经生成好、先跑一次 CMake 生成、或者手动创建一个 version.h。这个我在后面排查部分还会重点提。3. 三种源码直引方式总有一种适合你的工程3.1 方式一CMake 工程直接把源码加进 target如果你的工程是 CMake 组织的最简单的方式是给 JsonCpp 源码单独建一个静态库 target再让主 target 链接它。比如把 JsonCpp 源码放在third_party/jsoncpp目录下add_library(jsoncpp_src STATIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_reader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_value.cpp ${CMAKE_CURRENT_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_writer.cpp ) target_include_directories(jsoncpp_src PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/jsoncpp/include ) target_compile_features(jsoncpp_src PUBLIC cxx_std_11)然后在主 target 上直接target_link_libraries(my_app PRIVATE jsoncpp_src)即可。这样头文件路径通过 PUBLIC 传递主业务代码直接#include json/json.h就能用。如果你不想多出一个 target也可以直接把三个 cpp 加到现有 target 的源文件列表里add_executable(my_app 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 ) target_include_directories(my_app PRIVATE third_party/jsoncpp/include )我个人推荐前一种独立 target方式。因为 JsonCpp 的东西比较独立单独成 target 层次清晰以后如果要换版本或者换目录改动范围更小。这种做法的原理也很好理解源码直引并不是真的不编译而是让 JsonCpp 的源码和你自己的代码共享同一个构建系统统一被编译成目标文件最后一起链接成最终产物。你不需要对外暴露一个独立的库文件这也是它省事的原因。3.2 方式二Makefile 手动编译三个 cpp对于没有 CMake、只有老式 Makefile 或者手动脚本构建的工程可以直接把三个 cpp 文件加到编译命令里。以下面这条 g 命令为例g -stdc11 -Ijsoncpp/include \ main.cpp \ jsoncpp/src/lib_json/json_reader.cpp \ jsoncpp/src/lib_json/json_value.cpp \ jsoncpp/src/lib_json/json_writer.cpp \ -o demo在 Makefile 里可以这样组织JSONCPP_DIR ./third_party/jsoncpp JSONCPP_SRCS $(JSONCPP_DIR)/src/lib_json/json_reader.cpp \ $(JSONCPP_DIR)/src/lib_json/json_value.cpp \ $(JSONCPP_DIR)/src/lib_json/json_writer.cpp JSONCPP_INC -I$(JSONCPP_DIR)/include CPPFLAGS $(JSONCPP_INC) CXXFLAGS -stdc11 -Wall OBJS main.o json_reader.o json_value.o json_writer.o demo: $(OBJS) g $^ -o $ json_reader.o: $(JSONCPP_DIR)/src/lib_json/json_reader.cpp g $(CXXFLAGS) $(CPPFLAGS) -c $ -o $ json_value.o: $(JSONCPP_DIR)/src/lib_json/json_value.cpp g $(CXXFLAGS) $(CPPFLAGS) -c $ -o $ json_writer.o: $(JSONCPP_DIR)/src/lib_json/json_writer.cpp g $(CXXFLAGS) $(CPPFLAGS) -c $ -o $这里有个基本功编译阶段加-I指向 include 目录让#include json/json.h能解析到链接阶段不需要额外指定任何库因为 JsonCpp 的实现已经被一并编译进去了。很多人在编译库方案里经常遇到的 undefined reference 链接错误在这种方式下基本不会出现只要你把所有 cpp 文件都加进编译。3.3 方式三用官方脚本压成单文件嵌入式场景最省事有些场景下你连多文件都嫌麻烦比如嵌入式工程只允许你往源码里加一两个文件或者你希望把第三方依赖压缩到最小。JsonCpp 官方提供了一个 amalgamate 脚本可以把整个库压成一个jsoncpp.cpp和一个json/json.h整个库就只剩一个头文件和一个 cpp 文件。在 JsonCpp 源码仓库根目录下执行python amalgamate.py运行完会在 dist 目录下生成打包好的文件。使用方式非常直接g -stdc11 -Idist main.cpp dist/jsoncpp.cpp -o demo生成后的dist/目录里json/json.h就是统一的头文件jsoncpp.cpp是包含三个 cpp 实现的大文件。这个方案的原理其实就是 Unity Build把所有实现文件合并到一个编译单元里既减少了文件数量有时还能略微降低编译开销。我见到很多嵌入式工程就是用这种单文件方式直接把 jsoncpp.cpp 拖到工程里编译。如果你不想用 Python 脚本也可以手动做同样的合并新建一个jsoncpp_unity.cpp依次#include json_reader.cpp、#include json_value.cpp、#include json_writer.cpp然后把 include 路径配置好就行。不过我建议能用官方脚本就用官方脚本手动合并容易漏掉编译宏和内部头文件的依赖关系。4. 编译宏、链接错误与排查记录4.1 这几个编译宏提前搞清楚能省三小时源码直引踩坑大多出在编译宏上。先看 JsonCpp 的 config.h里面有几个关键的宏控制它的编译行为。JSONCPP_DLL宏这是个导出宏主要用于 Windows 平台上动态库的导出和导入。编译库方式下如果处理不好就会出现__declspec(dllexport)相关的链接错误。源码直引时不需要定义它config.h 默认把JSONCPP_API定义为空所有符号正常导出就是最普通的静态编译。JSONCPP_USE_SECURE_MEMORY宏定义后 JsonCpp 在释放内存前会用std::fill清空数据防止敏感信息残留在内存里。代价是性能略降。这个宏必须在所有使用 JsonCpp 的源文件里保持一致否则可能出现解析行为不一致的问题。一般的文件服务器、日志系统可以不开启涉及密钥、令牌解析的场景建议开启。JSON_NOEXCEPT宏控制异常相关行为。JsonCpp 默认使用异常如果你在非 C11 或者禁用异常的环境里用要专门测试解析错误时的行为。我自己的建议是除非已经跑通默认配置否则不要在源码直引的第一轮就尝试关异常不然会把自己代码 bug和JsonCpp 异常路径问题混在一起。还有一个容易忽略的是编译器标准选项。1.9.x 要求在编译 JsonCpp 时至少开启 C11。如果某个 cpp 文件用了老标准编译会报出一堆看不懂的模板错误。统一在构建脚本里加上-stdc11能避免很多麻烦。4.2 常见错误速查表下面这张表我整理自实际工程中的报错记录基本覆盖了源码直引 80% 的坑错误现象根本原因解决办法fatal error: json/json.h: No such file or directoryinclude path 没有指向include/目录在编译选项中加-Ijsoncpp/includefatal error: version.h: No such file or directory部分源码仓库缺少由 CMake 生成的 version.h下载正式发布包或先跑一次 CMake 生成undefined reference to Json::Value::...三个 cpp 没有全部参与编译或者链接时漏了对象文件确认 json_reader.cpp、json_value.cpp、json_writer.cpp 全部加入构建__declspec(dllexport)或JSONCPP_EXPORT相关错误定义了 JSONCPP_DLL误入动态库导出路径源码直引不要定义 JSONCPP_DLLC 版本相关的模板编译错误编译器低于 C11或者没有开-stdc11升级编译器或改用 JsonCpp 1.8.4和另一个 JSON 库符号冲突项目中已有别的库也定义了Json命名空间移除重名库或者给 JsonCpp 做命名空间隔离第三个错误非常典型我见过有人只加了 json_value.cpp 进工程结果编译能通过链接时满屏 undefined reference。因为 Reader 和 Writer 也依赖 Value 的实现三个文件缺一不可。如果你用 CMake 方式最好直接列全这三个 cpp不要用我只需要读 JSON所以只加 reader这种思维JsonCpp 内部耦合度很高分开加反而容易出错。5. 源码直引之后我实测过的优化与建议5.1 代码使用层面的两个优化点源码直引带来的一个好处是你可以和 JsonCpp 的实现更亲近也就更容易发现它的性能特点。我实测下来有两个点对工程性能影响比较明显。第一复用Json::CharReaderBuilder。如果你在一个循环里反复解析多个 JSON 报文每次都要创建CharReaderBuilder和std::istringstream开销会累积。不如把解析逻辑封装一下让 builder 复用Json::CharReaderBuilder readerBuilder; std::string errs; std::istringstream iss; Json::Value root; for (auto raw : jsonMessages) { iss.clear(); iss.str(raw); if (!Json::parseFromStream(readerBuilder, iss, root, errs)) { // 解析失败处理 } // 处理 root }第二判断某个 key 是否存在时优先用isMember()而不是直接访问下标。直接下标访问不存在的 key 会在 Value 内部构造一个空值会造成额外的类型创建和后续判断麻烦。写法上注意一下就能避免无谓的开销。如果你在嵌入式环境里做的是小体积还可以在编译阶段给 JsonCpp 的 cpp 文件单独开优化选项比如配合-ffunction-sections -fdata-sections和链接时的-Wl,--gc-sections把没链接到的函数和数据删掉能省一点体积。这在源码直引下做非常顺手因为 JsonCpp 的符号就在你的编译图里。5.2 不是所有项目都适合源码直引最后也想给源码直引泼一点冷水。如果项目规模很大、多个模块都会用到 JsonCpp且你们已经有成熟的公共库版本管理流程那按编译库方式统一管理反而更好。源码直引最怕的是每个模块都拷贝一份 JsonCpp 源码最后出现多个模块各自编译一份、符号不同、行为不一致的混乱。另外如果你的产品需要遵循某些严格的使用第三方组件的合规审查把源码以特定目录形式放进来通常比用系统库更容易追踪版本。这一点在交付类项目里反而成了源码直引的加分项。我自己的习惯是只要是我主导的工具型小项目、嵌入式工程、或者要交付给客户二次开发的代码JsonCpp 一律源码直引。第一次配置花十分钟之后所有环境都干净。最后一个小技巧如果你和我一样经常在多平台之间切建议在third_party/jsoncpp目录里把发布包的 LICENSE 一起保留。别删。一个是合规需要另一个是以后换版本的时候对比文件差异有原始发布包的版本号在旁边能省不少查档案的时间。本文还有配套的精品资源点击获取