鼠鼠学通信

首页 / C++

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

前言

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

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

cmake生成链接库及调用

动态库

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

.
├── 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

# ./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:

# ./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

# ./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

# ./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文件

// 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();
}

输出结果:

this is Main!
This is A!
This is B!

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

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

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

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

    make VERBOSE=1
    

关于private和public的不同

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

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

静态库

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

undefined reference to `this_b()'

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

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

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

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

项目cmake代码

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代码

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参数,即可控制生成的是动态库还是静态库

动态库调用

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. 在链接库时,若没有指定,而目录下同时有静态库和动态库,会优先调用哪一个?

    动态库