.so 依赖目录 cmake_CMake最佳实践

C++的项目的构建可以说是所有语言当中最耀眼的舞台了, 其中存在多种构建方式, 例如:

  1. Makefile

  2. CMake

  3. B2

  4. WAF

  5. SCons

  6. Visual Studio

  7. Autoconf/Automake/libtool

  8. Bazel

  9. xmake

工具虽多, 但是使用体验上却差强人意, 虽然有语言复杂度的原因, 但是笔者认为, 更多的是语言的维护者重视度不够导致.

本文将从C++项目的构建需求, 去尝试总结出什么样的构建系统是C++需要的, 以及如何将这些理念通过CMake来表达.

本文使用的示例代码位于这里(https://github.com/ethanvc/learn_cmake).

C++需要什么样的构建系统

C++需要什么样的构建系统? 为了得到这个问题的答案, 我们以一个输入法项目的例子来分析一下.

该项目包含如下几个模块:

  • ScheduleService: 计划任务服务. 定时执行任务的能力. 如定时更新客户端版本, 下载云控配置等.

  • CloudService: 云服务. 提供心跳等服务.

  • SecurityTransportService: 提供和后台的安全传输能力.

  • InputCore:/InputCoreBackend: 核心输入功能模块, InputCore需要注入到使用输入法的进程当中去. INputCoreBackend是单独的进程服务, 和InputCore配合, 执行复杂的查词逻辑. 由于InputCore的注入特殊性, 该模块需要全部静态链接其它依赖库, 避免对宿主进程进行影响.

  • InputCoreRpc: 这个模块是由Input CoreRpc.proto协议生成源文件构建出的库. 负责InputCore和Backend之间的数据序列化/反序列化工作.  

模块之间的依赖图如下:

f4dc01b65b1a89aa30c128d114e00529.png

日常项目构建, 有如下一些需求:

  1. 构建所有模块的调试版本. 方便开发时定位问题. 调试版本主要是为了禁用优化, 方便看堆栈.

    1. 对于VC++的编译器, 调试版本最重要的参数就是/Od /D "_DEBUG", 一个是禁用优化, 一个是定义了一个宏.

    2. 对于GCC, 参数为-g -o0. -g表示生成符号信息, -o0表示禁用优化.

  2. 构建所有模块的发布版本并打包.

  3. 已经安装了发布版本的安装包, 但是有一个模块出现异常, 需要编一个调试版本的进行替换来定位问题. 或者是问题已经修复, 需要编译修复版本替换有bug的模块文件观察效果.

  4. 编译器升级了, 增加的更多检查异常代码的选项. 为了提高代码稳定性, 需要将这些检查参数给每个模块都加上.

为了满足上述需求, 构建系统能够满足如下功能:

  1. 设置全局编译/链接参数. 有些参数就需要全局设置才能保证能够正常构建, 例如, 很多开发者喜欢为DEBUG定义一些特殊的行为, 导致DEBUG和RELASE编译出来的二进制不兼容, 因此有些目标用DEBUG, 有些用RELEASE大概率出现崩溃. 还有, VC的/MD /MDd /MT /MTd四个参数, 需要所有源文件使用同样的设置, 不然也无法构建成功.

  2. 单个目标自定义编译/链接参数. 比如一个动态链接库, 根据不同的宏构建出不同功能集合的输出, 此时可能需要为这个目标单独启用某些宏.

  3. 指定模块间的依赖关系. 对于C++, 假设A依赖模块B, 那么说明:

    上述, 表现到参数上, 即, 在编译A的时候, 需要通过-I参数, 将B模块的头文件搜索路径指定好, 同时需要设置库搜索路径参数和库文件名给到链接器.

    这里还有一个点需要注意, 基于模块话设计的考虑, 如果A依赖B, B依赖C, 那么, 编译A时, 原则C的头文件搜索路径不能添加进来, 除非显式指定.

    1. A需要B提供的头文件.

    2. A需要B提供的库文件(静态库OR动态库).

  4. 环境检测. 为了做到跨平台, 不少代码里面会根据操作系统, 通过宏编译不同的代码逻辑, 因此, 构建系统需要提供相关的能力来封装环境检测的逻辑, 提高C++构建的跨平台能力.

接下来, 我们来看一下如何通过CMake来完成上述构建需求.

CMake对构建的基本支持

设置全局编译/链接参数

cmake通过如下几个指令来设置全局配置:

  • add_compile_options: 增加全局编译参数.

  • add_definitions: 定义全局生效的宏. 其实也可以通过编译参数指定, 但是由于宏时C++语言的的一部分, 通过单独的指令可以屏蔽平台和编译器差异.

  • include_directories: 添加全局头文件搜索路径. 其实也可以通过编译参数指定.

  • 通过预定义变量指定的参数.

    • CMAKE_CXX_STANDARD: 指定C++标准的版本.

    • CMAKE_RUNTIME_OUTPUT_DIRECTORY: 指定可执行文件的输出路径, 如果是WIndows系统, 则输出文件会有exe或dll后缀名.

    • CMAKE_LIBRARY_OUTPUT_DIRECTORY: linux下so文件的输出路径.

    • CMAKE_ARCHIVE_OUTPUT_DIRECTORY: lib或.a文件的输出路径.

示例配置如下:

# 通过命令make VERBOSE=1可以看执行的命令行参数, 验证编译选项等是否生效cmake_minimum_required(VERSION 3.16.0 FATAL_ERROR)# 输出cmake的版本信息message("CMAKE_VERSION=${CMAKE_VERSION}")project(testinput LANGUAGES CXX)# 输出信息用# message("dir=${CMAKE_BINARY_DIR}")# 使用C++17标准set(CMAKE_CXX_STANDARD 17)# 设置lib库或.a文件的输出路径set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive)# 设置linux下so文件的输出路径set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)# 设置dll/exe文件的输出路径set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)### 设置编译选项# 该项目下的每个源文件在编译时, 都会增加.作为头文件搜索路径# 在VC编译其下, 将通过/I directory添加到编译参数中去# 在gcc下, 将通过-I directory添加到编译参数中去include_directories(.)# 定义全局生效的宏, 全局生效add_definitions(-DPROJECT_GLOBAL_MACRO)# 指定全局编译参数, MSVC时一个cmake检测并设置的全局变量if (MSVC)    add_compile_options(/W3 /WX)else()    add_compile_options(-W -Wall -Werror)endif()# VC编译器下, 指定链接运行时库的方式message("CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}")if(MSVC)    string(REPLACE /MDd /MTd CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG})    # message("CMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG}")    string(REPLACE /MD /MT CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE})    message("CMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE}")endif()# 输出编译命令行参数, 用于观察实现细节, 比如默认有些什么参数开启了# cmake构建后, 结果输出在文件compile_commands.json中set(CMAKE_EXPORT_COMPILE_COMMANDS ON)add_subdirectory(http)add_subdirectory(inputcore)add_subdirectory(cloudservice)add_subdirectory(ipc)

目标之间依赖的问题

C++中, A依赖B, 则说明, A需要B的:

  1. 头文件. 用于调用函数, 使用B定义的变量. 头文件的依赖通过设置头文件搜索路径来实现.

  2. 库文件. 如果B不是纯头文件实现的, 那么在构建A的时候就需要的库支持, 才能链接通过. 并且, 如果有其他模块依赖A, B的静态库或导出库需要一直传递才能最终链接成功. 例如有个X可执行文件需要构建, 则X在链接的时候, 需要A和B两个的静态库.

一个简单的示例如下:

############ CMakeLists.txt开始# INTERRACE告诉构建系统, 这个目标没有编译项, 无需编译, 即实现全部是头文件add_library(inputconfig INTERFACE inputconfig.h)target_sources()########### CMakeLists.txt开始add_executable(cloudservice cloudservice.cpp cloudservice.h)# 添加依赖. PRIVATE表示, 相应的依赖性是本模块的实现相关, 无需传递给依赖cloudservice的模块# 这里不传递的仅头文件设置, 链接时库还时需要添加的.target_link_libraries(cloudservice PRIVATE http)target_link_libraries(cloudservice PRIVATE inputcore)target_link_libraries(cloudservice PRIVATE ipc)# 由于inputconfig只有头文件, 因此需要使用INTERFACE告诉构建系统不要将lib库加如链接target_link_libraries(cloudservice INTERFACE inputconfig)

外部模块的依赖处理

cmake按照project管理一个项目下的所有目标, 但是当一个project需要依赖其他项目时, 则首先需要找到库信息, 然后方可通过target_link_libraries进行依赖. 下面介绍一种处理外部依赖的方法.

将外部模块转换为内部模块

如果外部模块时通过CMake构建的, 那么这种方案将非常简单, 将外部模块的代码通过add_subdirectory加入当前工程, 然后就可以通过target_link_libraries进行引用了.

参考资料

  • Modern CMake for modular design

  • Craig Scott: Enhanced source file handling with target_sources()

  • CMake CookeBook中文版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值