Loading shared library APIs dynamically

Example of loading shared library APIs dynamically.

显式调用和隐式调用

CMake Tutorial 构建自己的库中简单介绍了动态链接库和静态链接库,动态链接库的显式调用和隐式调用也与两者的区别类似:

  • 隐式调用在编译时指定需要链接的库,在程序中通过包含头文件的方式调用对应接口,在编译后运行二进制文件时需要做好动态库的链接,库的加载由系统完成。

  • 显式调用直到程序运行调用时才加载需要的库(.so的存储绝对路径)和API(函数名),由程序(员)负责库的加载和卸载,对内存的使用更加合理,简单的接口函数不需要包含头文件,只有对于类成员函数的调用由于使用特殊的实现方式才需要包含头文件。

extern "C"

C++程序(或库、目标文件)中,所有非静态(non-static)函数在二进制文件中都是以“符号(symbol)”形式出现的。这些符号都是唯一的字符串,从而把各个函数在程序、库、目标文件中区分开来。而在C中,符号名正是函数名,两者完全一样。而C++允许重载(不同的函数有相同的名字但不同的参数,甚至const重载),并且有很多C所没有的特性──比如类、成员函数、异常说明──几乎不可能直接用函数名作符号名。为了解决这个问题,C++采用了所谓的name mangling。它把函数名和一些信息(如参数数量和大小)杂糅在一起,改造成奇形怪状,只有编译器才懂的符号名。例如,被mangle后的foo可能看起来像foo@4%6^,或者,符号名里头甚至不包括“foo”。

其中一个问题是,C++标准并没有定义名字必须如何被mangle,所以每个编译器都按自己的方式来进行name mangling。有些编译器甚至在不同版本间更换mangling算法(尤其是g++ 2.x和3.x)。前文说过,在显示调用动态库中的函数时,需要指明调用的函数名,即使您搞清楚了您的编译器到底怎么进行mangling的,从而知道调用的函数名被C++编译器转换为了什么形式,,但可能仅仅限于您手头的这个编译器而已,而无法在下一版编译器下工作。

extern "C"即可以解决这个问题。用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。所以extern "C" 只是告诉编译器编和链接的时候都用c的方式的函数名字,函数里的内容可以为c的代码也可以为c++的。

dlfcn.h

// 将库加载到内存中,如果加载的库存在依赖库,则需先加载依赖库;
// 如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。
// libname: .so文件的绝对路径
// flag:处理.so中未定义函数的方式
//     - RTLD_LAZY:等待dlsym的调用再将指定的函数指针加载到内存中
//     - RTLD_NOW:将库中的函数指针立即加载到内存中
void *dlopen(const char *libname,int flag);

// dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。
// dlerror在返回错误信息的同时,也会清除错误信息。
char *dlerror(void);

// dlsym可以获得指定函数(symbol)在内存中的位置(指针)
void *dlsym(void *handle,const char *symbol);

// 将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。
int dlclose(void *);

显示调用动态链接库(.so)的类成员函数

为每个将被外部调用的类成员函数设计一个普通的接口函数,在接口函数的内部使用类的成员函数,为了去除单例模式的限制,将类对象作为接口函数的其中一个参数,调用类成员函数时通过类对象去调用。所有的extern "c"接口函数放在一个头文件中,外部程序通过头文件去调用。

Sample

dlfcn-sample展示了如何使用dlfcn.h提供的API来显示调用指定的动态链接库。

测试程序的核心是DLLoader的实现,这是一个base,dlfcn-sample测试程序依赖它去加载指定的动态链接库中的API,其中GetAPI使用了锁保护,并且使用map对象存储已加载的函数指针,确保不重复加载。

Interface.h是为了实现调用类成员函数设计的调用接口,对应每个类应该有一个自己的调用接口实现Interface.cpp,注意每个调用接口都需要带类对象参数,通过对象调用可以在一定程度上确保线程安全。

Summary

显式调用通常在大型项目中用的比较多,其中的一个应用场景是假设某个程序中的多个子模块可以使用同一套抽象接口封装,而且各个子模块互相之间没有耦合,在运行阶段会需要动态去加载和卸载相关模块。我们就可以将这些子模块封装成SDK并通过显式调用管理,其中这套抽象接口就是一个Interface.h,每个子模块对应一个调用实现Interface.cpp,而加载和卸载管理上只需要使用一个Manager类即可。

例如,某个算法应用程序中内置了多种算法SDK,用户可以通过配置文件指定当前需要运行的算法,那么这个程序框架应该如下图所示:

Last updated