linux下動態連接庫(.so)的顯式調用和隱式調用

進入主題前,先看看兩點預備知識。ios

1、顯式調用和隱式調用的區別c++

        咱們知道,動態庫相比靜態庫的區別是:靜態庫是編譯時就加載到可執行文件中的,而動態庫是在程序運行時完成加載的,因此使用動態庫的程序的體積要比使用靜態庫程序的體積小,而且使用動態庫的程序在運行時必須依賴所使用的動態庫文件(.so文件),而使用靜態庫的程序一旦編譯好,就再也不須要依賴的靜態庫文件了(.a文件)。程序員

        動態庫的調用又分爲顯示和隱式兩種方式,區別以下:算法

        一、 隱式調用須要調用者寫的代碼量少,調用起來和使用當前項目下的函數同樣直接;而顯式調用則要求程序員在調用時,指明要加載的動態庫的名稱和要調用的函數名稱。函數

        二、隱式調用由系統加載完成,對程序員透明;顯式調用由程序員在須要使用時本身加載,再也不使用時,本身負責卸載。this

        三、因爲顯式調用由程序員負責加載和卸載,比如動態申請內存空間,須要時就申請,不用時當即釋放,所以顯式調用對內存的使用更加合理, 大型項目中應使用顯示調用。spa

        四、當動態連接庫中只提供函數接口,而該函數沒有封裝到類裏面時,若是使用顯式調用的方式,調用方甚至不準要包含動態連接庫的頭文件(須要調用的函數名是經過dlsym函數的參數指明的),而使用隱式調用時,則調用方必需要加上動態庫中的頭文件,g++編譯時還須要要用參數-I指明包含的頭文件的位置。須要注意的是,當動態連接庫中的接口函數是做爲成員函數封裝在類裏面時,即便使用顯式調用的方式,調用方也必須包含動態庫中的相應頭文件(詳見5、顯示調用動態連接中的類成員函數)。.net

        五、顯式調用更加靈活,能夠模擬多態效果(具體見後文)。插件

        六、顯式調用的方式,必須加入頭文件dlfcn.h,makefile中的連接命令中要加入參數-ldl,須要用dlopen加載庫,dlsym取函數符號(函數名應用新定義的),dlclose卸載庫。設計

        七、隱式調用的方式,makefile中的連接命令中要加入參數-l加庫名,直接用庫裏的函數名就能夠

 

2、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是調用兩個動態庫的程序。

 

3、顯式調用

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以後,析構函數會被調用。

 

4、隱式調用

隱式調用不須要包含頭文件dlfcn.h,只須要包含動態連接庫中的頭文件,使用動態庫中的函數也不須要像顯示調用那麼複雜。

 

5、顯式調用動態連接中的類成員函數

顯示調用動態連接庫的類成員函數,有單獨的寫法,但比較少用。推薦的寫法是爲每一個要被外部調用的類成員函數設計一個普通的藉口函數,在接口函數內部使用類的成員函數。固然這就須要將類設計爲單例模式,由於不可能在每一個接口函數中都構造一個類的對象。

相關文章
相關標籤/搜索