C++项目文件组织与编译安装(CMake)

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库的基本步骤如下:

  1. 定义库名和源文件:首先,你需要确定你的库的名字以及构成这个库的源文件。
set(MODULE_NAME MyModule)
set(SOURCES src1.cpp src2.cpp)
  1. 使用add_library创建MODULE库:需要指定库的名字,类型(在这个例子中是MODULE),以及源文件。
add_library(${MODULE_NAME} MODULE ${SOURCES})
  1. 指定库的输出目录:这个目录应该是一个绝对路径,你可以使用CMake的变量来生成这个路径。
set_target_properties(${MODULE_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins)

以上就是在CMake中创建MODULE库的基本步骤。可能还需要根据实际需求来设置其他的目标属性,例如包含目录、链接库等。

MODULE库的使用和链接

使用MODULE库的一个基本的步骤:

  1. 加载MODULE库:在你的项目中使用MODULE库,你需要在你的代码中动态加载它。在C++中,你可以使用dlopen(在Unix-like系统中)或LoadLibrary(在Windows系统中)来加载库。你需要提供库的路径作为参数。
void* handle = dlopen("path/to/your/library.so", RTLD_LAZY);
  1. 使用MODULE库中的函数:加载库后,你可以使用dlsym(在Unix-like系统中)或GetProcAddress(在Windows系统中)来获取库中函数的地址。你需要提供库的句柄和函数的名称作为参数。
void (*func)() = (void (*)())dlsym(handle, "function_name");
  1. 关闭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

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值