目录
C++项目文件组织与编译安装(CMake)
文件结构
/YourSDK
|-- CMakeLists.txt
|-- /docs
||-- ...
|-- /include
||-- YourSDK
|||-- common.h
|||-- /graphics
||||-- renderer.h
||||-- ...
|||-- /network
||||-- tcp_client.h
||||-- ...
|||-- ...
|-- /src
||-- /graphics
|||-- renderer.cpp
||||-- CMakeLists.txt
|||-- ...
||-- /network
|||-- tcp_client.cpp
||||-- CMakeLists.txt
|||-- ...
||-- ...
|-- /tests
||-- /graphics
|||-- test_renderer.cpp
|||-- ...
||-- /network
|||-- test_tcp_client.cpp
|||-- ...
||-- ...
|-- /examples
||-- example1.cpp
||-- ...
|-- /third_party
||-- /lib1
||-- /lib2
||-- ...
|-- /scripts
||-- build.sh
||-- ...
|-- /README.md
|-- /LICENSE.txt
CMakeLists.txt:这是CMake的主配置文件,它描述了如何构建整个项目。
docs:存放项目的文档。
include:所有公共头文件都放在这里,按模块组织。
src:源代码文件,结构应与include目录相匹配。
tests:包含所有的单元测试和集成测试。
examples:提供SDK使用的示例代码。
third_party:存放所有第三方库和依赖。
scripts:构建、测试和部署的脚本。
README.md:项目的说明文档。
LICENSE.txt:项目的开源协议文件。
CMakeLists
CMakeLists.txt 文件通常包含以下几个部分:
项目定义:使用 project() 命令定义项目的名称和版本。
设置变量:使用 set() 命令定义和设置变量。
添加目标:使用 add_executable() 和 add_library() 命令定义构建目标。
链接库:使用 target_link_libraries() 命令链接库。
这种结构化的方式使得文件更加有组织,更容易阅读和理解。
命令 | 描述 | 示例 |
---|---|---|
project() | 定义项目名称和版本 | project(MyApp VERSION 1.0) |
set() | 定义和设置变量 | set(SOURCES main.cpp util.cpp) |
add_executable() | 定义可执行文件 | add_executable(MyApp ${SOURCES}) |
add_library() | 定义库 | add_library(MyLib SHARED ${SOURCES}) |
target_link_libraries() | 链接库 | target_link_libraries(MyApp MyLib) |
cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
# 定义项目名
project(MyApp VERSION 1.0)
# 输出项目名
message("Building project: ${PROJECT_NAME}")
# 设置编译选项
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 设置编译类型
set(CMAKE_BUILD_TYPE "Debug")
# 设置源文件
set(SOURCES ${PROJECT_SOURCE_DIR}/src/main.cpp)
# 设置MPI相关变量
set(MPI_C_COMPILER_INCLUDE_DIRS /opt/hpcx/ompi/include CACHE PATH "")
# 添加子目录,多级cmakelist
add_subdirectory(include)
find_package(OpenCV REQUIRED)
# 如果本cmakelist不是主文件,那么添加OpenCV,方便在其他的获得主cmakelist文件中调用,不需要重复find
include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIBRARY_DIRS})
add_definitions(${OpenCV_DEFINITIONS})
# 添加可执行文件
add_executable(${PROJECT_NAME} ${SOURCES})
# 链接库
target_link_libraries(${PROJECT_NAME} pthread stdc++fs)
# 添加头文件搜索路径
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_include_directories(${PROJECT_NAME} PUBLIC ${OpenCV_INCLUDE_DIRS})
以下是一些常见的与命名相关的CMake变量:
变量名 | 描述 (中文解释) |
---|---|
PROJECT_NAME | 当前项目的名称 |
CMAKE_PROJECT_NAME | 最顶层项目的名称 |
PROJECT_SOURCE_DIR | 当前项目源代码的根目录 |
PROJECT_BINARY_DIR | 当前项目构建的根目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前CMakeLists.txt所在的目录 |
add_subdirectory的基本使用
add_subdirectory(添加子目录)是CMake中的一个命令,它允许我们将项目分解成更小、更易于管理的部分。
在CMake中,当你使用add_subdirectory(src)命令时,你实际上是告诉CMake去查找src目录下的CMakeLists.txt文件,并执行其中的命令。
# 主CMakeLists.txt
add_subdirectory(src)
add_subdirectory(tools)
这意味着,首先,CMake会处理src目录中的CMakeLists.txt,然后处理tools目录CMakeLists.txt。
find_library和find_package命令的基本使用
find_library函数默认会在系统的标准库路径中查找,例如/usr/lib/和/usr/local/lib/
自定义库查找路径 (Customizing Library Search Paths)
幸运的是,find_library提供了PATHS选项,允许我们指定额外的查找路径。
find_library(EXAMPLE_LIBRARY NAMES example PATHS /path/to/my/libs)
这会使CMake首先在/path/to/my/libs目录下查找example库,然后再查找系统默认路径。我们可以使用多个PATHS来指定多个查找路径。
find_package(OpenCV REQUIRED)
# 如果本cmakelist不是主文件,那么添加OpenCV,方便在其他的获得主cmakelist文件中调用,不需要重复find
include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIBRARY_DIRS})
add_definitions(${OpenCV_DEFINITIONS})
add_library(hello SHARED hello.h hello.cpp)
target_link_libraries(hello ${OpenCV_LIBRARIES})
#在本cmakelist文件中添加OpenCV
target_include_directories(hello PUBLIC ${OpenCV_INCLUDE_DIRS})
使用 find_package(OpenCV REQUIRED) 命令来查找 OpenCV 库后,会设置一系列变量,其中包括:
OpenCV_FOUND: 一个布尔变量,指示是否找到了 OpenCV 库。
OpenCV_INCLUDE_DIRS: 包含 OpenCV 头文件目录的变量。
OpenCV_LIBRARIES: 包含 OpenCV 库文件的变量。
OpenCV_VERSION: 包含所找到的 OpenCV 版本号的变量。
OpenCV_DEFINITIONS: 包含 OpenCV 相关的定义的变量,可用于添加到项目中。
add_library命令的基本使用
add_library是CMake中的一个核心命令,用于添加库(libraries)到你的项目中。库是一个包含了预编译好的代码的文件,这些代码可以被你的应用程序或其他库所共享和重用。
在CMake中,add_library命令的基本语法如下:
add_library(<name> <SHARED|STATIC|MODULE|INTERFACE> [source1] [source2 ...])
add_library(hello SHARED hello.cpp hello.h )
其中,name是要创建的库的名称,<SHARED|STATIC|MODULE|INTERFACE>用于指定库的类型。你可以选择的类型包括SHARED(共享库,Shared Libraries)、STATIC(静态库,Static Libraries)、MODULE(模块库,Module Libraries)或INTERFACE(接口库,Interface Libraries)。[source1] [source2 …]则是构成库的源代码文件。
类型 | MODULE(模块库) | SHARED(共享库) | STATIC(静态库) |
---|---|---|---|
链接方式 | 不直接链接到其他目标,通常在运行时动态加载 | 链接到使用它的目标,运行时动态加载 | 在编译时链接到使用它的目标 |
适用场景 | 插件系统,需要运行时动态加载的代码 | 需要共享代码的多个程序 | 小型程序,或需要避免动态链接的场景 |
文件扩展名(Unix-like) | .so | .so | .a |
文件扩展名(Windows) | .dll | .dll | .lib |
CMake中创建MODULE库
在理解了MODULE库的基本概念和用途后,让我们进一步探索如何在CMake中创建MODULE库。
3.1 详细步骤与代码示例
在CMake中创建MODULE库的基本步骤如下:
- 定义库名和源文件:首先,你需要确定你的库的名字以及构成这个库的源文件。
set(MODULE_NAME MyModule)
set(SOURCES src1.cpp src2.cpp)
- 使用add_library创建MODULE库:需要指定库的名字,类型(在这个例子中是MODULE),以及源文件。
add_library(${MODULE_NAME} MODULE ${SOURCES})
- 指定库的输出目录:这个目录应该是一个绝对路径,你可以使用CMake的变量来生成这个路径。
set_target_properties(${MODULE_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins)
以上就是在CMake中创建MODULE库的基本步骤。可能还需要根据实际需求来设置其他的目标属性,例如包含目录、链接库等。
MODULE库的使用和链接
使用MODULE库的一个基本的步骤:
- 加载MODULE库:在你的项目中使用MODULE库,你需要在你的代码中动态加载它。在C++中,你可以使用dlopen(在Unix-like系统中)或LoadLibrary(在Windows系统中)来加载库。你需要提供库的路径作为参数。
void* handle = dlopen("path/to/your/library.so", RTLD_LAZY);
- 使用MODULE库中的函数:加载库后,你可以使用dlsym(在Unix-like系统中)或GetProcAddress(在Windows系统中)来获取库中函数的地址。你需要提供库的句柄和函数的名称作为参数。
void (*func)() = (void (*)())dlsym(handle, "function_name");
- 关闭MODULE库:在你完成了对库的使用后,你应该用dlclose(在Unix-like系统中)或FreeLibrary(在Windows系统中)来关闭库。
dlclose(handle);
请注意,这些步骤涉及到操作系统级别的API,因此可能会因操作系统不同而略有差异。同时,你可能需要对错误进行处理,因为动态加载库和获取函数地址都可能失败。
如果MODULE库依赖于其他库,你应该使用target_link_libraries命令在CMake中为MODULE库链接这些库或者find_package()链接第三方库。这样,当你的MODULE库被加载时,它所依赖的库也会被正确地加载。推荐使用完整的相对路径(相对于CMakeLists.txt文件)或绝对路径。
target_link_libraries(${MODULE_NAME} PRIVATE ${DEPENDENT_LIBS})
请注意,由于MODULE库在链接阶段不解析符号,因此在链接库时应使用PRIVATE而不是PUBLIC或INTERFACE。
当我们需要创建最终的库时,我们可以链接所有这些模块库:
add_library(final_sdk final_sdk.cpp)
target_link_libraries(final_sdk mymodule other_module ...)
CMake中创建SHARED库和STATIC库
add_library(MySharedLib SHARED ${SOURCES})
add_library(MyStaticLib STATIC ${SOURCES})
除了库类型的指定(即SHARED、STATIC或MODULE)之外,其他的部分与modulel库都是一样的。
优缺点
优点 | 缺点 |
---|---|
节省内存,因为多个程序可以共享同一个库的副本 | 需要确保运行环境中有所需的库 |
程序更新时,只需更新库文件,而不是每个使用该库的程序 | 可能会出现版本冲突,导致程序崩溃 |
可执行文件较小 | 加载程序时可能会稍微慢一些,因为需要加载库 |
3.3.2 静态链接的优缺点
优点 | 缺点 |
---|---|
可执行文件是自包含的,不依赖于外部库 | 可执行文件较大 |
运行速度可能稍微快一些,因为所有代码都在一个文件中 | 更新库时,需要重新编译和链接所有使用该库的程序 |
不会出现版本冲突 | 内存使用率较高,因为每个程序都有其自己的库副本 |
优点 | 缺点 |
---|---|
动态加载和卸载:MODULE库可以在运行时动态加载和卸载,这为构建插件系统提供了可能性。 | 需要使用系统调用:使用MODULE库需要对系统调用有一定的了解,这可能会增加编程的复杂性。 |
减少启动时间和内存占用:由于MODULE库只在需要时才加载,因此可以减少应用程序的启动时间和内存占用。 | 错误处理:加载MODULE库和获取函数地址可能会失败,因此需要进行错误处理。 |
增强了代码的模块化:每个MODULE库都可以作为一个独立的模块,这有利于代码的模块化和组织。 | 符号冲突:如果两个MODULE库中有相同的符号,可能会导致符号冲突。 |
动态链接提供了更大的灵活性,但可能需要更多的维护。而静态链接则提供了稳定性,但牺牲了一些灵活性。
MODULE库非常适合于需要动态加载的场景,例如插件系统。如果你的应用程序有一些可选的功能,那么你可以将这些功能放入MODULE库中,只在用户需要时加载。
RPATH, RUNPATH与$ORIGIN的深入探讨
在嵌入式领域,我们经常会遇到各种链接问题,尤其是在动态链接的环境中。为了更好地理解这些问题,我们需要深入探讨RPATH, RUNPATH和$ORIGIN这些关键概念。
RPATH(Runtime Library Path,运行时库路径)是一个在可执行文件或共享库中嵌入的路径,它指定了动态链接器在运行时应该搜索的目录。这是一个非常有用的功能,尤其是当我们的程序依赖于某些非标准位置的库时。
例如,考虑以下情况:你有一个程序myprog,它依赖于libfoo.so这个库。这个库位于/opt/mylibs/目录下,而这个目录并不在系统的默认库搜索路径中。通过在myprog中设置RPATH为/opt/mylibs/,你可以确保myprog在运行时能够找到libfoo.so。
在CMake中,我们可以使用set_target_properties来设置RPATH。
set_target_properties(target PROPERTIES
BUILD_WITH_INSTALL_RPATH TRUE
INSTALL_RPATH "/path/to/lib")
这里的target是你的目标名称,/path/to/lib是你希望设置的RPATH路径。
为什么我们要这样设置呢?从心理学的角度来看,人们总是希望事情能够简单、直接。当我们设置了RPATH,我们就告诉了系统,当它需要查找动态链接库时,应该首先查找这个路径。这样,系统就不需要在其他地方浪费时间去查找了。
与RPATH类似,RUNPATH(运行路径)也是一个在可执行文件或共享库中嵌入的路径。但与RPATH不同的是,当设置了RUNPATH时,动态链接器会忽略LD_LIBRARY_PATH环境变量。这意味着,如果你的程序设置了RUNPATH,那么即使你在环境变量中指定了正确的库路径,动态链接器也不会使用它。
这种行为可能会导致一些混淆,但它实际上是为了增加安全性。考虑这样一个场景:攻击者可以控制LD_LIBRARY_PATH,并使其指向一个包含恶意版本的libfoo.so的目录。如果你的程序使用RPATH,那么攻击者可以通过修改LD_LIBRARY_PATH来劫持你的程序。但如果你使用RUNPATH,这种攻击就不再有效。
$ORIGIN是一个特殊的标记,它在动态链接时表示可执行文件或共享库的当前路径。这是一个非常有用的功能,尤其是当我们需要创建一个可移植的应用程序或库时。
假设我们有一个应用程序myapp,它依赖于libbar.so这个库。我们希望将这两个文件打包到一个目录中,并确保无论这个目录被复制到哪里,myapp都能正确地找到libbar.so。
当我们遇到一个动态链接问题时,第一反应往往是使用ldd工具来诊断。ldd(List Dynamic Dependencies)是一个列出程序动态依赖的工具。
例如,当我们有一个名为my_program的程序,我们可以使用以下命令来查看它的动态链接依赖:
ldd my_program
install命令的基本使用
install() 命令用于定义项目的安装步骤。不仅能安装目标文件,如库和可执行文件,还能复制目录和文件,设置他们的权限。二进制文件、库、脚本、头文件等。
安装库和可执行文件
以下是一个基本的 install() 命令示例,该命令将一个库文件安装到指定的目录:
install(TARGETS mylibrary DESTINATION lib)
在这个例子中,TARGETS 参数指定了要安装的目标(通常是一个已经通过 add_library() 或 add_executable() 定义的目标),DESTINATION 参数指定了目标的安装位置。这里,“mylibrary” 是目标库的名字,“lib” 是目标目录。这条命令的含义是将 “mylibrary” 安装到 “lib” 目录中。
文件和目录的选择
我们可以通过 install(DIRECTORY …) 命令来安装目录及其子目录。例如:
install(DIRECTORY ${CMAKE_SOURCE_DIR}/mydir DESTINATION share/mydir)
在这个示例中,${CMAKE_SOURCE_DIR}/mydir 是源目录,share/mydir 是目标目录。
文件匹配与过滤
CMake 允许我们使用 PATTERN 参数来过滤和匹配文件。以下命令将仅安装 .h 和 .hpp 文件:
install(DIRECTORY ${CMAKE_SOURCE_DIR}/src/Module
DESTINATION ${CMAKE_SOURCE_DIR}/Release/include
FILES_MATCHING
PATTERN "*.h"
PATTERN "*.hpp"
EXCLUDE
PATTERN "DefaultInternalModuleFactory.hpp"
PATTERN "InternalModuleFactory.hpp")
这段代码的目的是将所有 .h 和 .hpp 文件从源目录复制到目标目录,但排除两个特定的 .hpp 文件。但实际上,我们可能会发现子目录中的 .hpp 文件也被排除了。
在实际应用中,我们可能会遇到一个问题:当使用 install(DIRECTORY …) 命令时,CMake 会复制指定目录及其所有子目录,但如果我们只想安装特定的文件(如 .h 文件),却不希望空的子目录也被一并复制,该如何操作呢?
这里,我们可以采用一种综合策略,即结合 install(DIRECTORY …) 和 install(FILES …) 命令。首先使用 install(DIRECTORY …) 命令复制所有需要的文件和目录结构,然后通过 install(FILES …) 命令精确控制、剔除不需要的空子目录。
以下是一个示例代码,展示了如何综合利用这两种命令:
# 安装所有 .h 文件并保留目录结构
install(DIRECTORY ${CMAKE_SOURCE_DIR}/src/module/ DESTINATION include
FILES_MATCHING PATTERN "*.h")
# 剔除不需要的空子目录(示例)
install(FILES ${CMAKE_SOURCE_DIR}/src/module/subdir/file.h DESTINATION include/subdir)
在这个例子中,我们首先使用 install(DIRECTORY …) 命令复制所有 .h 文件及其目录结构,然后通过 install(FILES …) 命令精确指定了需要保留的文件和目录,从而避免了不必要的空子目录的复制。
参考链接
https://www.zhihu.com/column/c_1696517817774592001
https://cmake-doc.readthedocs.io/zh-cn/latest/index.html