接前面的开胃菜 QtCreator 下如何优雅使用第三方库,继续聊聊C++ Qt程序中众多的动态库(共享库)如何管理。
缘起
用Qt开发一个程序,需要几十上百个的动态库。如果没有可心的包管理工具,那么这些动态库如何管理,还真的让人倍感头疼...
如果还有些强迫症,需要一个跨平台(Windows,Linux,MacOS,Embedded Linux)的方案,且同时支持cmake和qmake,...
同时,这个项目的开发和维护期需要保证5年以上...
注:本文介绍的是一个经过验证的手搓方案。新的应用程序直接考虑conan或者vcpkg可能更好一些。不管怎样,希望文中涉及四种不同的动态库使用方式,能对大家有一定参考意义。
动态库分类
首先,综合考虑动态库的接口稳定性,是否需要自行维护,是否自行开发,是否通用等因素,将动态库分成几类:
- 第三方代码库:稳定的第三方库,不需要自行维护。直接使用预编译的二进制库,作为开发环境的基本配置。不需要git管理代码,直接体现在脚本和README中即可。
- 基础代码库(框架库):自行开发的稳定的代码库,一些需要适配或魔改的采用MIT、BSD、LGPL等商业友好协议的比较小的三方库。每个框架(framework)一个git仓库,按版本号发布为动态库,以便于其他程序使用。
- 业务代码库(可复用模组库):自行开发的模组库,每个模组一个git仓库,每一个库可以独立测试,库中的example可以直接作为公司某些业务的正式程序使用。主要用途还是供上层应用程序使用。
- 应用程序(特定库):应用程序自身使用的代码库,位于应用程序自身的代码仓库中。
下一步呢?
分类之后,针对不同库,采用不同的应对策略就可以了。本身不复杂...
应用程序代码库
这个最简单,一个git仓库中的应用程序逻辑代码的直接拆分而已。qmake项目的代码结构大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
主要点:
- 各个不同
.pro
,通过.pri
文件进行沟通和共享配置。各个.pro文件内容都很少 - 比如动态库输出到什么地方,统一宏定义等,统一放到之 mylibs.pri 文件中
- 尽可能使用qmake的函数定义等功能,以简化项目工程文件中的重复内容
比如,在 src.pro 负责管理这些库(生成到合适位置):
1 2 3 4 |
|
app需要使用(找到)这些动态库和头文件,偷懒做法,写qmake函数将其隐藏起来,项目中直接用:
1 |
|
cmake做类似事情,比qmake简单一些。每个文件夹下面放置一个CMakeLists.txt文件,再适当定义符合自己口味的cmake函数可以简化书写。
业务代码库,每个独立业务模块一个git代码仓库
假定我有数十个来自不同部门的的独立模块需要控制。每个模块对应一个git仓库,应该还比较自然吧。
- 这个仓库,直接对应这个模块。里面的example可以直接反馈给模组开发部门使用。
- 一系列的这种仓库,单独测试通过后,可以作为git submodule,在上层应用程序中直接使用。
单个git仓库结构
以qmake为例,单个代码的git仓库结构大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
在这个仓库内,可以独立完成单元测试,编译验证。example中的例子也可以直接供其他部分使用。
这个example的好处是:不需要严格的版本号进行管控。About对话框中,只需要显示构建时间、构建所用源码的SHA值,仓库提交记录总数等,可用于追溯即可。
集成方式
不同的业务逻辑代码库进行组合,可以完成一些更复杂的功能,用于构筑上层用户应用程序。
可以将上面的各个git子仓库,直接以git submodule方式,集成到应用程序仓库中,一种组织结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
所有的子仓库,统一放置到某个submoduleLibs目录下,在src.pro
中,我们告诉它一下(让它统一管理)就好了:
1 2 3 4 |
|
然后我们需要告诉app.pro,动态库和头文件在什么地方。偷懒的话,自己定义变量,直接写成下面这样:
1 |
|
源码中,直接用
1 2 3 |
|
注意:但是这儿我们新增加了一个app_submodulelibs.pri
文件。该文件用于对所有的子仓库进行统一控制:
- 控制动态库输出到什么地方
- 控制统一使用哪一个版本基础库
所以,前面的业务代码库中的pri文件内,有类似下面的钩子代码(以保证作为子仓库集成时,行为受控):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
cmake
以上使用qmake举例,cmake其实差不多。整个代码完全结构不动,每个文件夹下面添加 CMakeLists.txt 文件,可以保持qmake和cmake两套系统同时工作的(尽管不建议这种左右手互博)。
基础代码库,公司框架库,使用动态库形式发布
这个比较纠结,不能所有代码都用子仓库以源码形式集成到应用程序仓库啊。
那么怎么办?我们可以将其做成framework,采用动态库的形式进行发布。
代码组织结构
这和前面的业务代码库有点相似,只是更繁杂一些/简单一些??:
- 不用考虑作为子仓库使用,不需要留钩子(hook)
- 需要作为动态库发布,质量要求更高,不能老改,乱改
- 如果不能保证ABI兼容,需要支持多版本并存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
这个没多少可说的,我们看看打包后结构
发布包结构
为了保持多版本并存,需要注意:
- 所有头文件放置到以版本号命令的文件夹中
- 所有动态库添加版本号作为后缀
- 提供给qmake或cmake使用的文件,包含版本号。
以Windows为例,代码包发布结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
注意:这里面分别为 cmake和qmake提供了 myqtaddons100-config.cmake 和 myqtaddons100.prf,需要什么黑魔法,尽情往里面放就可以。
只不过你能看到,到处充斥着这么多版本号,使用起来麻烦吗???
使用不麻烦
在程序代码中,不体现任何版本号
1 2 3 |
|
在工程文件中,只需要出现一次版本号
对于cmake工程来说,只需要
1 |
|
对于qmake工程来说
1 2 |
|
不用统一升级
我们前面提到,业务代码库数十个仓库,都依赖基础框架库,如果框架升级,那不得所有的仓库都变更啊??
其实不需要,比如:
- submodulelibs/mylib1 使用 myqtaddons100
- submodulelibs/mylib2 使用 myqtaddons130
- myapp 使用 myqtaddons150
各个业务库子仓库,单独编译时,比如mylib1会使用它自己指定的 100版本。
但是当它作为myapp的子仓库使用时,会自动使用myapp指定的150版本。
不要求ABI。只需要基础框架库保持源码级别兼容即可。只有做了不兼容的变更时,其他涉及到的业务逻辑库,才需要升级。
第三方库,配置到基本开发环境中
OpenCV是一个很大个头的C++库。把它放置到某个具体的Qt应用程序中不太现实。更不用说这东西迭代挺快,有好多版本。而且即使同一个版本,还有不同的编译选项,造成的不同动态库。
- OpenCV4.10
- OpenCV4.10-with-Cuda
- ...
简单起见,这些统统在外围屏蔽掉,我们项目中只需要直接启用opencv,然后源码中:
1 2 |
|
cmake工程
OpenCV提供cmake支持,不用手动编写搜索逻辑。直接在CMakeLists.txt中用就行了:
1 2 3 |
|
主打一个:应用程序不关心opencv的具体版本,以及在什么位置。
qmake工程
qmake稍微复杂一下,使用.pri/.prf/.prl“魔法”就行了。
.pro文件中,使用一行代码来启用:
1 |
|
或者友好点,找不到时,让他报错:
1 |
|
总之,将配置基础环境时,将其封装到一个opencv.prf文件中,然后配置qmake能找到这个文件就行(所以,系统中有多套OpenCV库,就可以有很多个opencv.prf文件)。
在Windows下,opencv.prf手动创建就可,Linux下,可以使用类似代码
1 2 3 4 5 6 7 8 9 |
|
其他
东西太杂,以上只做了简单整理和介绍。实际上,在整个项目中,尽管和构建无关,还有一些python写的向导脚本。
- 用于生成各个库的模板(取个名字,直接填肉即可,不用考虑qmake,cmake细节)
- 构建时,让qmake或cmake将关键的环境变量和变量信息记录到一个文件中(如果电脑上有多套Qt、OpenCV等库时这点很重要)
- 打包脚本根据构建信息,自动提取所有依赖文件,生成安装包
另外如上所有内容,均不依赖于QtCreator,在命令行下都可正常工作(便于CI/CD集成)。