进入主题前,先看看两点预备知识。
一、显式调用和隐式调用的区别
我们知道,动态库相比静态库的区别是:静态库是编译时就加载到可执行文件中的,而动态库是在程序运行时完成加载的,所以使用动态库的程序的体积要比使用静态库程序的体积小,并且使用动态库的程序在运行时必须依赖所使用的动态库文件(.so文件),而使用静态库的程序一旦编译好,就不再需要依赖的静态库文件了(.a文件)。
动态库的调用又分为显示和隐式两种方式,区别如下:
1、 隐式调用需要调用者写的代码量少,调用起来和使用当前项目下的函数一样直接;而显式调用则要求程序员在调用时,指明要加载的动态库的名称和要调用的函数名称。
2、隐式调用由系统加载完成,对程序员透明;显式调用由程序员在需要使用时自己加载,不再使用时,自己负责卸载。
3、由于显式调用由程序员负责加载和卸载,好比动态申请内存空间,需要时就申请,不用时立即释放,因此显式调用对内存的使用更加合理, 大型项目中应使用显示调用。
4、当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用方甚至不许要包含动态链接库的头文件(需要调用的函数名是通过dlsym函数的参数指明的),而使用隐式调用时,则调用方不可避免要加上动态库中的头文件,g++编译时还需要要用参数-I指明包含的头文件的位置。需要注意的是,当动态链接库中的接口函数是作为成员函数封装在类里面时,即使使用显式调用的方式,调用方也必须包含动态库中的相应头文件(详见五、显示调用动态链接中的类成员函数)。
5、显式调用更加灵活,可以模拟多态效果(具体见后文)。
二、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++的。有了上面两个预备知识后,下面以实际例子来演示两种不同的动态库调用方式。例子的结构组织为如下:
so1.h和so1.cc是第一个动态库中的文件,会编译链接为libso1.so;so2.h和so2.cc是第一个动态库中的文件,会编译链接为libso2.so;test.cc是调用两个动态库的程序。
三、显式调用
so1.h:
extern "C" void fcn();so1.cc:
#include <iostream>
#include "so1.h"
void fcn() {
std::cout << "this is fcn in so1" << std::endl;
}
so1的makefile:
libso1.so:so1.omake之后,将生成的libso1.so拷贝到test.cc所在目录下。
g++ so1.o -shared -o libso1.so
so1.o:so1.cc so1.h
g++ -c so1.cc -fPIC -o so1.o
.PHONY:clean
clean:
rm so1.o libso1.so
so2.h:
extern "C" void fcn();so2.cc:
#include <iostream>so2的makefile:
#include "so2.h"
void fcn() {
std::cout << "this is fcn in so2" << std::endl;
}
libso2.so:so2.omake之后,将生成的libso2.so拷贝到test.cc所在目录下。
g++ so2.o -shared -o libso2.so
so2.o:so2.cc so2.h
g++ -c so2.cc -fPIC -o so2.o
.PHONY:clean
clean:
rm so2.o libso2.so
test.cc:
#include <iostream>test的makefile:
#include <cstdlib>
#include <dlfcn.h>
using namespace std;
int main(int argc, char **argv) {
if(argc != 2) {
cout << "argument error!" << endl;
exit(1);
}
//pointer to function
typedef void (*pf_t)();
char *err = NULL;
//open the lib
void *handle = dlopen(argv[1], RTLD_NOW);
if(!handle) {
cout << "load " << argv[1] << "failed! " << dlerror() << endl;
exit(1);
}
//clear error info
dlerror();
pf_t pf = (pf_t)dlsym(handle, "fcn");
err = dlerror();
if(err) {
cout << "can't find symbol fcn! " << err << endl;
exit(1);
}
//call function by pointer
pf();
dlclose(handle);
return 0;
}
test:test.o
g++ test.o -lso1 -L. -lso2 -L. -ldl -Wl,-rpath=. -o test
test.o:test.cc
g++ -c test.cc -o test.o
make之后,终端运行结果如下:
可以看到这里,通过输入不同的参数,调用了不同的共享库中的fcn函数,是一种多态的表现,许多软件的不同插件就是这样实现的。
需要注意的是,要使用显式调用的方式,必须加入头文件dlfcn.h,makefile中的链接命令中要加入参数-ldl,否则报错。
dlfcn.h中提供的API说明如下:
1) dlopen
函数原型:void *dlopen(const char *libname,int flag);
功能描述:dlopen必须在dlerror,dlsym和dlclose之前调用,表示要将库装载到内存,准备使用。如果要装载的库依赖于其它库,必须首先装载依赖库。如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。
参数中的libname一般是库的全路径,这样dlopen会直接装载该文件;如果只是指定了库名称,在dlopen会按照下面的机制去搜寻:
a.根据环境变量LD_LIBRARY_PATH查找
b.根据/etc/ld.so.cache查找
c.查找依次在/lib和/usr/lib目录查找。
flag参数表示处理未定义函数的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数,先把库装载到内存,等用到没定义的函数再说;RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。
2) dlerror
函数原型:char *dlerror(void);
功能描述:dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。dlerror在返回错误信息的同时,也会清除错误信息。
3) dlsym
函数原型:void *dlsym(void *handle,const char *symbol);
功能描述:在dlopen之后,库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数,则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数,
4) dlclose
函数原型:int dlclose(void *);
功能描述:将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。如果存在析构函数,则在dlclose之后,析构函数会被调用。
四、隐式调用
隐式调用不需要包含头文件dlfcn.h,只需要包含动态链接库中的头文件,使用动态库中的函数也不需要像显示调用那么复杂。
五、显式调用动态链接中的类成员函数
显示调用动态链接库的类成员函数,有单独的写法,但比较少用。推荐的写法是为每个要被外部调用的类成员函数设计一个普通的借口函数,在接口函数内部使用类的成员函数。当然这就需要将类设计为单例模式,因为不可能在每个接口函数中都构造一个类的对象。