lee-romantic 's Blog
Everything is OK!
Toggle navigation
lee-romantic 's Blog
主页
About Me
归档
标签
基于CMake构建动(静)态库项目
2022-11-02 15:04:45
106
0
0
lee-romantic
# 1、CMake基础 ## 1.1、问题引入 - `CMake`工具,只需要开发者提供头文件路径、库路径、编译参数等基本参数,就能快速生成`Makefile`和`vcproj`文件等,可以做到再改动很小的情况下,满足跨平台项目的快速构建要求。由于CMake的简洁易用特性,我们通常会基于CMake构建工程,并引入第三方库进行开发工作。本文主要记录和学习CMake是如何引入第三方库的。 ## 1.2、使用的CMake简单流程 - 编写`cmake`配置文件`CMakeLists.txt`。 通常`CMakeLists.txt`放在项目顶层目录,根据需要也可在子目录放置。 - 在`CMakeLists.txt`文件所在目录创建一个`build`文件夹,然后cd进入目录。 不建目录理论上是可行的,但是生成的中间文件不易清理,另外`build`目录名词也是可以自定义的。 - 执行`cmake ..` 生成`makefile`。 - 执行`make`和`make install`进行编译和安装。 - 注意,进行`make install`需要`CMakeLists.txt`中定义了`install`的**相关规则、路径、文件**等才行。 - `windows`下,如果使用`mingw`的话编译工具为`mingw32-make.exe`,`install`安装则为`mingw32-make.exe install`,如果使用`msvc`的话编译工具为`MSBuild.exe`,命令`MSBuild.exe XXX.vcxproj`) ## 1.3、CMake的部分常用命令 - 构建准备(描述性命令) - 指定使用该`CMakeList.txt`文件需要的`cmake`最低版本,例如: `cmake_minimum_required(VERSION 3.5)` - 指定项目信息, 如项目名字,项目使用的语言等,例如: - `project(calculator)`,`project(MultipleObjectTracking LANGUAGES CXX CUDA)` - `project`会影响`PROJECT_SOURCE_DIR`(第一个`project`所在的`CMakelist.txt`所在的文件的目录)的值。 - 设置属性,比如: - 通用的属性可以放在单独的文件中如`ToolChain.cmake`,然后可以用`include (ToolChain.cmake)`等方式包含。 - `set(CMAKE_VERBOSE_MAKEFILE ON/OFF)`用于开关编译时的详细信息打印 - `set(CMAKE_INCLUDE_CURRENT_DIR ON)` 包含当前路径,比如包含当前目录单独的`main.cpp`进去。 - 设置C++标准 - `set(CMAKE_CXX_STANDARD 11)` 设置C++11标准。 - `set(CMAKE_CXX_STANDARD_REQUIRED True)` - ... - [常用变量](https://blog.csdn.net/mby1988/article/details/121706937): - 指定编译选项 - `CMAKE_C_FLAGS` : 指定`gcc`编译选项,如`-02 ,-g,`当然也可用通过`add_definitions`设置。 - `CMAKE_CXX_FLAGS`:指定`g++`编译选项。 - `CMAKE_C_FLAGS_DEBUG`:指定`debug`版本编译选项 - 指定链接选项 - CMAKE_EXE_LINKER_FLAGS - CMAKE_MODILE_LINKER_FLAGS - CMAKE_SHARED_LINKER_FLAGS - CMAKE_STATIC_LINKER_FLAGS - 指定编译器 - CMAKE_C_COMPILER:指定C编译器,如gcc - CMAKE_CXX_COMPILER:指定C++编译器,如g++ - BUILD_SHARED_LIBS:指定默认生成库文件类型,on:动态库,off 静态 - `CMAKE_BUILD_TYPE`:设置编译类型,如Debug、Release - `CMAKE_CONFIGURATION_TYPES`:设置编译类型,针对vs平台,`CMAKE_BUILD_TYPE`不起作用。 - 编译路径 - 可以设置的路径 - LIBRARY_OUTPUT_PATH `set(LIBRARY_OUTPUT_PATH, $(PROJECT_SOURCE_DIR}/output)` 指定库文件输出路径 - EXEC_OUTPUT_PATH `set(EXEC_OUTPUT_PATH, $(PROJECT_SOURCE_DIR}/output)` 指定可执行文件输出路径 - CMAKE_CURRENT_BINARY_DIR 使用`CMAKE_CURRENT_BINARY_DIR`指定当前的二进制文件生成路径,使用`CMAKE_CUREENT_SOURCE_DIR`指定当前`CMAKEFILE`文件所在目录 - 经常读取的路径 - `PROJECT_SOURCE_DIR`: 使用得最多的变量,当前项目的根目录,对于子目录(子工程,子库等),`PROJECT_SOURCE_DIR`就是指的子目录的根目录,有点类似于`CMAKE_CURRENT_SOURCE_DIR`。 - `PROJECT_BINARY_DIR`: 运行`cmake`命令的目录。 - 例如,我们新建了一个build文件夹,用于存放编译产物,那么我们在build文件夹中运行cmake命令时,`PROJECT_BINARY_DIR`就是`${PROJECT_SOURCE_DIR}/build`。 - 使用`PROJECT_BINARY_DIR`可以得到存放编译产物的目录,可以方便指定install目录。 - `CMAKE_SOURCE_DIR`: 顶级cmakelists.txt的文件夹目录,正因为其代表顶级cmake文件的目录,因此其和`PROJECT_SOURCE_DIR`是不一样的。 - `CMAKE_CURRENT_SOURCE_DIR`: 一般来说,一个工程会有多个`cmakelists.txt`文件,对应当前文件目录。`CMAKE_CURRENT_BINARY_DIR`则为对应build里的目录。 - CMAKE_MODULE_PATH: api(include/find_package)包含别的cmake文件时的搜索目录。 - CMAKE_PREFIX_PATH: api(find_libray/path)包含模块时的搜索目录。 - CMAKE_INSTALL_PREFIX: 调用install相关函数,要生成/保存的根目录路径。 - 编译相关 - add_compile_options:不同平台一般来说有不同的编译设置。 - add_definitions: 添加预处理器定义。 - `include_directories`: 如`visual studio`里的,头文件搜索目录,在当前项目以及当前项目用`add_subdirectory`添加的项目都会应用。 - `add_subdirectory` 添加一个子目录并构建该子目录,因此要求子目录中存在CMakeLists.txt文件。 - `add_library`:添加库,根据static或者shared参数生成静态或是动态库,默认静态库。 - `link_libraries` - 为链接器添加库的搜索路径,此命令调用之后生成的目标才能生效。link_directories()要放在add_executable()之前。 - 在当前项目以及当前项目用add_subdirectory添加的项目都会应用。 - `add_executable`:添加执行文件。 - `target_include_directories`:在编译目标文件<target>时指定头文件。 - <target>必须是通过add_executable()或add_library()创建,且不能是ALIAS目标。 - `target_link_libraries`:指定目标的link_directories。 - 其中,PUBLIC修饰的库或目标会被链接,并成为链接接口的一部分。 - PRIVATE修饰的目标或库会被链接,但不是链接接口的一部分。 - INTERFACE修饰的库会追加到链接接口中,但不会用来链接目标文件<target>。 - set_target_properties:指定项目一些具体编辑器里的属性,如生成lib/dll的目录。 - 其他 - **include** 从指定的文件加载、运行CMake代码。如果指定文件,则直接处理。如果指定module,则寻找module.cmake文件,首先在`${CMAKE_MODULE_PATH}`中寻找,然后在CMake的module目录中查找。 - **install** 方案包含的项目多,每次把需要发给用户的`include/lib/dll`按照指定目录格式放好,这个可以用来做这些。cmake默认会生成一个insatll项目,这个项目会执行所有install命令,比如生成的`include/lib/dll`放入以`CMAKE_INSTALL_PREFIX`为根路径的目录。 - **find_package** - `find_package(LibaryName)` 参考[find_package()的使用](https://www.cnblogs.com/penuel/p/13983503.html),有module和config两种模式。 - 在默认的module模式下,根据对应`CMAKE_MODULE_PATH`找到对应的`Find<LibaryName>.cmake`,先在CMAKE_MODULE_PATH变量对应的路径中查找。如果路径为空,或者路径中查找失败,则在cmake module directory(cmake安装时的Modules目录,比如/usr/local/share/cmake/Modules)查找。 - Config模式下的查找顺序,比Module模式下要多得多。而且,新版本的CMake比老版本的有更多的查找顺序(新增的在最优先的查找顺序)。它要找的文件名字也不一样,Config模式要找Config.cmake或-config.cmake。 - 一般来说,存在如下三下变量(你也可以定义成别的名字): - `<LibaryName>_FOUND` 是否找到库,比如`OPENCV_FOUND`; - `<LibaryName>_INCLUDE_DIR` 或者 `<LibaryName>_INCLUDES`等, 表示库头文件目录,比如`OpenCV_INCLUDE_DIRS`; - `<LibaryName>_LIBRARY`或者`<LibaryName>_LIBRARIES`等,表示 库链接文件路径,比如`OpenCV_LIBS`。 - 如果我们引用的第三方库没有提供Find<LibaryName>.cmake,我们可以自己写,只需要填充上面上面变量,就可以使用find_package,实际一般用如下几个函数确定这三个变量,而这几个函数默认都会去CMAKE_PREFIX_PATH查找: - FIND_PACKAGE_HANDLE_STANDARD_ARGS:<LibaryName>_FOUND - find_path:获得<LibaryName>_INCLUDE_DIR目录。 - find_library:获得<LibaryName>_LIBRARY 目录。 - 结合前面的`include_directories/link_libraries`引用对应的`<LibaryName>_INCLUDE_DIR/<LibaryName>_INCLUDE_DIR`就引入第三方库了。 - 参考 https://zhuanlan.zhihu.com/p/258118287 https://zhuanlan.zhihu.com/p/492932151 https://blog.csdn.net/mby1988/article/details/121706937 ## 1.4、CMake的简单例子 假如我们把所有的文件放在一起,假设其目录如下: ``` ├── add.cpp ├── add.h ├── CMakeLists.txt ├── main.cpp ├── sub.cpp └── sub.h ``` 那么我们的CMakeLists.txt可以如下编写: ``` #指定使用该CMakeList.txt文件需要的cmake最低版本 cmake_minimum_required(VERSION 3.5) #指定项目信息 project(calculator) #设置安装目录 set(INSTALL_DIR ${CMAKE_SOURCE_DIR}/install) #查找当前目录下的所有源文件 #并将名称保存到ALL_SRCS变量 aux_source_directory(. ALL_SRCS) #指定生成目标 #add_executable(calculator main.cpp add.cpp sub.cpp) add_executable(calculator ${ALL_SRCS}) #安装到安装目录 INSTALL(TARGETS calculator DESTINATION ${INSTALL_DIR}/bin) ``` # 2、动态库和静态库 ## 2.1、动态库和静态库区别 - 当程序与静态库链接时,静态库中所包含的所有函数方法都会被copy到最终的可执行文件中去。这就会导致最终生成的可执行代码量相对变多,相当于编译器将代码补充完整了。这种方式会让程序运行起来相对快一些,不过也会有个缺点: - 占用磁盘和内存空间,导致可执行exe程序过大。 - 另外,静态库会被添加到和它链接的每个程序中去, 而且这些程序运行时, 都会被加载到内存中,无形中又多消耗了更多的内存空间。 - 与动态库链接的可执行文件只包含它需要的函数方法的引用表,而不是所有的函数代码,只有在程序执行时, 那些需要的函数代码才会被拷贝到内存中。 - 这样就使可执行文件比较小, 节省磁盘空间,更进一步,操作系统使用虚拟内存,使得一份动态库驻留在内存中被多个程序使用,也同时节约了内存。 - 不过由于运行时要去链接库会花费一定的时间,执行速度相对会慢一些。 ## 2.2、[平台差异](https://blog.51cto.com/u_14832233/2993847) - `windows`中静态库是以`.lib`为后缀的文件,动态库是以`.dll`为后缀的文件。 - 但是注意,`windows`动态库工程将生成一个引入库`lib`文件和`dll`文件。 - 对一个`DLL`文件来说,其引入库文件`.lib`包含该`DLL`导出的函数和变量的符号名,而`.dll`文件包含该`DLL`实际的函数和数据。动态库的[引入库文件和静态库文件](https://blog.csdn.net/winsbb/article/details/123103724)尽管都是lib文件,但是有着本质的区别。 - linux中静态库是以.a为后缀的文件,动态库是以.so为后缀的文件。 - mac中静态库是以.a为后缀的文件,动态库是以.dylib为后缀的文件,以.framework为后缀的文件可能是静态库,也可能是动态库。 ## 2.3、包含静(动)态库的工程 假设我们的目录文件结构如下所示,打算将除法动态库`div.dll`, 乘法静态库`mul.lib`, `build`为创建的编译产物文件夹: ``` |── div |── include └──div.h ├── src └── div.c ├── CMakeLists.txt ├── mul.c ├── mul.c └── mul.h ├── CMakeLists.txt ├── build ├── CMakeLists.txt ├── main.c ``` `div`下文件内容分别为: `div.h`, 注意, [__declspec(dllexport)](https://www.cnblogs.com/depend-wind/articles/11126333.html)用于将DLL中的相关代码(类,函数,数据)暴露出来为其他应用程序使用,主要给msvc使用,所以如果是使用mingw导出动态库则无需声明: ``` #ifndef _DIV_H_ #define _DIV_H_ #define FLT_MAX 3.402823466e+38F __declspec(dllexport) float divide(float a, float b); #endif ``` div.c中为: ``` #include "div.h" float divide(float a, float b) { if (b == 0) { return FLT_MAX; } return a / b; } ``` div模块中的CMakeLists.txt为: ``` cmake_minimum_required(VERSION 3.5.0) project(div) include_directories(${PROJECT_SOURCE_DIR}/include) add_library(div SHARED src/div.c) # 需要外层CMakeLists.txt定义INSTALL_DIR install(TARGETS div DESTINATION ${INSTALL_DIR}/bin) install(FILES ${PROJECT_SOURCE_DIR}/include/div.h DESTINATION ${INSTALL_DIR}/include) ``` 同样的,mul.h中为: ``` #ifndef _MUL_H_ #define _MUL_H_ float multiply(float a, float b); #endif ``` mul.c: ``` #include "mul.h" float multiply(float a, float b) { return a*b; } ``` mul文件夹下的CMakeLists.txt内容是完全类似的: ``` cmake_minimum_required(VERSION 3.5.0) project(mul) #查找当前目录下的所有源文件, 并将名称保存到DIV_SRCS变量 aux_source_directory(. MUL_SRCS) include_directories(${CMAKE_SOURCE_DIR}) message(STATUS "MUL_SRCS: ${MUL_SRCS}") #生成静态链接库 add_library(mul STATIC ${MUL_SRCS}) #安装到外层CMakeLists.txt设置的目录INSTALL_DIR INSTALL(TARGETS mul DESTINATION ${INSTALL_DIR}/lib) ``` 最外层main.c只需要include两个子库的头文件`mul.h` 和`div.h`即可。 ``` #include <stdio.h> #include <stdlib.h> #include "mul.h" #include "div.h" // add() and sub() functions are defined here, // while multiply() and divide() functions are imported from libraries float add(float x, float y) { return x + y; } float sub(float x, float y) { return x - y; } void menu() { printf("**************************\n"); printf("****Wellcome to calculator!****\n"); printf("**** 1.add 2.sub *****\n"); printf("*****3.mul 4.div *****\n"); printf("*****0.exit *****\n"); printf("**************************\n"); } int main() { int n = 1; menu(); while (n) { printf("please input options:\n"); scanf("%d", &n); float x = 0, y = 0; switch (n) { case 1: printf("please input two nums!\n"); scanf("%f %f", &x, &y); printf("%f\n", add(x, y)); break; case 2: printf("please input two nums!\n"); scanf("%f %f", &x, &y); printf("%f\n", sub(x, y)); break; case 3: printf("please input two nums!\n"); scanf("%f %f", &x, &y); printf("%f\n", multiply(x, y)); break; case 4: printf("please input two nums!\n"); scanf("%f %f", &x, &y); printf("%f\n", divide(x, y)); break; case 0: printf("exit!\n"); break; default: printf("error!, please input again!\n"); } } } ``` 而最外层的CMakeLists.txt为: ``` #指定使用该CMakeList.txt文件需要的cmake最低版本 cmake_minimum_required(VERSION 3.5) #指定项目信息 project(calculator) #设置安装目录(PROJECT_BINARY_DIR为运行cmake命令的目录, 例如可以是${PROJECT_SOURCE_DIR}/build) set(INSTALL_DIR ${PROJECT_BINARY_DIR}/install) # 添加子目录,并包含头文件 add_subdirectory(mul) add_subdirectory(div) include_directories(${CMAKE_SOURCE_DIR}/mul) include_directories(${CMAKE_SOURCE_DIR}/div/include) #查找当前目录下的所有源文件, 并将名称保存到ALL_SRCS变量 aux_source_directory(. ALL_SRCS) #指定生成目标 #add_executable(calculator main.cpp add.cpp sub.cpp) add_executable(calculator ${ALL_SRCS}) # 链接 target_link_libraries(calculator mul) target_link_libraries(calculator div) #安装到安装目录 INSTALL(TARGETS calculator DESTINATION ${INSTALL_DIR}/bin) #install(TARGETS calculator div mul # RUNTIME DESTINATION ${INSTALL_DIR}/bin # LIBRARY DESTINATION ${INSTALL_DIR}/lib # ARCHIVE DESTINATION ${INSTALL_DIR}/lib/static) ``` 构建完成后,在build/install目录下就会有bin,include和lib文件夹,分别存放相关的可执行文件,动态库,静态库,头文件等,对于其他项目来说,这个install其实就是一个可以使用的第三方库了。 ## 2.3、从外部引用静(动)态库的工程 - 上一个例子是内部同时构建静态、动态库的例子,但是实际使用时,我们需要使用第三方库。 - 这里我们把上一节产生的install作为第三方库导入,因为都知道了include和lib库的路径(不知道路径就需要find_pakage操作),那么我们引入的CMakeLists.txt其实就很简单了: - 将install文件夹改名,拷贝到新项目中去; - 包含其头文件,指定链接搜索路径,链接即可: ``` ...... include_directories(${PROJECT_SOURCE_DIR}/third_party/div_lib/include) link_directories(${PROJECT_SOURCE_DIR}/third_party/div_lib/lib) target_link_libraries(test_calculator div_shared) ...... ``` ## 2.3、find_package操作 - 然而,在实际的开发中第三方库是很复杂的,可能存在各种依赖,使用者通常需要借助`find_package()`以及相关的Find<xxxLIB>.cmake,或者xxxLib-Config.cmake文件来查找相关的库目录,头文件目录,版本号等信息, 比如zip2库的[Findzip2.cmake](https://github.com/KillianBrnt/CPP_Babel/blob/f493dd034f3329346228f1006aeb541013038ff7/build/Findbzip2.cmake)。 - 为了加深理解,我们基于在另一个工程test_calculator中,通过`find_package()`命令引入前面自定义的动态库div_lib和静态库mul_lib。我们打算通过module模式引入div_lib, 通过config模式引入mul_lib,两者区别[参考](https://wuchenwei.blog.csdn.net/article/details/108318385?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-108318385-blog-127322424.pc_relevant_layerdownloadsortv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-108318385-blog-127322424.pc_relevant_layerdownloadsortv1&utm_relevant_index=1)。 - 项目结构如下: ``` ├── build ├── third_party ├── div_lib ├── include ├── div.h ├── lib ├── div_shared.dll ├── div_shared.lib ├── Finddiv_shared.cmake ├── mul_lib ├── include ├── mul.h ├── lib ├── mul.lib ├── mulConfig.cmake ├── CMakeLists.txt ├── main.c ``` - Finddiv_shared.cmake: ``` find_path(DIV_INCLUDE_DIR div.h ${PROJECT_SOURCE_DIR}/third_party/div_lib/include) find_library(DIV_LIBRARY NAMES div_shared PATHS ${PROJECT_SOURCE_DIR}/third_party/div_lib/bin ${PROJECT_SOURCE_DIR}/third_party/div_lib/lib) if (DIV_INCLUDE_DIR AND DIV_LIBRARY) set(DIV_FOUND TRUE) endif (DIV_INCLUDE_DIR AND DIV_LIBRARY) ``` - mulConfig.cmake: ``` find_path(MUL_INCLUDE_DIR mul.h ${PROJECT_SOURCE_DIR}/third_party/mul_lib/include) find_library(MUL_LIBRARY mul.lib ${PROJECT_SOURCE_DIR}/third_party/mul_lib/lib) if (MUL_INCLUDE_DIR AND MUL_LIBRARY) set(MUL_FOUND TRUE) endif (MUL_INCLUDE_DIR AND MUL_LIBRARY) ``` - 项目最外层的CMakeLists.txt: ``` cmake_minimum_required(VERSION 3.5) project(test_calculator) set(INSTALL_DIR ${PROJECT_BINARY_DIR}/install) # 使用module模式 set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/third_party/div_lib") find_package(div_shared REQUIRED) # 使用config模式 set(mul_DIR ${PROJECT_SOURCE_DIR}/third_party/mul_lib) find_package(mul CONFIG REQUIRED) if(DIV_FOUND AND MUL_FOUND) #include_directories(${DIV_INCLUDE_DIR} ${MUL_INCLUDE_DIR}) aux_source_directory(. ALL_SRCS) add_executable(test_calculator ${ALL_SRCS}) target_include_directories(test_calculator PRIVATE ${DIV_INCLUDE_DIR} ${MUL_INCLUDE_DIR}) target_link_libraries(test_calculator ${DIV_LIBRARY} ${MUL_LIBRARY}) INSTALL(TARGETS test_calculator RUNTIME DESTINATION ${INSTALL_DIR} LIBRARY DESTINATION ${INSTALL_DIR} ARCHIVE DESTINATION ${INSTALL_DIR}) else() message("required libraries div or mul not found! exit!") endif() ``` # 2.4、显式调用dll 除了可以在编译时链接相关的第三方库,在windows下,还可以调用`LoadLibrary`/`FreeLibrary`以显式链接到 DLL, [参考](https://blog.csdn.net/junxuezheng/article/details/126630681)。 ## 2.5、参考 - CMake入门教程 https://juejin.cn/post/6844903557183832078 - find_package参考 - https://zhuanlan.zhihu.com/p/97369704?utm_source=wechat_session - https://www.cnblogs.com/penuel/p/13983503.html - https://www.freesion.com/article/55211398447/ - https://wuchenwei.blog.csdn.net/article/details/108318385?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-108318385-blog-127322424.pc_relevant_layerdownloadsortv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-108318385-blog-127322424.pc_relevant_layerdownloadsortv1&utm_relevant_index=1 - CMake教程 https://zhuanlan.zhihu.com/p/258118287 https://zhuanlan.zhihu.com/p/492932151 https://blog.csdn.net/mby1988/article/details/121706937
上一篇:
C++ 部分反射功能的实现(通过字符串获取对象)
下一篇:
雷达成像与融合相关技术
0
赞
106 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
立即登录
, 发表评论.
没有帐号?
立即注册
0
条评论
More...
文档导航
没有帐号? 立即注册