C++库脚手架项目及思考

178 天前
 RiverBWU

之前在考虑编写一些个人的开源项目,不奢求成为什么明星项目,只希望把已有的技能和知识进行总结。一方面将来遇到类似需求时可以直接拿去用,另一方面也给遇到相同问题的人一些帮助。

最开始的想法是不管三七二十一先写起来,之后再逐步测试、修改和完善。没想到功能还没堆多少,那些觉得不会有问题懒得测的模块,总是写出低级 bug ;等想要添加单元测试时,面对的是写成一团的构建脚本;之后适配跨平台,每次提交代码都得在虚拟机之间来回切换、拉取代码和编译测试。

在总结失败教训并参考开源项目后,我决定编写一个适合自己需求的脚手架项目KRCppLibraryTemplate

天下苦构建久矣

就我个人经历而言,当初入门学习 C++时确实遇到了很多困难,但好在有很多优秀的书籍和资料(根本看不完),大部分问题也可以通过搜索找到答案。

没想到跨过这座大山后,迎面而来的是另一座大山——构建:

当你觉得学有所成,打算写个稍微像样点的项目时,就一定会被这些问题深深困扰。

这些问题都来自于 C++编译模型,以及平台和编译器实现细节,没办法三言两语概括,当初学习时主要参考了如下资料:

如果想要新人消化这些内容,恐怕不太适合当今快节奏的职场环境,所以稍微划划重点,其他的就让 AI 来辅助吧:

构建系统

使用构建系统的目的,主要是避免重复的手动编写编译脚本,同时自动分析依赖变更,从而控制重新编译的范围,加速构建过程。实际上构建系统还用于处理安装和打包等工作,有的支持不同平台,从而简化跨平台开发的工作。

不过构建系统自身也会带来一些复杂性,比如 make ,它基于手写依赖规则,并根据文件时间戳判断是否发生变动,如果漏写了依赖(头文件尤其常见),或是修改的是编译器宏定义等不修改文件的选项,很可能会得到错误的结果。

我曾经尝试系统的学习 make ,但最终结论是,遇到老项目,要么重写,要么别动它,遇事不决全量重新编译。

类似的,CMake 也有问题:

但话又说回来,涉及跨平台 C++开发,CMake 自身确实有问题,但选择 CMake 没啥问题。

CMake

比较系统和完善的参考资料如下:

现在的共识应该是 Modern CMake ,但我猜本就不多的 C++程序员里,懂得写 CMake 的人就更少了,更别提推动构建系统脚本的规范化。上面的知识从吸收到能够编写规范的代码需要很久的积累,所以这些就交给脚手架来完成吧。

现存优秀的项目

经常关注 CMake 的人应该都了解过这几个项目:

最终我的选择是造轮子( C++程序员必经之路),从需求角度来说,是因为我希望脚手架应该尽可能封装平台差异,能够支持动态库和静态库,而不是在一开始就对用户强加限制。另一方面则是为了对 CMake 的工程实践有一个完整的了解。

动态库与静态库

从用户角度来说,根据需要选择动态库或静态库是很正常的需求。就算不考虑使用动态库实现平滑升级、功能插件等需求,对于被多个库或可执行程序依赖的情况,使用动态库也可以减少链接时间和空间占用(尤其是 FFmpeg )。真正应该避免的是在存在菱形依赖的情况下进行动态库和静态库的混编,windows 中经典的跨 DLL 内存问题便来源于此,详情可见Professional CMake: A Practical Guide的 Mixing Static And Shared Libraries 章节。

很多个人开发者不太乐于专门适配动态库(比如Catch2: issue 2895),主要原因应该是会增加额外的工作量。

同时支持两种库的话又产生了新的问题,文章Building a Dual Shared and Static Library with CMake中进行了一些讨论:

原文章中的解决方案不支持将动态库与静态库安装至同一个目录下,这里主要介绍我的解决方案。

指定库类型

用户可以通过 static 和 shared 两个别名目标显式链接至对应版本的库:

target_link_libraries(<app1> PRIVATE KRLibrary::static)
target_link_libraries(<app2> PRIVATE KRLibrary::shared)

也可以不显式链接,而是通过选项进行指定,当库依赖层次较深时,便于从外部进行控制:

# 不指定动态库和静态库,通过 BUILD_SHARED_LIBS 或 KRLibrary_USE_SHARED_LIBS 控制
# 后者优先级更高
set(KRLibrary_USE_SHARED_LIBS ON) # 比起硬编码,一般是通过命令行或 CMakeCache.txt 进行修改
find_package(KRLibrary REQUIRED)
target_link_libraries(<app> PRIVATE KRLibrary::KRLibrary)

库的编译

动态库和静态库总是独立编译,不使用-fPIC编译静态库,因为会对静态库用户造成不必要的性能损耗。

