Linux(Ubuntu)环境下使用CMake生成so动态库并调用

May 14,2024   5947 words   22 min

Tags: C/C++

在很久很久以前,我用一篇笔记记录过如何利用Visual Studio将C++代码打包成Windows平台下的DLL动态链接库以供使用。最近在做项目的时候,又遇到了类似的需求。只不过不同点在于要在Ubuntu平台下实现。所以这篇笔记就再次简单记录一下如何在Ubuntu平台下利用CMake工具将C++代码打包成动态库.so文件以供调用,以备以后查阅。

1. CMake构建可执行程序

1.1 基本程序

尽管在之前的博客已经“无数次”使用CMake构建项目了,这里为了笔记的完整性,还是简单提一下。一个最简单的CMake项目由CMakeLists.txt文件和代码文件构成。一个简单的CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.2)
project(cmake_basic_example)

set(CMAKE_CXX_STANDARD 11)

add_executable(cmake_basic_example main.cpp)

首先通过cmake_minimum_required指定了编译本CMake项目所需的最低版本,然后指定了项目的名称。同时,通过CMAKE_CXX_STANDARD变量设置了C++的编译标准为C++11。然后,利用add_executable命令构建名为cmak_basic_example的可执行文件,对应的代码是后面的main.cpp。完整内容如下:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

编写代码完成后,在终端中输入如下命令即可编译项目,得到可执行文件:

mkdir build
cd build
cmake ..
make

运行结果如下。

1.2 稍微复杂的程序

上面的程序比较简单,没有任何额外的依赖库。而在实际开发中,必然会依赖很多库,例如OpenCV、Eigen等。在CMake项目中,配置这些依赖也是十分简单的。一个示例CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.2)
project(cmake_basic_example)

set(CMAKE_CXX_STANDARD 11)

# Eigen
find_package(Eigen3 REQUIRED)
include_directories(${EIGEN3_INCLUDE_DIRS})

# OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(cmake_basic_example main.cpp)
target_link_libraries(cmake_basic_example ${OpenCV_LIBS})

一些部分和上面是相同的,就不再赘述,主要增加了三个命令。首先是find_package,这个命令是让CMake去寻找是否有指定的依赖库。后面的REQUIRED关键字表示必须找到,否则报错。第二个命令是include_directories,这个命令表示找到对应的库以后,将其所在的文件夹路径include到项目中来,这样,在写代码#include的时候就能比较顺利找到相关文件。这个命令的写法是相对固定的,基本都是XXX_INCLUDE_DIRS。第三个命令是target_link_libraries,表示将依赖库对应的库文件和项目关联起来,这样编译的时候才不会报错。否则只有前面include_directories的头文件,真正编译的时候就会遇到“为定义的引用”这种错误。对应的main.cpp如下:

#include <iostream>
#include <opencv2/opencv.hpp>
#include <Eigen/Dense>

using namespace cv;
using namespace std;
using namespace Eigen;


int main() {
    Mat m1(4, 3, CV_8UC1);
    cout << m1 << endl << endl;

    MatrixXd mat(3, 3);
    mat(0, 0) = 1.3;
    cout << mat << endl;

    return 0;
}

编写代码完成后,在终端中输入如下命令即可编译项目,得到可执行文件:

mkdir build
cd build
cmake ..
make

运行效果如下。

2. CMake构建动态库

上面我们介绍了如何用CMake构建可执行程序,核心是add_executable命令。那么如何构建动态库呢?核心命令则是add_library命令,下面介绍。

2.1 基本动态库

首先展示不依赖其它库的动态库构建。我们要写的代码由头文件和代码文件组成,头文件可以看作是代码文件的“目录”。CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.2)
project(cmake_library_example)

set(CMAKE_CXX_STANDARD 11)

add_library(example SHARED example.cpp)

大部分内容和1.1相同,不同的就是add_library命令。该命令会构建一个名为libexample.so的动态库(lib前缀是自动加上的),代码内容来自于example.cpp。另外,需要注意的是这里的关键词SHARED,如果没有,那么生成的就是.a结尾的静态库文件,加上则是.so动态库。简而言之,.a为Linux下的静态库文件,可以看作是一个或多个.o文件(Linux中的.o文件相当于Windows中的.obj文件)的集合。.so文件为动态库(共享库)文件,类似于Windows平台中的DLL文件。example.hexample.cpp分别如下。

#include <iostream>

using namespace std;

void example_func();

int add_func(int a, int b);

在头文件中,我们声明了两个函数。

#include "example.h"

void example_func() {
    cout << "print from example_func" << endl;
}

int add_func(int a, int b) {
    return a + b;
}

在cpp文件中,我们编写了具体的处理代码。完成代码编写以后,在终端中输入如下命令即可编译项目,得到动态库文件:

mkdir build
cd build
cmake ..
make

你会发现,在CMake中,构建可执行文件和动态库文件其实流程是几乎相同的。编译完成效果如下。

2.2 稍微复杂的动态库

前面的动态库我们没有依赖其它库,但实际上我们需要其它库的支持。和1.2部分一样的,我们还是以OpenCV和Eigen两个库为例。CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.2)
project(cmake_library_example)

set(CMAKE_CXX_STANDARD 11)

# Eigen
find_package(Eigen3 REQUIRED)
include_directories(${EIGEN3_INCLUDE_DIRS})

# OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

add_library(example SHARED example.cpp)
target_link_libraries(example ${OpenCV_LIBS})

你会发现和1.2部分的CMake配置几乎是一样的,不同的就是add_executable变成了add_library。头文件和代码文件分别如下。

#include <iostream>
#include <opencv2/opencv.hpp>
#include <Eigen/Dense>

using namespace cv;
using namespace std;
using namespace Eigen;

void test_opencv();

void test_eigen();

我们分别写了两个函数来测试OpenCV和Eigen库的使用情况。具体实现如下:

#include "example.h"

void test_opencv() {
    Mat m1(4, 3, CV_8UC1);
    cout << m1 << endl << endl;
}

void test_eigen() {
    MatrixXd mat(3, 3);
    mat(0, 0) = 1.3;
    cout << mat << endl;
}

完成代码编写以后,在终端中输入如下命令即可编译项目,得到动态库文件:

mkdir build
cd build
cmake ..
make

编译完成效果如下。

3. CMake项目动态库的使用

上面,我们介绍了动态库的生成。生成后下一个问题自然是如何使用。其实也非常简单。还记得前面的target_link_libraries命令。核心的模式是头文件+动态库文件。下面介绍。

3.1 基本动态库的使用

这里我们使用2.1部分编译得到的简单动态库,其没有依赖其它额外的库。我们将头文件example.h和动态库文件libexample.so拷贝到项目目录下的include文件夹和lib文件夹(没有就自己新建),如下图所示。 然后编写CMakeLists.txt,内容如下:

cmake_minimum_required(VERSION 3.2)
project(lib_use_test)

set(CMAKE_CXX_STANDARD 11)

add_executable(lib_use_test main.cpp ${PROJECT_SOURCE_DIR}/include/example.h)
target_link_libraries(lib_use_test ${PROJECT_SOURCE_DIR}/lib/libexample.so)

你会发现大部分内容和1.2部分是相同的。一方面,我们将头文件通过add_executable命令放到项目里来,避免找不到头文件的情况,另一方面,我们还是利用target_link_libraries命令,将动态库文件也放到项目里来,避免出现“未定义的引用”错误。这里面稍微需要注意的是PROJECT_SOURCE_DIR这个变量。这个变量表示的是CMakeLists.txt所在文件夹的路径。由于CMake在编译的时候,默认会去/usr/lib这类文件夹去寻找动态库,所以我们需要明确的指定我们自己编译的动态库文件的路径,这样才不会报找不到库的错误。当然还有一种方法就算将我们自己的动态库文件拷贝一份到/usr/lib路径下面(个人并不推荐,最好不好随意修改系统层级的目录)。然后,我们编写测试代码,如下。

#include <iostream>
#include "include/example.h"

int main() {
    example_func();

    int a = 3, b = 9;
    int c = add_func(a, b);
    cout << c << endl;

    return 0;
}

最后,在终端中输入如下命令即可编译项目,得到可执行文件:

mkdir build
cd build
cmake ..
make

运行效果如下。 也说明我们成功的在另一个CMake项目中调用了先前编译好的动态库文件。

3.2 稍微复杂动态库的使用

这里,我们使用2.2部分的头文件和编译得到的动态库。CMakeLists.txt文件如下。

cmake_minimum_required(VERSION 3.2)
project(lib_use_test)

set(CMAKE_CXX_STANDARD 11)

# Eigen
find_package(Eigen3 REQUIRED)
include_directories(${EIGEN3_INCLUDE_DIRS})

# OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(lib_use_test main.cpp ${PROJECT_SOURCE_DIR}/include/example.h)
target_link_libraries(lib_use_test ${PROJECT_SOURCE_DIR}/lib/libexample.so ${OPENCV_LIBS})

可以看到,整体流程和1.2还是差不多的。我们还是首先找到了OpenCV和Eigen这两个库,并把对应的库通过target_link_libraries放进来。同时,也把我们自己的动态库也放了进来。对应的测试代码如下:

#include <iostream>
#include "include/example.h"

int main() {
    test_eigen();

    test_opencv();

    return 0;
}

最后,在终端中输入如下命令即可编译项目,得到可执行文件:

mkdir build
cd build
cmake ..
make

运行效果如下。 至此,我们便掌握了调用更为复杂的动态库的方法。

最后,需要注意的是,对于不同架构的平台,如x86、ARM等,彼此编译的动态库是不兼容的。因此,如果要跨平台运行代码,最好拿着源码在目标平台重新编译得到.so文件,才可以正常运行。

4. 参考资料

  • [1] https://blog.csdn.net/qq_62815252/article/details/133580376
  • [2] https://blog.csdn.net/fengruoying93/article/details/102641632

本文作者原创,未经许可不得转载,谢谢配合

返回顶部