進入主題前,先看看兩點預備知識。
一、顯式調用和隱式調用的區別
我們知道,動態庫相比靜態庫的區別是:靜態庫是編譯時就加載到可執行文件中的,而動態庫是在程序運行時完成加載的,所以使用動態庫的程序的體積要比使用靜態庫程序的體積小,並且使用動態庫的程序在運行時必須依賴所使用的動態庫文件(.so文件),而使用靜態庫的程序一旦編譯好,就不再需要依賴的靜態庫文件了(.a文件)。
動態庫的調用又分為顯示和隱式兩種方式,區別如下:
1、 隱式調用需要調用者寫的代碼量少,調用起來和使用當前項目下的函數一樣直接;而顯式調用則要求程序員在調用時,指明要加載的動態庫的名稱和要調用的函數名稱。
2、隱式調用由系統加載完成,對程序員透明;顯式調用由程序員在需要使用時自己加載,不再使用時,自己負責卸載。
3、由於顯式調用由程序員負責加載和卸載,好比動態申請內存空間,需要時就申請,不用時立即釋放,因此顯式調用對內存的使用更加合理, 大型項目中應使用顯示調用。
4、當動態鏈接庫中只提供函數接口,而該函數沒有封裝到類里面時,如果使用顯式調用的方式,調用方甚至不許要包含動態鏈接庫的頭文件(需要調用的函數名是通過dlsym函數的參數指明的),而使用隱式調用時,則調用方必須要加上動態庫中的頭文件,g++編譯時還需要要用參數-I指明包含的頭文件的位置。需要注意的是,當動態鏈接庫中的接口函數是作為成員函數封裝在類里面時,即使使用顯式調用的方式,調用方也必須包含動態庫中的相應頭文件(詳見五、顯示調用動態鏈接中的類成員函數)。
5、顯式調用更加靈活,可以模擬多態效果(具體見后文)。
6、顯式調用的方式,必須加入頭文件dlfcn.h,makefile中的鏈接命令中要加入參數-ldl,需要用dlopen加載庫,dlsym取函數符號(函數名應用新定義的),dlclose卸載庫。
7、隱式調用的方式,makefile中的鏈接命令中要加入參數-l加庫名,直接用庫里的函數名就可以。
二、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.o
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
make之后,將生成的libso1.so拷貝到test.cc所在目錄下。
so2.h:
extern "C" void fcn();
so2.cc:
#include <iostream>
#include "so2.h"
void fcn() {
std::cout << "this is fcn in so2" << std::endl;
}
so2的makefile:
libso2.so:so2.o
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
make之后,將生成的libso2.so拷貝到test.cc所在目錄下。
test.cc:
#include <iostream>
#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的makefile:
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,只需要包含動態鏈接庫中的頭文件,使用動態庫中的函數也不需要像顯示調用那么復雜。
五、顯式調用動態鏈接中的類成員函數
顯示調用動態鏈接庫的類成員函數,有單獨的寫法,但比較少用。推薦的寫法是為每個要被外部調用的類成員函數設計一個普通的借口函數,在接口函數內部使用類的成員函數。當然這就需要將類設計為單例模式,因為不可能在每個接口函數中都構造一個類的對象。
---------------------
作者:hujingLiu
來源:CSDN
原文:https://blog.csdn.net/lc_910927/article/details/42393121
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!