当库作为顶层项目进行构建时,认为用户是库本身的开发者或打包人员,此时默认对两种类型的库都进行编译。

当库作为子项目被引入时,认为用户可能只需要其中一种类型的库,此时为两种类型的库目标设置EXCLUDE_FROM_ALL属性,只有被使用时才会进行编译,避免用户产生不必要的构建开销。

windows 下还需要处理动态库和静态库的.lib同名的问题,如果使用的是 MSBuild ,同名库文件很可能就被静默覆盖掉了,使用 Ninja 才会提示错误。我的解决方案是为 windows 下的静态库基本名称添加_static后缀,如果有不同的需求可以按需自行调整。

符号导出

MSVC 默认不导出动态库符号,需要手动通过dllexport 和 dllimport进行控制。GCC 和 Clang 默认导出全局符号,但也提供了对应的控制选项。对于上述属性,CMake 提供了<LANG>_VISIBILITY_PRESETVISIBILITY_INLINES_HIDDEN进行控制。

从“让错误尽早被发现”的角度出发,我选择默认不导出符号:

CXX_VISIBILITY_PRESET "hidden"
VISIBILITY_INLINES_HIDDEN ON

而导出宏则交给GenerateExportHeader自动生成,开发过程中只需要引入生成的头文件,并为要导出的函数添加宏标记即可:

#pragma once
#include <krlibrary/export.hpp>
namespace krlibrary
{
KRLIBRARY_EXPORT void exported_hello();
} // namespace krlibrary

动态库和静态库共用一份头文件,因此使用静态库时,自动设置宏选项禁用符号导出属性,不需要用户手动处理:

target_compile_definitions(
    "${static_target}"
    PUBLIC "${project_name_uppercase}_STATIC_DEFINE"
)

安装

安装一般涉及三类文件:

从打包人员的角度出发,则可以分为两种场景:

这两个选项是通过 CMake 的install功能中的 Componet 提供的,安装时默认都安装,也可以指定安装:

$ cmake --install build/ --prefix install/ --component KRLibrary_Runtime
-- Install configuration: "Release"
-- Up-to-date: install/lib/libkrlibrary.so.1.0.0
-- Up-to-date: install/lib/libkrlibrary.so.1

这里额外说明一下,install 中的 component 与 find_package 中的没有任何关系,完全是正交的概念。这里的可以理解为简单的打了个标签方便安装时进行选择,名字也可以按照自己的意图编写,例如干脆细分成*_Headers*_Static*_Runtime等,然后添加到对应组件的属性中即可。

另一种角度是从需求角度出发,例如只需要动态库或者静态库之一,那么可以在构建阶段进行设置:

cmake -S . -B build/ -DKRLibrary_ENABLE_INSTALL_STATIC=OFF

同理可以通过选项KRLibrary_ENABLE_INSTALL_SHARED控制动态库的安装。这两个选项和前述 componets 也是正交的。

其他

提供了 Catch2 的集成,手动处理了它在构建中可能遇到的一些小问题。CI 方面则编写了比较通用的 Github Action 脚本,代码覆盖分析在 Linux 和 windows 平台分别使用gcovrOpenCppCoverage,编译器警告选项适配了 GCC 、Clang 和 MSVC ,应该足够应付大部分开发场景了。

实际上还处理了一些比较细节的问题,但一方面篇幅所限,另一方面光是要介绍其应用场景可能就要想半天,需要的人就直接阅读源代码吧(懒)。

2654 次点击
所在节点    C++
6 条回复
bfjm
177 天前
推荐一下 xmake 简单方便快捷
kneo
177 天前
总结的不错。不过感觉标题不太准确。
Nosub
177 天前
大佬,膜拜。
hwdq0012
177 天前
@bfjm 好像除了图形那些开发,用这个的人很少, 工作为导向还是 cmake 路宽, cuda, vtk 这些官方的示例都是 cmake ,我始终不知道为什么那么多人用 xmake, cmake 又不是不能用
hwdq0012
177 天前
我觉得 op 可以参考一下 vcpkg 的作为,
资产缓存,二进制缓存, 版本控制,options 控制,单元测试, 跨平台,等等特性,只要你自己搭一个 vcpkg private registry, 再自己写一个 port 发布到自己的 registry 中,基本上就不会想自己整这些东西了

https://learn.microsoft.com/en-us/vcpkg/produce/publish-to-a-git-registry
这个是 vcpkg 官方教程,很详细,

我的工程每个项目都可以 cmake 时自动生成 vcpkg 需要的 portfile 和 versions, 到 vcpkg, 提交到 private registry 后,就可以用 vcpkg install 安装了, abi 用 vcpkg 的 trplet 控制, 可以了解一下 vcpkg 清单模式
rolinbutterfly2
161 天前
谢谢大佬的分享

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1118551

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX