在C++使用动态库,(linux下是.so
,windows下是.dll
) 比较常见的方式是在编译时,直接连接到程序中。但是除了这种方式外,还可以使用的动态加载的方式去使用动态库。
两种方式的区别
在编译时把库连接到程序:这种方式是在编译的时候,就确定了要链接的库文件,然后通过编译参数在链接时直接把动态库的地址空间等等信息连接到程序中。程序在运行时,可以直接根据路径去寻找动态库,然后加载到程序中,然后运行,这种方式在日常开发中用的比较多。
在程序运行时动态加载库:这种方式是在程序运行时,通过调用系统函数,把动态库加载到程序中,然后执行动态库中的代码。这种方式和编译时链接的优势是可以在程序运行的过程中动态加载和卸载库。可以在不修改源程序的前提下,使用新的库。这种方式,比较常见的应用是程序的插件系统。还有一个就是服务器的热更可以用这个来实现。
在编译时使用动态库的例子传送门
动态加载库
不废话了,直接开始上代码
在程序运行的过程中动态加载库,需要依赖操作系统,所以在不同的系统上有不同的系统调用函数。
在linux
上需要用到 dlopen
函数加载库,dlclose
函数释放库,dlsym
函数 查找库函数
需要的头文件 #include <dlfcn.h>
在windows
上需要 LoadLibrary
宏加载库,FreeLibrary
宏释放库,GetProcAddress
函数查找库函数
需要的头文件 #include <windows.h>
基类功能
在C++中可以通过定义一个抽象类来作为所有库的基类,所有的库文件都实现这个基类,然后重写基类的纯虚函数。可以在加载到所有库后,都可以把库里的类作为抽象类的派生类。
先定义一个基类 base.h
1#ifndef DLOAD_BASE_H
2#define DLOAD_BASE_H
3
4/**
5 * 必须实现 moduleName_create 函数,来初始化对象
6 * extern "C" Base *module1_create() {
7 * return new Module;
8 * }
9 *
10 * //必须实现 moduleName_destroy 函数,来回收对象
11 * extern "C" void module1_destroy(Base *obj) {
12 * delete obj;
13 * }
14 */
15
16
17class Base {
18 public:
19 virtual std::string readLine(const std::string &) = 0;
20
21 virtual ~Base() = default;
22};
23
24#endif //DLOAD_BASE_H
这个基类的功能很简单,只有一个纯虚函数readLine
这个函数会传入一个字符串,然后返回一个字符串
注释中的哪两个函数,后面会有详细的介绍
实现一个模块
可以把一个库看做是一个模块,现在实现一个模块
1//简单的模块 例子
2//转大写
3
4#include <algorithm>
5#include <string>
6#include "../base.h"
7
8class Module1 : public Base {
9 std::string readLine(const std::string &str) override {
10 std::string str2(str);
11 std::transform(str.begin(), str.end(), str2.begin(), ::toupper);
12 return str2;
13 }
14};
15
16//必须实现 moduleName_create 函数,来初始化对象
17extern "C" Base *module1_create() {
18 return new Module1;
19}
20
21//必须实现 moduleName_destroy 函数,来回收对象
22extern "C" void module1_destroy(Base *obj) {
23 delete obj;
24}
这个功能非常简单,把传入的字符串转成大写,然后返回
- 为什么需要
Base *module1_create()
和void module1_destroy(Base *obj)
这两个函数
因为在把库加载完成后,需要使用库里的函数,但是不能直接查找C++的类,然后再初始化对象,只能在库里完成C++对象的初始化,然后返回对象的指针。
所以需要在库里有对应的函数来初始化对象和回收对象,所以就有了这两个函数。
- 为什么要
extern "C"
因为C++有函数重载的功能,所以编译器在编译代码的时候,会对函数重命名。但是对函数重命名的规则,没有统一的标准,不同编译器有不同的规则。像 module1_create
这个函数可能就被重命名成 _Z14module1_create
这样的字符串。这样后面使用 dlsym
或者 GetProcAddress
函数查找库里的函数时,就没法找到对应的函数了。所以使用extern "C"
让编译器使用C的规则来编译这段函数
至于这两个函数的名字 module1_create
和 module1_destroy
没有强制的要求,但是要有一定的规范。否则在加载到库后,没法根据函数名查找到对应的函数。这里用到的规则是 模块名_create
和 模块名_destroy
加载库
下面开始加载库,因为在同的系统下,加载库调用的函数不同,所以使用 宏来完成不用系统下的条件编译,最终完成加载库
1//声明创建对象的函数
2typedef Base *(*create)();
3
4//声明回收对象的函数
5typedef void (*destroy)(Base *);
6
7//调用系统函数,加载动态库
8
9#ifdef _WIN32
10
11HINSTANCE loadLib(Base **base, const char *path, const char *funName) {
12 auto handle = LoadLibrary(path);
13 if (!handle) {
14 return nullptr;
15 }
16 auto cr = (create) GetProcAddress(handle, funName);
17 if (cr) {
18 *base = cr();
19 }
20 return handle;
21}
22
23//调用系统函数,卸载动态库
24void freeLib(HINSTANCE handle, Base *obj, const char *funName) {
25 auto free = (destroy) GetProcAddress(handle, funName);
26 if (free) {
27 free(obj);
28 }
29 FreeLibrary(handle);
30}
31
32#else
33
34void *loadLib(Base **base, const char *path, const char *funName) {
35 auto handle = dlopen(path, RTLD_LAZY);
36 if (!handle) {
37 return nullptr;
38 }
39 auto cr = (create) dlsym(handle, funName);
40 if (cr) {
41 *base = cr();
42 }
43 return handle;
44}
45
46//调用系统函数,卸载动态库
47void freeLib(void *handle, Base *obj, const char *funName) {
48 auto free = (destroy) dlsym(handle, funName);
49 if (free) {
50 free(obj);
51 }
52 dlclose(handle);
53}
54
55#endif
在代码最开始的位置,通过 typedef
声明了两个函数的指针,在查找到函数后,把函数强转成对应的类型,才能在后面使用
使用库
1
2int main() {
3 std::string libPath;
4#ifdef _WIN32
5 libPath = std::string("./module/libmodule1" + ".dll");
6#else
7 libPath = std::string("./module/libmodule1" + ".so");
8#endif
9 Base *module = nullptr;
10 auto handle = loadLib(&module, libPath.c_str(), std::string("module1_create").c_str());
11
12 if (!module) {
13 std::cout << "load lib module1" << " fail" << std::endl;
14 return 1;
15 }
16 std::cout << module->readLine("abc") << std::endl;
17
18 return 0;
19}
现在基本就完成了一个动态库的动态加载过程。如果想要拓展,只要再按照这个规则,写一个新的模块然后加载上来就可以了。
最后放一个相对完整的动态加载的demo,github