Content Table

创建使用动态链接库

想一想大多数时候我们的项目是不是所有代码都会放在同一个工程中?人少的时候问题不大,但当项目越来越大,开发人员越来越多,会发觉开发、管理能让人窒息,大家都绞在一起,出问题时互相推诿责任,各自有理,这时如果按照功能模块进行分组各自开发,以库的形式提供给其他人使用,就能够最大限度的并行开发,提高工作效率,而且项目的模块也很清晰,责任一目了然,此外使用动态链接库后还能够按模块升级,编译的速度也更快。下面就介绍怎么在工程中创建和使用动态链接库。

Windows 中叫动态链接库 (Dynamic Link Library: .dll),Linux 中叫共享库 (Shared Library: .so),Mac 下后缀为 .dylib,这几种叫法实际指的是一种类型的库,这里都统称为动态链接库吧。

理解动态链接库需要理解符号的概念,符号包含函数、变量或者类,分为公有符号和私有符号:

  • 公有符号: 在其他程序或者库使用的符号,需要根据用途使用宏进行标记:

    • Q_DECL_EXPORT: 编译为动态链接库时符号要标记为 Q_DECL_EXPORT,表明是导出的符号
    • Q_DECL_IMPORT: 在调用动态链接库时符号要标记为 Q_DECL_IMPORT,表明是导入的符号
  • 私有符号: 除了公有符号外的其他符号,在此库之外不应该被访问,不需要进行标记

    建议: 不要在头文件中声明私有符号。

符号上的标记 Q_DECL_EXPORTQ_DECL_IMPORT 不能同时存在,为了在导出和导入时使用同一个头文件, 头文件中包含下面的宏,编译时根据条件使用不同的宏就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <QtCore/qglobal.h>

// 根据条件定义 LIB 为不同的宏
#if defined(BUILD_LIB)
#define LIB Q_DECL_EXPORT
#else
#define LIB Q_DECL_IMPORT
#endif

// 使用 LIB 修饰符号,编译时会根据编译条件替换为 Q_DECL_EXPORT 或者 Q_DECL_IMPORT
class LIB Calculator {
...
};

为了达到了在导出和导入时使用同一个头文件的目的:

  • 生成动态链接库工程的 pro 文件中添加 DEFINES += BUILD_LIB,在编译的时候 LIB 就会被替换为 Q_DECL_EXPORT
  • 使用动态链接库工程的 pro 文件中千万不要加 DEFINES += BUILD_LIB,编译的时候发现没有定义 BUILD_LIB,则 LIB 就会被替换为 Q_DECL_IMPORT

说了这么多,还是不知道 Qt 中怎么创建和使用动态链接库,下面就以使用子工程的方式介绍创建和使用动态链接库,工程结构如下:

1
2
3
4
5
6
7
8
9
SharedLibraryTest
├── MyLibrary
│   ├── Calculator.cpp
│   ├── Calculator.h
│   └── MyLibrary.pro
├── MyProject
│   ├── MyProject.pro
│   └── main.cpp
└── SharedLibrary.pro

SharedLibraryTest 是 Subdirs Project,包含动态链接库的工程 (MyLibrary) 和使用动态链接库的工程 (MyProject)。

创建工程

Qt Creator 中创建工程的步骤如下:

  1. 创建工程 SharedLibraryTest:
    1. File > New File or Project... > Other Project > Subdirs Project
    2. 点击 Choose...
    3. 输入工程名 SharedLibraryTest 然后创建
  2. 创建工程 MyLibrary:
    1. 工程 SharedLibraryTest 上右键 > New Subproject... > Library > C++ Library
    2. 点击 Choose...
    3. 选择 Type 为 Shared Library
    4. 输入工程名 MyLibrary 然后创建
  3. 创建工程 MyProject:
    1. 工程 SharedLibraryTest 上右键 > New Subproject... > Application > Qt Console Application
    2. 点击 Choose...
    3. 输入工程名 MyProject 然后创建

修改 MyLibrary

为了清晰起见,我们先把 MyLibrary 下的 .h 和 .cpp 文件都删除掉,创建 C++ Class Calculator 得到 Calculator.h 和 Calculator.cpp,然后编辑它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件名: Calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

#include <QtCore/qglobal.h>

#if defined(BUILD_LIB)
#define LIB Q_DECL_EXPORT
#else
#define LIB Q_DECL_IMPORT
#endif

class LIB Calculator {
public:
Calculator();
int add(int a, int b) const;
};

LIB void work();

#endif // CALCULATOR_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 文件名: Calculator.cpp
#include "Calculator.h"
#include <QDebug>

Calculator::Calculator() {

}

int Calculator::add(int a, int b) const {
return a + b;
}

static void doWork() {
qDebug() << "work() -> doWork()";
}

void work() {
::doWork();
}

因为这个工程是生成动态链接库的,所以需要在工程的 pro 文件中增加 DEFINES += BUILD_LIB 这一句,这样编译时 LIBQ_DECL_EXPORT,工程的 pro 文件如下:

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
#-------------------------------------------------
#
# Project created by QtCreator 2017-11-05T08:33:53
#
#-------------------------------------------------

QT -= gui

TARGET = MyLibrary
TEMPLATE = lib

DEFINES += BUILD_LIB

# Output directory
CONFIG(debug, debug|release) {
output = debug
}
CONFIG(release, debug|release) {
output = release
}

