0%

CMake同时编译cpp和cu文件,并且生成动态链接库

CMake同时编译cpp和cu文件,并且生成动态链接库

前言

最近碰到一个项目需求,在ubuntu22的环境下,使用TensorRT框架推理优化神经网络模型,使用cuda并行处理多路输入,并将这部分功能封装为动态链接库,方便主程序调用。

这个需求主要涉及到cmake编译生成动态链接库,以及cpp和cu文件如何同时编译。

cmake生成链接库及调用

动态库

假如有三个cpp文件,main.cpp,a.cpp, b.pp,项目文件目录如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── bin
├── build
├── CMakeLists.txt
├── include
│ ├── a.h
│ └── b.h
├── lib
├── README.md
└── src
├── A
│ ├── a.cpp # 生成动态库A
│ └── CMakeLists.txt
├── B
│ ├── b.cpp # 生成动态库B
│ └── CMakeLists.txt
└── Main
├── CMakeLists.txt # 主程序
└── main.cpp

其中调用关系为:主程序调用库A,A依赖于库B

项目目录下的CMakeLists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ./CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(Test)

# 设置项目文件路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib) # 静态库
set(BIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin) # 编译输出结果
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include) # 外部头文件
set(PRO ${CMAKE_CURRENT_SOURCE_DIR}) # 自定义库路径

# include_directories(${HEAD_PATH}) # 链接外部头文件

# 添加子目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/A) # 动态链接库目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/B) # 动态链接库目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/Main) # 主程序文件目录

Main程序目录下的CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./src/Main/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(Main)

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SRC)

set(EXECUTABLE_OUTPUT_PATH ${BIN_PATH})
add_executable(Main ${SRC})

target_include_directories(Main PRIVATE ${HEAD_PATH})
target_include_directories(Main PRIVATE ${LIB_SRC_PATH}/include)

# 链接库文件
target_link_directories(Main PRIVATE ${LIB_PATH})
target_link_libraries(Main PRIVATE A)

A库目录下的CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./src/A/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(A)

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SRC)

set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(A SHARED ${SRC})

target_include_directories(A PRIVATE ${HEAD_PATH})
target_include_directories(A PRIVATE ${LIB_SRC_PATH}/include)

# 链接库文件B
target_link_directories(A PRIVATE ${LIB_PATH})
target_link_libraries(A PRIVATE B)

B目录下的CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
# ./src/B/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(B)

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SRC)

set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(B SHARED ${SRC})

target_include_directories(B PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/header)
target_include_directories(B PRIVATE ${LIB_SRC_PATH}/include)

cpp和.h文件

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
// a.h
void this_a();

// b.h
void this_b();

// a.cpp
#include <iostream>
#include <b.h>

void this_a(){
std::cout<<"This is A!"<<std::endl;
this_b();
}

// b.cpp
#include <iostream>

void this_b(){
std::cout<<"This is B!"<<std::endl;
}

// main.cpp
#include <iostream>
#include "a.h"

int main(){
std::cout << "this is Main!" << std::endl;
this_a();
}

输出结果:

1
2
3
this is Main!
This is A!
This is B!

疑问? 为什么这里exe调用的时候只指定了A的库位置,但是没影响输出?

  • 静态库本身不携带依赖的库代码,因此:当可执行文件链接静态库时,必须显式链接静态库的所有依赖库(包括直接/间接依赖)。

  • 动态库默认不自动传递依赖,如果希望可执行文件自动获取其依赖,需通过 target_link_libraries 传递依赖链,这里使用了target_link_libraries自动传递了依赖,

  • 若出现链接错误(如 undefined reference),通常是因为未正确传递依赖链。可使用以下命令观察实际链接参数:

    1
    make VERBOSE=1

关于private和public的不同

在 CMake 中,PUBLICPRIVATE 是用于控制目标(如库或可执行文件)属性传递性的关键字,它们的核心区别在于:
PRIVATE 定义的属性仅作用于当前目标,而 PUBLIC 定义的属性会同时作用于当前目标和依赖它的其他目标

关键字 作用范围 典型场景
PRIVATE 仅当前目标 内部实现依赖(如仅自己需要的头文件、编译选项、库)
PUBLIC 当前目标 + 依赖它的其他目标 接口依赖(如头文件暴露给使用者、需要传递的链接库)

静态库

将上面的两个动态库改为静态库后,注意,生成两个库的静态库后,这里不执行两个库的CMake文件,会出现链接错误:

