如何链接含CMake全局变量条件定义类型/变量的C++第三方库
我正在CMake管理的C项目中尝试将Toulbar2作为C库链接到主可执行文件,过程中遇到了严重的链接障碍。我检索了多个技术社区的同类问题,均未找到适配我场景的解决方案,尝试了所有能想到的方法仍未解决,故寻求帮助。
我的运行环境为Ubuntu 18.04,CMake版本3.23,项目采用标准C11;我具备通用编程基础,但属于C与CMake的初/中级使用者。
我无法列举全部尝试,仅说明最接近可行方案的几次尝试,便于排查问题:
- 1)find_package导入方案:最初我采用导入非标准库的常规方式,在CMakeLists.txt中使用
find_package()查找包,后续链接找到的LIBRARIES、添加找到的INCLUDE_DIRS,但很快发现Toulbar2既不提供Find<package>.cmake文件,也不提供<name>Config.cmake配置文件,该方案无法生效。 - 2)动态库链接方案:该方案是我认为最接近预期效果的方案。提前创建build目录后,可通过命令
cmake -DLIBTB2=ON ..轻松将Toulbar2编译为动态库,执行make编译后会在build/lib/Linux路径下生成.so文件,安装后可通过find_library命令让CMake自动查找该库,对应的CMakeLists.txt配置如下:
[...] find_library(TB2_LIBRARIES tb2) if(TB2_LIBRARIES) set(all_depends ${all_depends} ${TB2_LIBRARIES}) else(TB2_LIBRARIES) add_compile_definitions("-DNO_TB2") message("Compiling without Toulbar2, if you want to use it, please install it first") endif(TB2_LIBRARIES) [...] target_link_libraries(main ${all_depends}) [...]
该配置可部分生效:CMake能正确找到库并执行链接命令,但尝试#include <toulbar2lib.hpp>时提示找不到头文件。我随后添加了头文件搜索路径配置:
include_directories(/path/to/header/file's/directory)
添加后头文件可被找到,但编译时头文件内大量标识符无法识别。问题根源是:Toulbar2中部分变量、类型通过#ifdef、#ifndef等预处理指令做条件定义,这些条件判断依赖的全局宏是编译Toulbar2时由CMake生成定义的,例如头文件中使用的Cost类型就属于这类条件定义内容。我认为预编译库时这些定义已经固化在库中,链接对应库后包含头文件时应该能正常识别,但实际并未生效。
- 3)源码子目录引入方案:该方案侵入性更强,属于无思路时的尝试:我将完整的Toulbar2源码克隆目录复制到我的项目中,在主CMakeLists.txt中添加配置:
add_subdirectory(toulbar2)
Toulbar2本身自带CMakeLists.txt,理论上可直接作为子项目引入,后续添加Toulbar2源码src目录(存放所需头文件)的搜索路径即可。但实际仍出现和方案2完全相同的问题:即使子项目Toulbar2可无错正常编译,主项目编译时条件定义的变量、类型仍无法识别。
所有可行方案我都愿意参考,但更希望获得类似方案1、2的优雅非侵入式解决方案,适配后续代码公开发布的需求,在此提前致谢。
核心问题本质:C/C++预处理宏仅在当前编译单元的预处理阶段生效,不会被固化到二进制库中。编译Toulbar2时生成的宏定义只在编译Toulbar2自身源码时生效,不会自动传递给链接它的上层项目,因此直接包含头文件时,条件编译块无法被正确触发。
非侵入式动态库方案(推荐,适配公开发布需求)
第一步:确认依赖文件位置
开启-DLIBTB2=ON编译并安装Toulbar2后,除了生成的libtb2.so动态库,还会生成一个编译期自动创建的tb2config.h头文件,所有控制类型、条件编译的宏都定义在这个文件中。默认make install后,该文件会和toulbar2lib.hpp一同被安装到系统头文件目录下。
第二步:修正CMake配置
不要硬编码绝对路径,使用CMake原生查找逻辑自动定位依赖,兼容自定义安装路径和系统默认路径:
# 查找tb2动态库 find_library(TB2_LIBRARIES NAMES tb2 libtb2) # 查找对外公开头文件toulbar2lib.hpp find_path(TB2_PUBLIC_INCLUDE_DIR NAMES toulbar2lib.hpp PATH_SUFFIXES toulbar2) # 查找编译生成的配置头文件tb2config.h find_path(TB2_CONFIG_INCLUDE_DIR NAMES tb2config.h PATH_SUFFIXES toulbar2) if(TB2_LIBRARIES AND TB2_PUBLIC_INCLUDE_DIR AND TB2_CONFIG_INCLUDE_DIR) # 两个头文件目录都加入当前目标的搜索路径 target_include_directories(main PRIVATE ${TB2_PUBLIC_INCLUDE_DIR} ${TB2_CONFIG_INCLUDE_DIR}) # 链接动态库 target_link_libraries(main PRIVATE ${TB2_LIBRARIES}) message(STATUS "Found Toulbar2 library: ${TB2_LIBRARIES}") else() target_compile_definitions(main PRIVATE NO_TB2) message(STATUS "Toulbar2 not found, compiling without TB2 support. Install with -DLIBTB2=ON to enable related features.") endif()
注意:废弃旧的全局
include_directories写法,使用target_include_directories仅为需要链接TB2的目标添加搜索路径,避免污染其他编译目标。
第三步:验证头文件引入
源码中直接引入公开头文件即可,不需要单独引入tb2config.h:
#include <toulbar2lib.hpp>
只要头文件搜索路径配置正确,toulbar2lib.hpp会自动引入同路径下的tb2config.h,所有条件编译的类型、宏都会被正确识别。
子目录引入方案修正
如果选择源码内嵌的方式,不要手动添加头文件搜索路径,直接链接Toulbar2的导出目标,CMake会自动传递所有PUBLIC属性的头文件目录、编译宏和依赖库:
# 开启LIBTB2选项后再引入子目录 set(LIBTB2 ON CACHE BOOL "Build toulbar2 as library" FORCE) add_subdirectory(toulbar2) # 直接链接tb2目标,自动继承所有编译期配置 target_link_libraries(main PRIVATE tb2)
之前子目录方案失效的原因是手动添加src目录路径时,遗漏了Toulbar2 build目录下生成的tb2config.h路径,也没有继承目标的编译定义,直接链接导出目标会自动处理这些传递依赖。
公开发布适配
如果需要开源项目,可以把上述Toulbar2查找逻辑封装成独立的FindToulbar2.cmake模块,放在项目的cmake/目录下,在主CMakeLists.txt中添加list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)即可。其他用户编译时只要提前安装好带LIBTB2=ON选项编译的Toulbar2,构建系统就会自动识别依赖,不需要修改任何配置。
内容的提问来源于stack exchange,提问作者Orazio Rillo