DESTDIR = ../bin
OBJECTS_DIR = $$output
MOC_DIR = $$output
RCC_DIR = $$output
UI_DIR = $$output

SOURCES += \
Calculator.cpp

HEADERS += \
Calculator.h

unix {
target.path = /usr/lib
INSTALLS += target
}

下面的部分不是必须的,只是我常用来组织编译输出的目录:

1
2
3
4
5
6
7
8
9
10
11
12
CONFIG(debug, debug|release) {
output = debug
}
CONFIG(release, debug|release) {
output = release
}

DESTDIR = ../bin
OBJECTS_DIR = $$output
MOC_DIR = $$output
RCC_DIR = $$output
UI_DIR = $$output

提示:

  • 使用宏 LIB 在头文件里声明修饰公有符号 class LIB CalculatorLIB void work(),可以在其他模块中使用,而 doWork() 是私有符号,不应在头文件里声明,对于其他模块是不可访问的

  • 不用宏 LIB 修饰公有符号

    • 使用 MinGW,Clang 编译器时,Qt 能正常的编译出动态链接库,头文件中的类、函数等都能被其他工程使用,但是 VS 的编译器不能省
    • 其他模块中的对象使用传统的 SIGNAL SLOT 方式能正常的和动态链接库中的对象建立信号槽链接,如 QObject::connect(scrollBar, SIGNAL(valueChanged(int)), label, SLOT(setNum(int))),但是使用 Qt 5 新的信号槽语法 QObject::connect(scrollBar, &QScrollBar::valueChanged, [] {}) 时提示错误 signal not found in class XXX,也就是说想要使用新的语法建立信号槽链接就不能省去宏 LIB。原因是信号是内联函数,不使用宏的时候没有正确的导出。
  • 为什么要提省掉宏 BUILD_LIB 和 LIB 呢?
    以前我们有一个项目开发了一年多,运营得不错,有了第二个客户,这个客户的需求和第一个客户的需求有 80% 相同,然后我们就把第一个项目复制了一份,在此基础上进行修改,过段时间又有了第三个客户,还是同样的情况,绝大部分需求都是相同的,如果再继续复制一份进行修改,当相同的功能更新时,需要同时更新 3 个项目,容易出错,需要大量的测试,更幸福的是,很快我们就有了第四个客户,于是我们决定把项目中的公共部分根据功能模块提取出来作为动态链接库,以库的方式提供给其他模块调用。此时面临一个问题,如果动态链接库中的公有符号都使用宏来修饰,工作量也不小,后来发现不用宏也可以,高兴坏了,这样只需要按模块移动文件了,花了 2 天的时间就完成了任务。继续开发了一段时间后需要和库中的对象建立信号槽链接,已经习惯了新的链接语法,这时就发现了上面提到的 signal not found in class XXX 的错误,后来采用了性价比最高的办法,就是使用传统的信号槽链接语法来解决这个问题的

修改 MyProject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 文件名: main.cpp
#include <Calculator.h>
#include <QDebug>

int main(int argc, char *argv[]) {
Q_UNUSED(argc)
Q_UNUSED(argv)

// 调用库中的函数
work();

// 生成库中的类对象
Calculator calculator;
qDebug() << calculator.add(2, 3);

return 0;
}

工程的 pro 文件如下,没有 DEFINES += BUILD_LIB 这一句哦,因为这个工程是使用动态链接库的,LIB 应该为 Q_DECL_IMPORT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

INCLUDEPATH += $$PWD/../MyLibrary
LIBS += -L$$OUT_PWD/../bin -lMyLibrary

# Output directory
CONFIG(debug, debug|release) {
output = debug
}
CONFIG(release, debug|release) {
output = release
}

DESTDIR = ../bin
OBJECTS_DIR = $$output
MOC_DIR = $$output
RCC_DIR = $$output
UI_DIR = $$output

SOURCES += main.cpp

INCLUDEPATH += $$PWD/../MyLibrary 添加 MyLibrary 的路径到包含目录中,使用的时候就可以 include <Calculator.h> 这样包含头文件了, LIBS += -L$$OUT_PWD/../bin -lMyLibrary 则是引入工程 MyLibrary 生成的动态链接库。

很多人奇怪,-L 和 -l 到底是啥?

  • -L 指定动态链接库所在文件夹
  • -l 指定动态链接库的名字,不需要指定库的前缀和后缀,Qt 会为自动识别,因为不同的系统中动态链接库的前缀和后缀都不同
    • Windows 中前缀为空,后缀为 .dll
    • Linux 中前缀为空,后缀为 .so
    • Mac 中前缀为 lib,后缀为 .dylib
  • 如果不使用 -L 和 -l,直接使用 LIBS += 动态链接库的绝对路径或相对路径 也是可以的,例如 LIBS += C:/curl/bin/curl.dll
  • 还能使用通配符一次导入多个库,如 LIBS += C:/curl/bin/*.dll,当有 20 个 dll 要导入时,通配符的方式就很省事了

运行程序

编译、运行工程,控制台输出:

work() -> doWork()
5

可以看到编译输出目录生成了动态链接库,并且在工程中也成功使用了,以后复杂的项目就可以按模块进行开发了。当然也可以不必使用 Subdirs Project 的方式,而是每个模块一个工程,pro 文件进行简单的修改即可。

参考资料

  • Qt 的帮助文件中搜索: Creating Shared Libraries ,查看怎么创建动态链接库
  • Qt 的帮助文件中搜索: Third Party Libraries,查看怎么使用第三方的动态链接库