简介:一份已经编译好的 Jsoncpp 动态库与静态库,面向 Windows 平台使用 Visual Studio 进行原生 C++ 开发的工程师。Jsoncpp 是轻量且常用的 C++ JSON 解析与序列化库,资源省去了下载源码、配置 CMake 或从零编译的流程,拿到后即可把头文件和库文件接入项目,适合需要快速实现 JSON 读写能力、又不想折腾构建环境的中初级开发者。压缩包共 6 个文件,包含 2 个头文件、2 个静态库(lib)和 2 个动态库(dll),其中 debug 与 release 版本各一套,既能满足调试期符号检查,也能在发布时选择更精简的运行时配置;整体大小约 892KB,轻量、易放入现有工程。资源已有 519 人学习/下载。对采用 MSVC 的 C++ 项目来说,这套库文件非常实用:链接对应 lib 后即可编译,运行时随程序带上同名 dll;由于编译时没有按 C 格式导出,接口形态更贴合 C++ 代码风格,在继承、重载、异常等场景下使用更顺手,能明显降低集成 Jsoncpp 的试错成本。
1. 先弄明白:动态库和静态库对Jsoncpp意味着什么
Jsoncpp是我在C++项目里处理JSON时最常用的库:轻、稳定、编译快,接口集中在Json::Value、Json::Reader、Json::Writer这些类上,而且不依赖第三方库。但正因为太轻,很多人拿到源码后根本不关心"静态库还是动态库"这件事,随手塞进工程编译,直到要对外分发SDK、要升级库版本、或者刚编出来的程序在别人机器上跑不起来,才意识到这个选择比想象中重要得多。
把Jsoncpp编成动态库还是静态库,表面上是CMake里一个开关的差别,实际牵扯到符号导出、运行时库匹配、部署方式和ABI兼容一整条链路。这篇文章把我自己在Windows和Linux两侧编译、链接、调试Jsoncpp静态库和动态库的实测经验完整写出来,哪些环节最容易翻车、翻车之后怎么定位,都会展开讲,适合正在做C++项目集成、或者准备把一个JSON解析能力作为库交付给团队使用的开发者参考。
1.1 两种库形态的本质差别
静态库(.a/.lib)在链接阶段会被整个复制进最终的可执行文件,之后运行时不再需要这个库文件。动态库(.so/.dll)在编译链接时只记录符号引用,真正加载发生在程序启动阶段,由系统的动态加载器从指定路径找到对应的库文件。
可以这么理解:静态库就像你把一本书的某个章节复印下来,自己装订进笔记本,书本身之后放哪都无所谓;动态库则像论文里的参考文献引用,写论文时只需要写编号,评审时系统会按照编号去图书馆取书,一旦图书馆里没有这本书或者换成了不同版本,评审现场立刻出问题。
对Jsoncpp这种小型库来说,静态库带来的额外体积其实可以忽略不计,但它也意味着以后想升级JSON解析能力,必须重新编译整个可执行程序。动态库则相反,你可以在不重新编译主程序的前提下单独替换libjsoncpp.so或jsoncpp.dll,前提是保持接口和ABI兼容。
1.2 Windows下.lib文件的双重身份
交叉编译过Windows版本的朋友应该有个感受:其他平台区分静态库和动态库很直观,.a就是静态,.so就是动态,Windows却经常被.lib搞晕。因为Visual Studio工具链里,.lib既可能是真正的静态库文件,也可能只是动态库的导入库(Import Library),它本身没有任何实际代码,只是记录了DLL导出了哪些符号,供链接器在编译阶段使用。
Jsoncpp用CMake构建时,如果编译动态库,会同时产出jsoncpp.dll和jsoncpp.lib,后者就是导入库;如果编译静态库,产出的是类似jsoncpp_static.lib的文件。很多人把动态库旁边的.lib当成静态库直接参与链接,结果编译链接全过,程序一运行就报"找不到jsoncpp.dll"。我在帮同事排查时见过不止一次这种问题,所以第一步先把这个概念理清,后面才不容易乱。
2. 编译静态库:一条省心路线和四个翻车点
2.1 Linux下用CMake十分钟编出libjsoncpp.a
先拉源码,jsoncpp的GitHub仓库地址是https://github.com/open-source-parsers/jsoncpp,用git clone或者直接下载release包都行。源码结构不复杂,核心代码在src/lib_json/目录下,头文件在include/json/下。
编译静态库最简单的方式是CMake:
git clone https://github.com/open-source-parsers/jsoncpp.git cd jsoncpp mkdir build-static && cd build-static cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF .. cmake --build . --target jsoncpp_static构建完成后,在build-static/lib/目录下能找到libjsoncpp.a。如果你用的版本里target名称不是jsoncpp_static,别慌,不同小版本的CMakeLists.naming略有差异,最稳妥的办法是直接cmake --build .全部构建,然后去lib/目录看生成了什么,或者查看CMakeCache里有没有BUILD_SHARED_LIBS这个变量。
如果你不想用CMake,手工编译其实也就三条命令:
cd jsoncpp g++ -std=c++11 -c src/lib_json/*.cpp -Iinclude -O2 ar rcs libjsoncpp.a *.osrc/lib_json/下其实只需要编译json_reader.cpp、json_value.cpp、json_writer.cpp这三个文件,用通配符*.cpp会一并处理,简单粗暴但不会错。
2.2 Windows下MSVC静态库的运行时库匹配问题
Windows上用CMake生成静态库,命令和Linux大同小异,只是需要指定Visual Studio生成器:
cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_SHARED_LIBS=OFF .. cmake --build . --config Release但在MSVC环境下,有一个Linux完全不会遇到的坑:运行时库(Runtime Library)的/MT和/MD必须匹配。静态库本身用什么运行时库编译,使用方也必须用相同的模式链接,否则会直接报LNK2038: mismatch detected for 'RuntimeLibrary'。
原因说起来也不复杂:MSVC的/MT表示静态链接C/C++运行时库,/MD表示动态链接运行时库,两种模式下new、delete、内存分配器和部分标准库对象的内部布局存在差异。Jsoncpp静态库如果用自己的运行时库分配了一块内存,而你的应用程序用另一套运行时库去delete它,轻则崩溃,重则内存损坏。更隐蔽的是,有些项目CMake构建时没有报错,但程序运行到某个边界就异常退出。
解决办法是在使用方CMake里统一设置:
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")然后在编译Jsoncpp静态库时保持同样的设置,两边都是/MT或两边都是/MD。
2.3 链接静态库的顺序与-fPIC教训
Linux下链接静态库,最经典的错误是"明明指定了-ljsoncpp,却依然报undefined reference"。八成原因是库的位置写在了源文件之前:
# 这样写会报链接错误 g++ -std=c++11 -Iinclude -Lbuild-static/lib -ljsoncpp main.cpp -o demo # 正确姿势是把库放在所有源文件之后 g++ -std=c++11 -Iinclude main.cpp -Lbuild-static/lib -ljsoncpp -o demo原因在于链接器对静态库的处理方式:它只会从.a中提取那些能够解析"当前尚未解决符号"的成员。如果链接器先处理-ljsoncpp,此时目标文件的符号还没被读进来,它不知道要提取哪些成员,等后面处理main.cpp时,Json::Value这些符号已经来不及补救了。
另外一个容易被忽略的点是-fPIC。如果你打算把编译好的libjsoncpp.a再链接进自己的动态库,编译静态库时必须加上-fPIC位置无关代码选项。否则链接自己的.so时会报类似relocation R_X86_64_PC32 against symbol ... can not be used when making a shared object的错误。用CMake时可以直接加:
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBUILD_SHARED_LIBS=OFF ..这个开关会确保静态库里的目标文件都是位置无关的,既可以直接链接进可执行文件,也可以继续封装进别的动态库。
3. 编译动态库:-shared只是起点,SONAME才是关键
3.1 Linux下编译Jsoncpp动态库并理解SONAME
动态库的CMake编译过程看起来和静态库很像,只是开关变成开:
mkdir build-shared && cd build-shared cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. cmake --build .构建完成后去lib/目录,你会发现产物不是孤零零一个文件,而是一组带符号链接的文件:
libjsoncpp.so -> libjsoncpp.so.1 libjsoncpp.so.1 -> libjsoncpp.so.1.9.5 libjsoncpp.so.1.9.5这就是SONAME机制在起作用。CMake在构建动态库时会根据VERSION和SOVERSION属性,自动生成这种带版本号的符号链接。编译器在链接阶段通过-ljsoncpp找到libjsoncpp.so这个无版本号文件,但可执行文件运行时实际记录的是SONAME,也就是libjsoncpp.so.1。
为什么要在意这个细节?因为它决定了你的程序升级库版本时是否需要重新编译。只要你的新库仍然叫libjsoncpp.so.1,只是从1.9.5升到1.9.6,那所有链接过旧版本的程序都可以直接用新库替换,无需动编译过的可执行文件。如果当初编译时没有设置SONAME,直接把动态库命名为libjsoncpp.so,那程序依赖记录的就是这个具体文件名,每次版本变化都要重新链接。
手工编译动态库时,也可以手动模拟这套逻辑:
g++ -std=c++11 -fPIC -c src/lib_json/*.cpp -Iinclude -O2 g++ -shared -Wl,-soname,libjsoncpp.so.1 -o libjsoncpp.so.1.9.5 *.o ln -s libjsoncpp.so.1.9.5 libjsoncpp.so.1 ln -s libjsoncpp.so.1 libjsoncpp.so注意中间那条-Wl,-soname,libjsoncpp.so.1不能省,这是JSON解析库能否平滑升级的关键。
3.2 Windows下DLL、导入库与导出宏的分工
Windows下Jsoncpp动态库构建成功后,你会在Release目录同时看到jsoncpp.dll和jsoncpp.lib。这里的.lib就是导入库,不是静态库。它们的分工是这样的:编译链接时代码引用Jsoncpp函数,链接器通过jsoncpp.lib知道这些符号在DLL里,于是在可执行文件里写入一段"去加载jsoncpp.dll并调用这个函数"的描述;程序启动后,加载器再按照搜索路径找到jsoncpp.dll完成真正的调用。
这里就涉及到Windows特有的导出宏问题。Jsoncpp的头文件在Windows下会用__declspec(dllexport)和__declspec(dllimport)来标注哪些符号需要导出或导入,具体由json/config.h里的宏控制。编译Jsoncpp动态库本身时,CMake会在jsoncpp_lib这个target上自动添加导出宏定义;使用方通过CMake的target_link_libraries链接时,这些编译选项会一起传递过来,所以最省心的方式就是两边都用CMake target。
但如果你在Windows下手写Makefile或者直接用IDE配置工程,就得自己处理好导入宏,否则可能出现编译通过但链接时符号类型不匹配、或者生成的exe在运行时报找不到DLL入口点这类问题。我个人的经验是:Windows下手动管理这些宏特别容易遗漏,宁可把Jsoncpp的CMake工程作为子目录add_subdirectory加入进来,让CMake帮你管这堆细节。
3.3 运行时找不到libjsoncpp.so的三种解法
动态库编译成功只是第一步,运行才是大头。Linux下最经典的报错长这样:
./demo: error while loading shared libraries: libjsoncpp.so.1: cannot open shared object file: No such file or directory这条错误信息你肯定见过,原因也很好理解:编译时链接器根据-L参数找到了libjsoncpp.so,但程序运行时,动态加载器不会再去看编译时的搜索路径,它只按默认路径和配置来找libjsoncpp.so.1。你的库安装在/usr/local/lib而系统默认搜索路径里没有它,自然就找不到。
临时解决办法是设置环境变量:
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH ./demo这种方式适合自己调试,不适合交付。更推荐的做法是在编译时给程序加上RPATH/RUNPATH,让程序优先从自己所在目录找库:
g++ -std=c++11 -Iinclude main.cpp -Lbuild-shared/lib -ljsoncpp -Wl,-rpath,'$ORIGIN' -o demo$ORIGIN表示可执行文件所在的目录,这样只要把demo和libjsoncpp.so.1放在同一个目录,程序无论被复制到哪台机器都能正常加载,这是我最常用的交付方式。注意$ORIGIN在shell里要用单引号包住,防止它被环境变量展开。
Windows相应对付方案简单一些,把jsoncpp.dll放到exe的同级目录即可,Windows搜索DLL时优先从exe所在目录开始,系统目录和PATH排在后面。如果开发调试阶段嫌麻烦,也可以把dll路径加进PATH,但我建议养成exe和dll同目录的习惯,以后发布不会手忙脚乱。
4. 实测:同一个程序,静态链接和动态链接差在哪里
4.1 ldd、文件大小与"能否拷贝即运行"
理论讲再多,不如直接把两种链接方式的结果摆出来对比。我写了一个最简单的解析程序:
#include <json/json.h> #include <iostream> #include <sstream> int main() { Json::Value root; Json::CharReaderBuilder builder; std::string errs; std::istringstream input(R"({"name":"jsoncpp","type":"library"})"); bool ok = Json::parseFromStream(builder, input, &root, &errs); if (ok) { std::cout << root["name"].asString() << std::endl; } return ok ? 0 : 1; }分别用静态库和动态库编译:
g++ -std=c++11 main.cpp -Iinclude -Lbuild-static/lib -ljsoncpp -o demo_static g++ -std=c++11 main.cpp -Iinclude -Lbuild-shared/lib -ljsoncpp -Wl,-rpath,'$ORIGIN' -o demo_shared两个可执行文件的大小都不大,jsoncpp本身也就几百KB级别,静态版比动态版多出的体积在实际项目中基本可以忽略。真正的差别在依赖关系上:
| 检查项 | demo_static | demo_shared |
|---|---|---|
| ldd结果中包含libjsoncpp | 不含 | 包含libjsoncpp.so.1 |
| 单独拷贝exe到其他目录 | 可运行 | 报找不到动态库 |
| exe同目录放上对应so后 | 不依赖 | 可运行 |
| 替换库文件版本 | 需要重新编译exe | 只要SONAME不变即可 |
所以"拷贝即运行"这件事,静态库确实省心,但也意味着库的bug修复无法在不重编的情况下生效。
4.2 换库版本、排查崩溃时的真实体感
我在实际项目中两个都长期用过,感受很直观。用动态库时,改一行Jsoncpp源码,只需重新编译库文件并替换安装,程序下次启动就是新逻辑,这在排查"同一个JSON在另一种编码环境里解析异常"时非常方便。用静态库时则要完整重新构建整个可执行文件并重新发版,步骤多,出错概率也高。
但动态库在排查崩溃问题时也有一个麻烦:如果发布版本做了符号剥离,gdb拿到的调用栈只有地址,没有函数名,你只能看到libjsoncpp.so.1这个模块长度。静态库或者带-g编译的动态库则在崩溃现场能直接看到Json::Value、readValue这些函数名。我通常会保留一份带调试符号的debug动态库,专门用来复现线上问题。
另外跨编译器场景要格外小心ABI兼容性。Jsoncpp大量使用std::string和std::vector,如果编译Jsoncpp的GCC版本与使用方不同,并且_GLIBCXX_USE_CXX11_ABI宏不一致,那么两者对std::string对象的内存布局认知是不同的,运行时传递字符串参数可能直接崩溃。这种问题最难查,因为编译和链接都不会报错。做对外SDK时,我一般会在分发说明里明确写清楚编译器版本和ABI宏设置,并额外提供一份静态库版本给无法对齐环境的使用方。
5. 选型建议:三类项目我分别怎么做
5.1 内部工具直接用静态库或amalgamation
如果你只是自己在公司内部用,或者做一个独立的小工具,我建议直接用静态库,甚至更省事一点,直接用Jsoncpp官方提供的amalgamation单文件方案。在Jsoncpp源码根目录执行:
python amalgamate.py会在dist/目录下生成一个jsoncpp.cpp和json/json.h,把这两个文件加进你的工程直接编译就行,完全不需要处理库链接、头文件路径、SONAME这些事。这种方式本质上是"源码级静态集成",既能享受静态库的自包含优点,又省掉所有库管理的麻烦。
唯一的限制是:如果项目里多个模块都这么干,且每个模块都包含一份jsoncpp.cpp,最终链接时会因为符号重复而冲突。所以它更适合单体应用或插件这种只有一处使用JSON解析能力的场景。
5.2 对外SDK才需要认真做动态库
如果你的项目需要把功能封装成SDK发给第三方团队,那就不能偷懒用amalgamation了。这时候动态库是更合适的选择,原因有几个:第一,第三方拿到SDK后不需要关心Jsoncpp的编译细节;第二,Jsoncpp如果出现安全修复或功能更新,你只发布一个新的动态库文件,对方替换即可,不用重新编译整个接入工程;第三,SDK内部使用的Jsoncpp符号不应该泄露给外部,动态库可以配合控制导出符号来隐藏内部实现。
发布时除了动态库本体,一定要带上头文件,并明确标注你使用的是哪个Jsoncpp版本。我见过太多次"SDK里带的头文件是1.8.x,但动态库是1.9.x,结构体定义对不上,解析出来的数据全错"的案例。版本号写在头文件里、写在库文件名里、写在文档里,三处一致,缺一不可。
5.3 一份简短的集成检查清单
最后把我每次集成Jsoncpp时都会过一遍的检查要点列出来,供你参考:
- 确认是静态库还是动态库,Windows下区分静态
.lib和导入库.lib - MSVC环境必须确认
/MT和/MD全局一致 - Linux下动态库一定要检查SONAME,别生成裸的
libjsoncpp.so - 发布程序前用
ldd检查依赖,确保目标机器能解析所有动态库 - 如果使用静态库链接进自己的动态库,编译静态库时开
-fPIC - 对外分发时提供debug版库或保留符号文件,否则崩溃栈查不动
- 跨工具链编译时,确认
_GLIBCXX_USE_CXX11_ABI一致,或提供静态库选项
这套检查清单其实也适用于其他C++第三方库,只是Jsoncpp的体积小、依赖少,踩坑之后更容易定位原因,很适合作为理解和掌握"动态库vs静态库"这套机制的第一个实验对象。等你在Jsoncpp上把这些问题理清楚,再去碰ONNX Runtime、Qt这类大型依赖库时,至少不会再因为基础概念混淆而浪费一整天时间。
本文还有配套的精品资源,点击获取