Qt 中如何创建和使用库
!(https://file.mculoop.com/images/2022/11/image-20221111151059907_1668150660.png)
## 库
将经常用到的程序模块化,制作成库。库的概念给编程带来明显的好处,不需要因为要开发或修改一个小软件,就要重写、编译大量的源代码,降低了开发难度,节约了可观的时间。
程序编译成可执行程序的过程一般包括:预处理、编译、汇编和链接几个步骤。我们所说的库就是在链接过程中用到的(动态库的显式链接除外),对不同类型的库有不同的处理模式。
库常分为动态库(共享库)和静态库。链接阶段使用静态库的方式是从静态库中获取用到的程序并集成到目标程序中(静态地存在),以后使用程序的时候就不再需要到静态库中去找了。而与之相对,动态库则在链接过程中不集成到目标程序中去,使用时动态地加载。
静态库文件的后缀一般是 `.lib`、`.a`,动态库文件的后缀一般是 `.dll`、`.so`。
<!--more-->
## 什么是动态链接库?
动态链接库(Dynamic Link Library,缩写 DLL),是微软在 Windows 操作系统中实现共享函数库概念的一种方式。这些库函数的扩展名是 `.dll`、`.ocx`(包含 ActiveX 控制的库)或者 `.drv`(旧式的系统驱动程序)。而 Qt 在所有平台上将动态库统称为共享库(Shared Library),所以在 Linux 下 `.so` (Shared Object)文件的作用与 Windows 下的 `.dll` 是一样的。
动态库的链接分为隐式链接和显式链接两种。
### 隐式链接
大多数情况下的选择隐式链接的方式,因为它的用法比较简单。当在程序中直接调用动态库的导出函数时,就要使用隐式链接的方式。在链接阶段,链接器要将目标程序与动态库的导入库(.Lib 文件)链接。
MSDN [确定要使用的链接方法](https://msdn.microsoft.com/zh-cn/library/253b8k2c.aspx) :
> 导入库仅包含加载 DLL 的代码和实现 DLL 函数调用的代码。在导入库中找到外部函数后,会通知链接器此函数的代码在 DLL 中。要解析对 DLL 的外部引用,链接器只需向可执行文件中添加信息,通知系统在进程启动时应在何处查找 DLL 代码。
### 显式链接
有时也要使用显式链接的方法,因为它更加灵活。
> 直到运行时,应用程序才知道需要加载的 DLL 的名称。例如,应用程序可能需要从配置文件获取 DLL 的名称和导出函数名。
>
> 如果在进程启动时未找到 DLL,操作系统将终止使用隐式链接的进程。同样是在此情况下,使用显式链接的进程则不会被终止,并可以尝试从错误中恢复。例如,进程可通知用户所发生的错误,并让用户指定 DLL 的其他路径。
>
> 如果使用隐式链接的进程所链接到的 DLL 中有任何 DLL 具有失败的 DllMain 函数,该进程也会被终止。同样是在此情况下,使用显式链接的进程则不会被终止。
>
> 因为 Windows 在应用程序加载时加载所有的 DLL,故隐式链接到许多 DLL 的应用程序启动起来会比较慢。为提高启动性能,应用程序可隐式链接到那些加载后立即需要的 DLL,并等到在需要时显式链接到其他 DLL。
>
> 显式链接下不需将应用程序与导入库链接。如果 DLL 中的更改导致导出序号更改,使用显式链接的应用程序不需重新链接(假设它们是用函数名而不是序号值调用 GetProcAddress),而使用隐式链接的应用程序必须重新链接到新的导入库。
显式加载动态链接库时,需要用到 `LoadLibrary()` 函数。然后使用 `GetProcAddress()` 函数获取动态链接库中导出函数的地址。
若要指定某个数据、函数或类被导出(即可被调用方使用),需在其前加上导出声明:`__declspec(dllexport)`
同样,若要导入某个数据、函数或类则应在其前面加上导入声明:`__declspec(dllimport)`
下面会用实例说明如何使用动态库(隐式/显式)和静态库。
## 如何创建动态链接库?
执行 文件 -> 新建文件或项目 -> Library,选择 C++ 库:
!(https://file.mculoop.com/images/2022/11/image-20221111151757170_1668151077.png)
类型选择共享库:
!(https://file.mculoop.com/images/2022/11/image-20221111151813238_1668151093.png)
根据需要选择模块,在使用 Qt 自动模板的情况下,QtCore 是必须选的:
!(https://file.mculoop.com/images/2022/11/image-20221111151832587_1668151112.png)
IDE 自动创建了项目的基础文件,包括一个项目文件、两个头文件以及一个 cpp 源文件:
!(https://file.mculoop.com/images/2022/11/image-20221111151847536_1668151127.png)
在项目文件中,我们可以看到模板为 lib ,makefile 级宏定义增加 `MYDLL_LIBRARY` 定义:
```
QT -= gui
TARGET = myDLL
TEMPLATE = lib
DEFINES += MYDLL_LIBRARY
SOURCES += mydll.cpp
HEADERS += mydll.h\
mydll_global.h
unix {
target.path = /usr/lib
INSTALLS += target
}
```
`TEMPLATE = lib` 是用来告诉 qmake 要编译为库。
可以通过 `CONFIG` 变量来指定编译为哪种库:
`CONFIG += dll `: The library is a shared library (dll)
`CONFIG += staticlib` : The library is a static library.
`CONFIG += plugin` : The library is a plugin.
默认为动态链接库。
在头文件 mydll_global.h 中定义了本项目使用的动态链接库导出、导入声明的宏: `MYDLLSHARED_EXPORT`。
```cpp
#ifndef MYDLL_GLOBAL_H
#define MYDLL_GLOBAL_H
#include "QtCore/qglobal.h"
#if defined(MYDLL_LIBRARY)
#define MYDLLSHARED_EXPORT Q_DECL_EXPORT
#else
#define MYDLLSHARED_EXPORT Q_DECL_IMPORT
#endif
#endif // MYDLL_GLOBAL_H
```
因为在项目文件中定义了 `MYDLL_LIBRARY` ,所以在编译时会将 `MYDLLSHARED_EXPORT` 定义为 `Q_DECL_EXPORT`。`Q_DECL_EXPORT` 又是什么呢?在 qtcore/qcompilerdetection.h 中有定义:
```cpp
#elif defined(__GNUC__)
...
# ifdef Q_OS_WIN
# define Q_DECL_EXPORT __declspec(dllexport)
# define Q_DECL_IMPORT __declspec(dllimport)
# elif defined(QT_VISIBILITY_AVAILABLE)
# define Q_DECL_EXPORT __attribute__((visibility("default")))
# define Q_DECL_IMPORT __attribute__((visibility("default")))
# define Q_DECL_HIDDEN __attribute__((visibility("hidden")))
# endif
...
```
这样做有助于代码的跨平台编译。
在 mydll.h 中我导出了一个类和一个函数:
```cpp
#ifndef MYDLL_H
#define MYDLL_H
#include "mydll_global.h"
class MYDLLSHARED_EXPORT MyDLL
{
public:
MyDLL();
int add(int a, int b);
};
#ifdef __cplusplus
extern "C" {
#endif
MYDLLSHARED_EXPORT int mul(int a, int b);
#ifdef __cplusplus
}
#endif
MYDLLSHARED_EXPORT short mul(char a, short b);
#endif // MYDLL_H
```
为什么 `mul` 函数的声明还要包裹 `extern "C"` 呢?
在这里是为了举个例子说明 C 与 C++ 在导出时的区别。为了支持函数的重载,C++ 对全局函数的处理方式与 C 有明显的不同。编译器编译函数的过程中会对函数名进行修饰,将函数的参数类型也加到函数名;而 C 语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名( `__cdecl` 方式)。例如在不加 `extern "C"` 声明时,`int mul(int a, int b)` 导出函数名为 `_Z3mulii` (其中的 3 表示函数名的长度,这种修饰规则叫做 name mangling,不同编译器甚至不同版本是不同的,真是混乱!详情参阅:https://en.wikipedia.org/wiki/Name_mangling );而加上 `extern "C"` 后则为 `mul`( `__cdecl` 方式);`short mul(char a, short b)` 没有加 `extern "C"`,导出函数名为 `_Z3mulcs`。
VC++ 与 MinGW 在 name mangling 规则上是不同的,Qt MinGW 编译的 c++ dll 是不能直接被 VC++ 调用的,一般要导出为 C 函数来使用。关于 name mangling 以及 VC++ 与 Qt MinGW 动态链接库的互相调用就不在本文中细说了。
源文件 mydll.cpp 无需多言:
```cpp
#include "mydll.h"
MyDLL::MyDLL()
{
}
int MyDLL::add(int a, int b)
{
return a + b;
}
int mul(int a, int b)
{
return a * b;
}
short mul(char a, short b)
{
return a * b + 1; //+1是为了更容易看出区别
}
```
写好代码后编译并构建,出现如下窗口意味着已构建完成,这个窗口是用来执行动态链接库的,不用理他。
!(https://file.mculoop.com/images/2022/11/image-20221111153710469_1668152230.png)
在构建目录生成了动态链接库 myDLL.dll(若在 Linux 系统则会生成 .so 共享库文件)、动态 libmyDLL.a( MinGW 生成的不是 .lib ,作用是一样的,不明白为什后缀不直接用 .lib )。 .lib/.a 有动态与静态之分,虽然文件名相同,但内容与作用差别很大。静态 .lib/.a 将导出声明和实现都放包含在其中,编译时用到的部分会嵌入到宿主程序中,在编译后的程序运行时不再需要;而动态 .lib/.a 不包含实现部分,仅包含声明、动态库的加载和调用相关代码,用于编译后的链接过程,在编译后的程序运行时同样不再需要, 这些我们在前面已经说过了。
## 如何使用动态链接库?
### 隐式调用法
发布动态库的时候附上动态库的头文件,或者将两个头文件合并。在知道原理的情况下以什么样的形式来使用动态库就无所谓了。此外还要提供 .a(可以不用)和 .dll 文件。
将 mydll_global.h、mydll.h、myDLL.dll 文件拷贝到项目的根目录(或其他目录),在项目文件中配置库的路径:
```
LIBS += ./myDLL.dll # makefile 所在目录
```
将 .dll 放置于 makefile 文件所在的路径;
在项目中加入头文件 mydll_global.h、mydll.h ;
main.cpp 写入如下测试代码:
```cpp
#include "QCoreApplication"
#include "QDebug"
#include "mydll.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug() << MyDLL().add(1, 2);
qDebug() << mul((int)2, (int)3); //2 * 3
qDebug() << mul((char)2, (short)3); //2 * 3 + 1
return a.exec();
}
```
好了,大功告成!运行一下试试看:
!(https://file.mculoop.com/images/2022/11/image-20221111153905915_1668152346.png)
### 显式调用法
显示调用即动态调用,是么时候需要使用就在上面时候通过 API 去调用,用起来比较灵活,却相对麻烦一点。Qt 提供了一个 QLibrary 类,便于动态链接库的显式调用。
仅把 main.cpp 修改如下即可:
```cpp
#include "QCoreApplication"
#include "QDebug"
#include "QLibrary"
#include "mydll.h"
int main(int argc, char *argv[])
{
MyDLL *dllClass;
typedef int(*mul_t)(int, int);
typedef short(*mul2_t)(char, short);
QCoreApplication a(argc, argv);
QLibrary dll("myDLL.dll");
if(dll.load())
{
dllClass = (MyDLL *)dll.resolve("MyDLL");
mul_t mul = (mul_t)dll.resolve("mul");
mul2_t mul2 = (mul2_t)dll.resolve("_Z3mulcs");
qDebug() << dllClass->add(1, 2);
qDebug() << mul((int)2, (int)3); //2 * 3
qDebug() << mul2((char)2, (short)3); //2 * 3 + 1
}
return a.exec();
}
```
运行结果嘛,当然是一样了。
## 如何使用静态链接库?
静态链接库上面已有提到过,我们需配置项目文件,修改为 `CONFIG += staticlib`。静态库不能使用动态库的导出方式,需要把 mydll_global.h 删除,并修改 mydll.h ,去除所有 `MYDLLSHARED_EXPORT` 声明,重新编译和构建。构建后没有 .dll 文件生成了,拷贝生成的 libmyDLL.a 以及头文件到测试工程中。将测试工程的项目文件的库定义修改为:
```
LIBS += -L./ -libmyDLL.a #将 libmyDLL.a 置于 makefile 所在目录
#LIBS += ./libmyDLL.a #也可以用
```
编译并构建,同样的结果被输出。
至此,大致将 Qt 的动态库与静态库的用法总结完毕。至于怎么与其他 C/C++ 编译器、其他语言共享库,还需今后深入地学习和实践。
## 参考资料
1. 确定要使用的链接方法 https://msdn.microsoft.com/zh-cn/library/253b8k2c.aspx
2. Name mangling https://en.wikipedia.org/wiki/Name_mangling
页:
[1]