1
undefined reference to `this_b()'

这就对应上述所说的:当可执行文件链接静态库时,必须显式链接静态库的所有依赖库(包括直接/间接依赖)。

因此,这个时候我们显式链接静态库的所有依赖库。在./src/Main/CMakeLists.txt中添加如下内容:

1
2
target_link_directories(Main PRIVATE ${PRO}/temp)
target_link_libraries(Main PRIVATE B)

将TensorRT推理后的模型封装为库

项目cmake代码

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
cmake_minimum_required(VERSION 3.10)

# 设置程序依赖路径,此处需要根据实际生产环境进行修改
set(CUDA_TOOLKIT_ROOT_DIR cuda路径) # cuda安装路径
set(TRT_ROOT tensorRT路径) # tensorRT路径

# 设置cuda编译选项
set(CMAKE_CUDA_COMPILER ${CUDA_TOOLKIT_ROOT_DIR}/bin/nvcc) # 显式的指定cuda编译器
set(CMAKE_CUDA_ARCHITECTURES OFF) # 让CMake和nvcc自动检测当前可用的GPU架构

project(TRT)

# 设置项目文件路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib) # 静态库
set(BIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin) # 编译输出结果
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include) # 外部头文件
set(LIB_SRC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/FreqBandRecognitionTRT) # 自定义库路径


# 设置子项目输出文件名称
set(EXE_TRT TestExe) # 测试程序名称
set(SO_TRT TestDll) # 生成的动态链接库名称

include_directories(${HEAD_PATH}) # 链接外部头文件
include_directories(${CUDA_TOOLKIT_ROOT_DIR}/include) # cuda头文件
include_directories(${TRT_ROOT}/include) # tensorRT头文件

link_directories(${TENSORRT_ROOT}/lib) # tensorRT静态库

# 设置环境变量,确保运行时可以找到TensorRT库
set(ENV{PATH} "tensorRT路径/bin:$ENV{PATH}")
set(ENV{LD_LIBRARY_PATH} "tensorRT路径/lib:$ENV{LD_LIBRARY_PATH}")

# 启动cuda编译
if (CMAKE_CUDA_COMPILER)
enable_language(CUDA) # 告诉系统要使用CUDA语言,绝不可以省略
message(STATUS "CUDA support enabled.")

if (POLICY CMP0146) # 忽略CMP0146警告
cmake_policy(SET CMP0146 OLD)
endif ()
include(FindCUDA)
set(CUDA_ARCH_LIST Auto CACHE STRING "List of CUDA architectures (e.g. Pascal, Volta, etc)\
or compute capability version (6.1, 7.0, etc) to generate code for. Set to Auto for \
automatic detection (default).")
cuda_select_nvcc_arch_flags(CUDA_ARC_FLAGS ${CUDA_ARC_LIST})
list(APPEND CUDA_NVCC_FLAGS ${CUDA_ARCH_FLAGS})
else ()
message(WARNING "CUDA Support disable.")
endif ()

# 添加编译选项 -Wno-deprecated-declarations
add_compile_options(-Wno-deprecated-declarations)

# 添加子目录
# add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/TestDll) # 动态链接库目录
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/TestExe) # 测试文件目录

封装成库的cmake代码

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.10)
project(TestDll VERSION 1.0 LANGUAGES CXX CUDA) # 允许cuda编译

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC)
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${SO_TRT} STATIC ${SRC})
target_include_directories(${SO_TRT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/header) # 链接内部头文件
target_include_directories(${SO_TRT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # 外部引用头文件
set_target_properties(${SO_TRT} PROPERTIES CUDA_SEPARABLE_COMPILATION ON) # 使用cuda编译
target_link_libraries(${SO_TRT} PUBLIC nvinfer nvinfer_plugin nvonnxparser) # 链接tensorRT库
target_link_directories(${SO_TRT} PUBLIC ${TRT_ROOT}/lib)
target_link_libraries(${SO_TRT} PUBLIC cufft) # 链接cuda库
target_link_directories(${SO_TRT} PUBLIC ${CUDA_TOOLKIT_ROOT_DIR}/lib)

只需要更改此处的add_library参数,即可控制生成的是动态库还是静态库

动态库调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.10)

project(TestExe VERSION 1.0 LANGUAGES CXX CUDA)

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC)

set(EXECUTABLE_OUTPUT_PATH ${BIN_PATH})
add_executable(${EXE_TRT} ${SRC})

target_include_directories(${EXE_TRT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/header)
target_include_directories(${EXE_TRT} PRIVATE ${LIB_SRC_PATH}/include)

# 链接库文件
target_link_directories(${EXE_TRT} PRIVATE ${LIB_PATH})
target_link_libraries(${EXE_TRT} ${SO_TRT})
target_link_libraries(${EXE_TRT} ${CUDA_LIBRARIES})

# 添加生成的库的链接库文件
set_target_properties(${EXE_TRT} PROPERTIES CUDA_SEPARABLE_COMPILATION ON) # 使用cuda编译
target_link_libraries(${EXE_TRT} nvinfer nvinfer_plugin nvonnxparser) # 链接tensorRT库
target_link_directories(${EXE_TRT} PUBLIC ${TRT_ROOT}/lib)
target_link_libraries(${EXE_TRT} cufft) # 链接cuda静态库
target_link_directories(${EXE_TRT} PUBLIC ${CUDA_TOOLKIT_ROOT_DIR}/lib)

调用动态库时,可以不显式说明调用的库的位置,但是为了安全起见,此处最好说明

静态库调用

静态库调用一定要加上上述的要调用的库的链接库文件,否则会出现链接错误。

踩坑记录

  1. 在cpp文件中使用cuda的库使用cmake编译时,会导致链接错误

    原因:没有使用nvcc自动分离cuda代码和cpp代码。

    解决方案:将cpp文件改为cu结尾

  2. 在链接库时,若没有指定,而目录下同时有静态库和动态库,会优先调用哪一个?

    动态库