简介:面向在 Ubuntu 22.04、GCC 11.4.0 环境下编译 WonderTrader 的 C++ 开发者与量化交易系统搭建人员,这份依赖库用于缓解第三方组件版本不一致、头文件缺失以及构建路径配置繁琐等常见问题,可服务于策略回测、实盘部署和二次开发等场景。压缩包共约 2000 个文件,以 1856 个 hpp 和 144 个 h 头文件为主,覆盖 JSON 解析、格式化输出、时间处理及基础工具等常用模块,整体约 14.5MB,体积轻便,便于随工程归档和迁移。资源描述还提供了 MyDependsGcc=/deps/wtcpp 的环境变量示例,并示意把 CMake 中的 MyDeps 指向该目录,读者可据此核对自身构建配置,减少反复查找依赖与调试路径的时间。已有 240 人学习下载,适合需要快速补齐 WonderTrader 编译依赖、排查头文件引用和路径问题的中高级 C++ 用户参考。
1. 在 Ubuntu 上把 WonderTrader 依赖库拉下来,第一次 cmake 多半会停在红字上
Could not find Boost这行红字,几乎是每个在 Ubuntu 上第一次碰 WonderTrader 依赖库的人都会遇到的提示。这套依赖库本质上是把 boost、openssl、zlib、curl 这类编译期要头文件、链接期要.so、运行期还要能找到.so的第三方组件,按固定版本和固定目录结构准备好,让 C++ 侧的交易框架用git拉下来、一次cmake就能完成配置。它适合两类人:一类是想在本机把回测进程跑起来的量化开发者,另一类是准备改动 C++ 接口层、已经被链接错误带来过麻烦的工程师。
2. 依赖清单与版本选型:boost 为什么不能用 apt 里那份
先把一句话说在前面:这套依赖库真正难的在于让每条依赖都固定在对应目录、对应版本上,能在一次 configure 里全部对上,而“把库装上”本身反而最简单。装错位置的 boost 比没装 boost 更难排查,因为编译期能找到头文件、链接期能找到库,运行期却会莫名崩溃。
2.1 三张清单:编译期、链接期、运行期各缺什么
同一个依赖在三个阶段的身份不一样,缺哪一环报的错完全不同。头文件缺失报的是fatal error: boost/xxx.hpp: No such file or directory,属于预处理阶段;库文件缺失报的是undefined reference to ...,属于链接阶段;运行期找不到.so报的是error while loading shared libraries,属于装载阶段。这三类错误在排查时不能混为一谈,否则会白查半天。
我一般会先列一张表,把每条依赖在三个阶段的需求写清楚,再去动手:
| 依赖 | 在框架里的角色 | 编译期 | 链接期 | 运行期 | 版本策略 |
|---|---|---|---|---|---|
| boost | 智能指针、日期时间、文件系统、asio 网络 | 头文件 | system/thread/filesystem/chrono 等 | .so需可寻址 | 锁定具体小版本,自编译 |
| openssl | 通信加密、哈希摘要 | 头文件 | libssl、libcrypto | 同上 | 跟随系统大版本,别跨 1.1.1 与 3.x |
| zlib | 行情与数据文件压缩 | zlib.h | libz | 同上 | 系统包即可 |
| curl | HTTP 数据拉取 | 头文件 | libcurl | 同上 | 系统包即可 |
| libiconv | 部分接口的编码转换 | 头文件 | libiconv | 同上 | 系统包或自编译 |
| sqlite3 | 本地数据缓存 | 头文件 | libsqlite3 | 同上 | 系统包即可 |
| python3-dev | 上层绑定的扩展模块 | Python.h | 无 | 需匹配解释器 | 与系统 python3 小版本一致 |
表里唯一需要“较真”的是 boost。其余几条用apt装开发包基本够用,因为它们对外接口变化慢、ABI 也相对固定。boost 不一样,它由几十个独立库拼成,版本之间既有接口变化,也有“某个组件从需要链接变成 header-only”这种结构性调整,锁不住版本就等着踩坑。
2.2 boost 锁版本的真实理由:apt 给你的永远不是你想要的
Ubuntu 各发行版仓库里的 boost 版本是跟着系统生命周期走的,20.04 给的是 1.71 一线,22.04 给到 1.74 一线,更早的版本给得更低。你在开发机上apt install libboost-all-dev装完编译通过,换一台机器或者换一个基础镜像,find_package(Boost 1.70 REQUIRED)直接判定不满足,工程连配置阶段都走不过去。
第二个理由是 ABI 与编译器的一致性。自己编译 boost 时用的是系统g++,标准库是 libstdc++,这跟主工程的编译环境天然对齐;而某些预编译包是用clang+ libc++ 出的,混进 libstdc++ 的工程里,链接期可能还能过,运行期在字符串和异常处理上出问题,这类故障最难定位。所以自编译一份、放在工程自己的目录下,是很值得做的一件事。
第三个理由是“多版本共存”。你手上可能同时有两个基于 WonderTrader 的项目,一个停留在旧版本接口,一个跟进新版本,它们对 boost 的要求不一样。把 boost 装在/usr/local下,两个项目就会互相覆盖;装在工程目录里,各自独立,谁也影响不到谁。
2.3 目录布局约定:include、lib、bin 三段式
依赖库落地时,我习惯按下面这个结构摆,后面所有的环境变量和 CMake 参数都围绕它来:
third_party/ ├── include/ # 所有头文件,boost/ openssl/ curl/ 平铺在内 ├── lib/ # 所有 .so 与 .a,链接期 -L 指向这里 ├── bin/ # 少数带可执行产物的依赖 ├── src/ # 源码与编译中间产物,打完包可以删 └── share/include与lib分开是硬性要求,因为大多数find_package模块默认就是在这两个子目录里找东西。把源码目录当成 prefix 传进去是最常见的误操作:源码树里确实有boost/头文件,但没有lib/libboost_system.so,结果就是编译期通过、链接期全部失败。
bin目录别忽略。有些依赖会在安装时往bin里放工具程序,如果你只拷贝include和lib而漏掉bin,某些构建脚本在调用这些工具时会直接报“命令不存在”,而报错信息里根本看不出跟依赖库有关。
3. Ubuntu 上的落地流程:apt 装什么、git 拉什么、b2 怎么传参
这一章是能直接照着敲的部分。整套流程分四步:系统底座、拉取依赖、编译 boost、把路径喂给 CMake。四步之间是有依赖关系的,前一步没做干净,后一步的报错会把你带到完全错误的方向上去。
3.1 系统底座:该装的装,不该装的一律不装
# 基础工具链,缺一个后面都会卡住 sudo apt-get update sudo apt-get install -y \ build-essential \ cmake \ git \ pkg-config \ wget \ unzip # 除 boost 之外的运行期/编译期依赖,直接用系统包 sudo apt-get install -y \ libssl-dev \ zlib1g-dev \ libcurl4-openssl-dev \ libsqlite3-dev \ libicu-dev \ python3-devbuild-essential一次带齐gcc、g++、make、libc6-dev,比逐个装省事。pkg-config很多人会漏,但 openssl、curl 这类库的find_package在部分 CMake 版本上会退化到走pkg-config查询,没有它就会出现“明明装了却找不到”的现象。
关键点是:不要装libboost-all-dev。只要它进了系统,/usr/include/boost和/usr/lib/x86_64-linux-gnu/libboost_*.so就会一直在那里,find_package的搜索顺序很容易先命中系统那份,然后你自编译的那份就成了摆设。如果已经装了,先清掉再继续,别指望靠环境变量压住它。
3.2 用 git 拉取依赖库并确认拿到的分支
# 统一放在家目录下的工作区,路径别带空格和中文 mkdir -p ~/work && cd ~/work # 拉取依赖库,--depth 1 只取最新一次提交,省时间 git clone --depth 1 <依赖库地址> wt-deps cd wt-deps # 确认当前落在哪个提交上,把哈希记进构建日志 git log -1 --format='%H %ci %s' # 如果依赖库用 tag 管理版本,列出来确认拿对了没 git tag --list | tail -n 20--depth 1会让仓库变成浅克隆,拿不到完整历史,但日常构建够用;如果后面需要切到某个旧 tag,得先git fetch --unshallow补全历史,否则会报unknown revision。这个坑很隐蔽,因为报错信息只提到 tag 不存在,不会告诉你是因为浅克隆。
把git log -1的哈希记进构建日志,是给未来的自己留一份追溯记录。半年后回来看“当时到底是哪版依赖编出来的二进制”,没有这个哈希就只能靠猜。
3.3 boost 的 bootstrap 与 b2 参数逐条说明
cd ~/work/wt-deps/src/boost # 生成 b2 构建器,--prefix 指向我们约定的三段式目录 # --with-libraries 只编需要的,全量编译能磨掉一两个小时 ./bootstrap.sh \ --prefix=$HOME/work/wt-deps/third_party \ --with-libraries=system,thread,filesystem,date_time,chrono,atomic,regex # 正式编译并安装 ./b2 -j$(nproc) \ link=static,shared \ runtime-link=shared \ threading=multi \ variant=release \ cxxflags="-fPIC" \ --layout=system \ install逐个说清楚这几个参数,因为它们决定了后面能不能链得上:
--with-libraries是白名单,只编列出来的组件。boost 全量编译在普通机器上动辄一两个小时,而交易框架真正用到的就那么几个,没必要全编。注意system在新版本里已经 header-only,但把它写进列表不会出错,写进去更保险。
link=static,shared同时产出.a和.so。静态库用于最终交付时减少运行期依赖,动态库用于本地快速迭代——改一行代码重新链接只要几百毫秒,不用把整个可执行文件重打一遍。
runtime-link=shared表示 boost 自己的库动态链接 libstdc++,而不是把 C++ 运行时静态塞进去。这个必须和主工程保持一致,否则同一个进程里会出现两份 C++ 运行时,异常和std::string跨库传递时行为不可预期。
cxxflags="-fPIC"是必须的。主工程如果产出.so(比如给上层语言做绑定的扩展模块),链接了非 PIC 的静态库会直接报relocation R_X86_64_32 against ... can not be used when making a shared object。这个错误信息跟 boost 一点关系都没有,第一次见很容易懵。
--layout=system让产出的库文件名不带编译选项后缀,也就是libboost_thread.so而不是libboost_thread-gcc11-mt-x64-1_74.so。带后缀的命名方式会让find_package的自动推导失效,得手写一堆变量去兜,不值得。
3.4 让 CMake 找到它:三个环境变量的分工
# 写进 ~/.bashrc,或者项目根目录的 env.sh,用 source 加载 export WT_DEPS=$HOME/work/wt-deps/third_party # BOOST_ROOT 是 FindBoost 模块的第一优先搜索点 export BOOST_ROOT=$WT_DEPS # CMAKE_PREFIX_PATH 覆盖所有 find_package,优先级更高更通用 export CMAKE_PREFIX_PATH=$WT_DEPS:$CMAKE_PREFIX_PATH # 运行期动态库搜索路径,只在当前 shell 有效 export LD_LIBRARY_PATH=$WT_DEPS/lib:$LD_LIBRARY_PATH # pkg-config 也要指过去,openssl / curl 会用到 export PKG_CONFIG_PATH=$WT_DEPS/lib/pkgconfig:$PKG_CONFIG_PATHBOOST_ROOT和CMAKE_PREFIX_PATH的区别值得说清楚:前者只对FindBoost.cmake生效,后者对所有find_package生效。两个都设上是最省事的做法。如果只设BOOST_ROOT而 CMake 版本较新(3.20 以后),它可能优先走BoostConfig.cmake这套新机制,而新机制根本不看BOOST_ROOT,于是你会看到“变量明明设了却没用”的奇怪现象。遇到这种情况,加-DBoost_NO_BOOST_CMAKE=ON强制回退到老模块即可。
LD_LIBRARY_PATH只在当前 shell 会话有效,换个终端窗口就没了。这在调试期很烦人——刚验证通过的二进制,新开一个窗口跑就报找不到库。正式一点的做法是把它写进可执行文件的 RUNPATH,下一章会讲。
4. 把依赖接到工程上:一次完整的 configure 与三层验证
依赖装好了不等于能用,得跑到主工程的构建链路里验证一遍。这一章按“配置 → 链接 → 运行”三层往下走,每层都有对应的检查手段,出错时能快速定位到是哪一层的问题。
4.1 CMakeLists 里的最小可用片段
cmake_minimum_required(VERSION 3.16) project(wt_core CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 动态链接,避免把 boost 静态塞进产物导致体积膨胀 set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_MULTITHREADED ON) # 版本号写死,低于这个版本直接配置失败,别让它悄悄降级 find_package(Boost 1.70 REQUIRED COMPONENTS system thread filesystem chrono date_time) find_package(OpenSSL REQUIRED) find_package(ZLIB REQUIRED) find_package(CURL REQUIRED) # 用 imported target,不要手写 -lboost_system 这种老写法 target_link_libraries(wt_core PRIVATE Boost::system Boost::thread Boost::filesystem Boost::chrono OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB CURL::libcurl ) # 把依赖库的 lib 目录写进 RUNPATH,省掉 LD_LIBRARY_PATH set_target_properties(wt_core PROPERTIES BUILD_RPATH "${WT_DEPS}/lib" INSTALL_RPATH "${WT_DEPS}/lib" INSTALL_RPATH_USE_LINK_PATH TRUE )Boost::system这类 imported target 是 CMake 3.5 之后的标准写法,它会自动带上头文件路径和库路径,比手写${Boost_INCLUDE_DIRS}和${Boost_LIBRARIES}更可靠,尤其在多版本共存的时候。老工程里常见的那种手写变量写法,遇到新版 boost 很容易漏掉某个组件。
BUILD_RPATH和INSTALL_RPATH是两个不同的东西:前者作用于构建目录里的产物,后者作用于make install之后的产物。只设前者,本地能跑,装完就不能跑了;只设后者,本地调试时就得到处找库。
INSTALL_RPATH_USE_LINK_PATH TRUE会把链接时用到的所有非系统库路径自动追加进 RUNPATH。这一行能省掉大量手工维护,代价是产物里会写进绝对路径,迁机器时如果依赖目录不在同一位置就会失效。团队内部统一路径时用它很舒服,对外分发就老老实实只写相对路径或者干脆静态链接。
4.2 链接期的三件套检查
# 1. 看配置阶段到底找到了哪份 boost cmake -S . -B build -DBoost_DEBUG=ON 2>&1 | grep -i "boost_ # 2. 看产物的动态依赖,确认链接的是自编译那份而非系统那份 ldd build/wt_core | grep -E "boost|ssl|crypto" # 3. 看符号有没有真的解析掉,U 开头的是未定义 nm -D -C build/wt_core | grep " U " | head -n 20 # 4. 直接读 RUNPATH,确认 rpath 写进去了 readelf -d build/wt_core | grep -E "RPATH|RUNPATH"第一步的输出里有Boost_DIR、Boost_INCLUDE_DIR、Boost_LIBRARY_DIR这几个关键变量,一眼就能看出它找的是/usr还是你的third_party。找不到问题的时候,这条命令基本是必跑。
第二步的ldd输出里,路径如果是/usr/lib/x86_64-linux-gnu/libboost_thread.so.1.71.0,说明系统那份被优先命中了,自编译那份没起作用。这时候别急着改LD_LIBRARY_PATH,先回去检查是不是libboost-all-dev还装着。
第三步的nm是看链接有没有真的完成。如果出现大量U开头的 boost 相关符号,说明库根本没链上,只是头文件找到了。这种情况在只写了target_include_directories而漏了target_link_libraries的时候特别常见。
4.3 运行期的实战验证:跑一个最小回测
编译通过不代表跑得起来。最省时间的验证方式是加载一个最小策略、跑一段历史数据,把整条链路——读配置、连数据、跑引擎——全部走一遍。
# 加载依赖库路径后直接运行,观察有没有加载期报错 cd build LD_LIBRARY_PATH=$WT_DEPS/lib ./wt_core --config ../cfg/backtest.yaml # 如果不想每次带环境变量,用 patchelf 把 RUNPATH 补进去 patchelf --set-rpath "$WT_DEPS/lib:/usr/lib/x86_64-linux-gnu" ./wt_corepatchelf这一步在临时排查时很好用:不用重编,直接把路径打进产物,改完立刻能验证问题出在 rpath 上还是出在别处。生产构建里我还是用 CMake 的INSTALL_RPATH,patchelf只当诊断工具。
跑起来之后要看的不是有没有崩,而是启动日志里那些“加载了哪个模块、用了哪个版本”的信息。有经验的开发者会要求框架在启动时打印关键依赖的版本号,这样线上出问题时能第一句就知道环境对不对。
5. 避坑与排查:五个让人卡一下午的常见问题
5.1 报 Could not find Boost,但目录里明明有
现象:cmake输出Could not find the following Boost libraries: boost_thread,可ls $WT_DEPS/lib明明能看到libboost_thread.so。
原因有两种。一是BOOST_ROOT指向了源码目录而不是install之后的 prefix,源码树里有头文件但没有库。二是 CMake 版本较新,走了BoostConfig.cmake这条新路径,它只认Boost_DIR,不认BOOST_ROOT。
解决办法是先跑-DBoost_DEBUG=ON看它到底在哪找,然后按需指定:老模块用-DBOOST_ROOT=$WT_DEPS -DBOOST_LIBRARYDIR=$WT_DEPS/lib,新模块用-DBoost_DIR=$WT_DEPS/lib/cmake/Boost-1.xx.0。拿不准就加-DBoost_NO_BOOST_CMAKE=ON强制走老模块,行为最可预测。
5.2 链接期 undefined reference to boost::filesystem 相关符号
现象:配置阶段一切正常,make到最后一步报一长串未定义符号,全是 boost 命名空间下的。
原因通常是只加了头文件路径、没加链接库,或者链接顺序不对。CMake 的target_link_libraries有顺序语义,被依赖的库要放在后面,用 imported target 能自动处理,手写-l就得自己排。
还有一种情况是升级 boost 之后出现的:某些组件在新版本里变成了 header-only,老 CMakeLists 里还写着链接它,而这个目标已经不存在了,配置期就会失败。这时候把对应组件从COMPONENTS里去掉即可。
5.3 运行期 error while loading shared libraries
现象:编译链接全过,一执行就报libboost_thread.so.1.74.0: cannot open shared object file。
原因是 rpath 没写进去,而LD_LIBRARY_PATH只在设置它的那个 shell 里有效。用readelf -d查 RUNPATH,如果是空的,那就是这个问题。
解决办法分两种场景:开发机直接export LD_LIBRARY_PATH,或者用patchelf临时补;要交付的产物就在 CMake 里设INSTALL_RPATH,并且用INSTALL_RPATH_USE_LINK_PATH TRUE把依赖路径自动带上。系统级安装的依赖还可以写/etc/ld.so.conf.d/*.conf再ldconfig,但那会影响整机,多个项目共存时慎用。
5.4 GLIBCXX 版本不匹配,换机器就崩
现象:开发机上跑得好好的二进制,拷到另一台机器上报version 'GLIBCXX_3.4.29' not found。
原因是编译机的 gcc 比运行机新,libstdc++ 的符号版本对不上。这个问题的错误提示相对友好,直接告诉缺哪个版本。
解决办法优先是统一环境——构建和运行用同一个基础镜像,这是最省心的。退一步可以静态链接 libstdc++(-static-libstdc++ -static-libgcc),代价是产物体积变大,而且如果进程里有多个模块各自静态链了 libstdc++,跨模块抛异常会有麻烦。我不太推荐在插件化架构里用这一招。
5.5 系统 boost 和自编译 boost 混在一起
现象:编译一切正常,运行时偶发崩溃,栈回溯指向 boost 内部,看着像内存越界,改代码怎么改都不对。
原因是头文件来自自编译的那份(版本 A),链接期命中的却是系统那份(版本 B),两个版本的类布局不一致,std::string、容器、智能指针跨版本传递时直接错乱。这类问题最难查,因为报错点跟真正的原因隔着好几层调用。
排查方法是把ldd、nm、readelf三件套全跑一遍,逐个确认每个 boost 库的实际路径,只要有一条指向/usr/lib/x86_64-linux-gnu就是它。根治办法是把系统的libboost-all-dev卸干净,再在 CMake 里加set(Boost_NO_SYSTEM_PATHS ON),从源头上堵死系统路径。从那以后我每接一个新环境,第一件事就是跑一遍ldd确认没有混用。
6. 让依赖库变成能交付的产物:校验、归档与一次构建多处复用
依赖装好只是开始,真正省时间的是把它变成一份可复现的产物。我的做法是写一个bootstrap_deps.sh,把 apt 安装、git 拉取、b2 编译、路径导出全部串起来,脚本末尾用sha256sum给每个关键.so打一份指纹清单,存成deps.sha256。任何人拿到这份依赖目录,跑一遍sha256sum -c deps.sha256就能确认自己没有拿错版本,比口头说“我用的是 1.74”可信得多。
然后是归档。tar -czf wt-deps-$(date +%Y%m%d).tar.gz third_party/,把整个三段式目录打成一个包,扔到内网的文件服务上。新机器上解压出来、source env.sh、直接cmake,整个过程不到五分钟。这一步的价值在协作场景里最明显:新人入职第一天不用花半天编译 boost,也不会因为各自编译参数不同导致“你这边能跑我这边不能跑”。
最后一个技巧是关于校验的:不要只校验库文件,把头文件目录也纳进去。我吃过一次亏——某个依赖的头文件在安装时被后来的操作覆盖了半截,库是对的、头文件是旧的,编译能过,运行时结构体字段对不上,查了整整两天。现在我的脚本里会对include/下所有.h、.hpp做一次汇总哈希,跟库的哈希一起存。从那以后我每次交付依赖包,都强制走一遍“解压 → 校验 → 编译最小示例”这三步,少一步都不放心。希望帮到你。
本文还有配套的精品资源,点击获取