在公司使用C++ 作開發,公司的大拿搭了一個C++的跨平臺開發框架。在C++開發領域我仍是個新手,有不少知識要學,好比Dll庫的開發。html
參考了不少這方面的資料,對DLL有一個基本全面的瞭解。有一個問題讓我有點困惑,普通的導入導出C++類的方式都是使用_declspec(dllexport) /_declspec(dllimport)來導入導出類,可是在公司的開發中咱們沒有導入導出,而是定義了一些只有純虛函數的抽象類,而後定義了一個工廠類,將這個工廠類註冊到框架的服務中心中,使用時從服務中心拿到這個工廠類,就能夠建立Dll中的其它類。對這種使用方式我不太理解,google+百度搜索了不少這方面的內容,不少blog講到了這種使用方式,可是也沒有講清楚這樣使用的原理,後來找到了一篇老外寫的blog,講得比較清楚。編程
使用只有純虛函數的抽象類之因此不須要導出,是由於純虛函數的虛表使然。下面是同老外的bkig中抽出來的一個示例。框架
純虛函數類的定義以下:函數
使用dll的代碼以下:post
該示例中導出了一個方法來建立IXyz對象,可是並無導出IXyz對象,IXyz類中只有純虛函數。這是如何實現的呢?我所知道的是,須要將Dll中的類導出,導出的符號將放到導出符號表中,在連接的時候根據這些符號來定位函數的地址,這個IXyz類沒有聲明導出,固然類中的函數就不回生成在導出符號表中,那麼怎麼定位到函數的地址呢?下面這張原文中的圖給出了很清晰的解釋:學習
圖中的僞代碼部分解釋了函數的調用過程,是經過虛表來定位函數的。由於定義的是隻有純虛函數的抽象類,這樣的類編譯以後會有一個純粹的虛表,能夠經過這張純粹的虛表來進行函數調用,因此經過這種方式來使用dll的第一步是應以只帶純虛函數的抽象類,或者說接口。
更多內容,參見下面:ui
1、導出類的簡單方式google
這種方式是比較簡單的,同時也是不建議採用的不合適方式。加密
只須要在導出類加上__declspec(dllexport),就能夠實現導出類。對象空間仍是在使用者的模塊裏,dll只提供類中的函數代碼。不足的地方是:使用者須要知道整個類的實現,包括基類、類中成員對象,也就是說全部跟導出類相關的東西,使用者都要知道。經過Dependency Walker能夠看到,這時候的dll導出的是跟類相關的函數:如構造函數、賦值操做符、析構函數、其它函數,這些都是使用者可能會用到的函數。url
這種導出類的方式,除了導出的東西太多、使用者對類的實現依賴太多以外,還有其它問題:必須保證使用同一種編譯器。導出類的本質是導出類裏的函數,由於語法上直接導出了類,沒有對函數的調用方式、重命名進行設置,致使了產生的dll並不通用。
部分代碼(DLL頭文件):
//2011.10.6//cswuyg//dll導出類,比較差勁的方法#pragma once#ifdef NAIVEAPPROACH_EXPORTS#define NAIVEAPPROACH_API __declspec(dllexport)#else#define NAIVEAPPROACH_API __declspec(dllimport)#endif//基類也必須導出,不然警告:class NAIVEAPPROACH_API CBase{public: void Test();private: int m_j;};//也必須導出class NAIVEAPPROACH_API CDate{public: void Test2();private: int m_k;};class NAIVEAPPROACH_API CNaiveApproach : public CBase{public: CNaiveApproach(int i = 0); // TODO: add your methods here. void Func();private: int m_iwuyg; CDate m_dobj;};
Demo代碼見附件NaiveApproach部分。
2、導出類的較好方式
這種方式是比較合適的,跟com相似。
結構是這樣的:導出類是一個派生類,派生自一個抽象類——都是純虛函數。使用者須要知道這個抽象類的結構。DLL最少只須要提供一個用於獲取類對象指針的接口。使用者跟DLL提供者共用一個抽象類的頭文件,使用者依賴於DLL的東西不多,只須要知道抽象類的接口,以及獲取對象指針的導出函數,對象內存空間的申請是在DLL模塊中作的,釋放也在DLL模塊中完成,最後記得要調用釋放對象的函數。
這種方式比較好,通用,產生的DLL沒有特定環境限制。藉助了C++類的虛函數。通常都是採用這種方式。除了對DLL導出類有好處外,採用接口跟實現分離,可使得工程的結構更清晰,使用者只須要知道接口,而不須要知道實現。
部分代碼:
(1)DLL頭文件:
//2011.10.6//cswuyg//dll導出類//dll跟其使用者共用的頭文件#pragma once#ifdef MATUREAPPROACH_EXPORTS#define MATUREAPPROACH_API __declspec(dllexport)#else#define MATUREAPPROACH_API __declspec(dllimport)#endifclass IExport{public: virtual void Hi() = 0; virtual void Test() = 0; virtual void Release() = 0;};extern "C" MATUREAPPROACH_API IExport* _stdcall CreateExportObj();extern "C" MATUREAPPROACH_API void _stdcall DestroyExportObj(IExport* pExport);
(2)導出類頭文件:
//2011.10.6//cswuyg//dll導出類// 實現類#pragma once#include "MatureApproach.h"class ExportImpl : public IExport{public: virtual void Hi(); virtual void Test(); virtual void Release(); ~ExportImpl();private:};
Demo代碼見附件MatureApproach部分。
3、總結
導出類是比較簡單的,比較容易混淆的概念上一篇總結已經說完了。本質上來講,跟導出函數沒差異。使用VS2005自動生成的代碼能夠省去不少力氣,比起之前作練習什麼都是本身動手寫方便多了。要注意一下工程的設置,熟悉它們的做用能夠加快編程速度。
Demo代碼附件:
參考資料:
http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx
2010.8.31~2010.9.1總結
2011.9.28~30整理
燭秋
(1) 顯式調用:使用LoadLibrary載入動態連接庫、使用GetProcAddress獲取某函數地址。
(2) 隱式調用:可使用#pragma comment(lib, 「XX.lib」)的方式,也能夠直接將XX.lib加入到工程中。
編寫dll時,有個重要的問題須要解決,那就是函數重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼裏解決採用extent」c」、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另外一種是採用def文件。
緣由:由於C和C++的重命名規則是不同的。這種重命名稱爲「Name-Mangling」(名字修飾或名字改編、標識符重命名,有些人翻譯爲「名字粉碎法」,這翻譯顯得有些莫名其妙)
聽說,C++標準並無規定Name-Mangling的方案,因此不一樣編譯器使用的是不一樣的,例如:Borland C++跟Mircrosoft C++就不一樣,並且可能不一樣版本的編譯器他們的Name-Mangling規則也是不一樣的。這樣的話,不一樣編譯器編譯出來的目標文件.obj 是不通用的,由於同一個函數,使用不一樣的Name-Mangling在obj文件中就會有不一樣的名字。若是DLL裏的函數重命名規則跟DLL的使用者採用的重命名規則不一致,那就會找不到這個函數。
C標準規定了C語言Name-Mangling的規範(林銳的書有這樣說過)。這樣就使得,任何一個支持c語言的編譯器,它編譯出來的obj文件能夠共享,連接成可執行文件。這是一種標準,若是DLL跟其使用者都採用這種約定,那麼就能夠解決函數重命名規則不一致致使的錯誤。
影響符號名的除了C++和C的區別、編譯器的區別以外,還要考慮調用約定致使的Name Mangling。如extern 「c」 __stdcall的調用方式就會在原來函數名上加上寫表示參數的符號,而extern 「c」 __cdecl則不會附加額外的符號。
dll中的函數在被調用時是以函數名或函數編號的方式被索引的。這就意味着採用某編譯器的C++的Name-Mangling方式產生的dll文件可能不通用。由於它們的函數名重命名方式不一樣。爲了使得dll能夠通用些,不少時候都要使用C的Name-Mangling方式,便是對每個導出函數聲明爲extern 「C」,並且採用_stdcall調用約定,接着還須要對導出函數進行重命名,以便導出不加修飾的函數名。
注意到extern 「C」的做用是爲了解決函數符號名的問題,這對於動態連接庫的製造者和動態連接庫的使用者都須要遵照的規則。
動態連接庫的顯式裝入就是經過GetProcAddress函數,依據動態連接庫句柄和函數名,獲取函數地址。由於GetProcAddress僅是操做系統相關,可能會操做各類各樣的編譯器產生的dll,它的參數裏的函數名是原本來本的函數名,沒有任何修飾,因此通常狀況下須要確保dll’裏的函數名是原始的函數名。分兩步:一,若是導出函數使用了extern」C」 _cdecl,那麼就不須要再重命名了,這個時候dll裏的名字就是原始名字;若是使用了extern」C」 _stdcall,這時候dll中的函數名被修飾了,就須要重命名。2、重命名的方式有兩種,要麼使用*.def文件,在文件外修正,要麼使用#pragma,在代碼裏給函數別名。
_declspec還有另外的用途,這裏只討論跟dll相關的使用。正如括號裏的關鍵字同樣,導出和導入。_declspec(dllexport)用在dll上,用於說明這是導出的函數。而_declspec(dllimport)用在調用dll的程序中,用於說明這是從dll中導入的函數。
由於dll中必須說明函數要用於導出,因此_declspec(dllexport)頗有必要。可是能夠換一種方式,可使用def文件來講明哪些函數用於導出,同時def文件裏邊還有函數的編號。
而使用_declspec(dllimport)卻不是必須的,可是建議這麼作。由於若是不用_declspec(dllimport)來講明該函數是從dll導入的,那麼編譯器就不知道這個函數到底在哪裏,生成的exe裏會有一個call XX的指令,這個XX是一個常數地址,XX地址處是一個jmp dword ptr[XXXX]的指令,跳轉到該函數的函數體處,顯然這樣就平白無故多了一次中間的跳轉。若是使用了_declspec(dllimport)來講明,那麼就直接產生call dword ptr[XXX],這樣就不會有多餘的跳轉了。(參考《加密與解密》第三版279頁)
這是一種函數的調用方式。默認狀況下VC使用的是__cdecl的函數調用方式,若是產生的dll只會給C/C++程序使用,那麼就不必定義爲__stdcall調用方式,若是要給Win32彙編使用(或者其餘的__stdcall調用方式的程序),那麼就可使用__stdcall。這個可能不是很重要,由於能夠本身在調用函數的時候設置函數調用的規則。像VC就能夠設置函數的調用方式,因此能夠方便的使用win32彙編產生的dll。不過__stdcall這調用約定會Name-Mangling,因此我以爲用VC默認的調用約定簡便些。可是,若是既要__stdcall調用約定,又要函數名不給修飾,那可使用*.def文件,或者在代碼裏#pragma的方式給函數提供別名(這種方式須要知道修飾後的函數名是什麼)。
舉例:
·extern 「C」 __declspec(dllexport) bool __stdcall cswuyg();
·extern 「C」__declspec(dllimport) bool __stdcall cswuyg();
·#pragma comment(linker, "/export:cswuyg=_cswuyg@0")
指定導出函數,並告知編譯器不要以修飾後的函數名做爲導出函數名,而以指定的函數名導出函數(好比有函數func,讓編譯器處理後函數名仍爲func)。這樣,就能夠避免因爲microsoft VC++編譯器的獨特處理方式而引發的連接錯誤。
也就是說,使用了def文件,那就不須要extern 「C」了,也能夠不須要__declspec(dllexport)了(不過,dll的製造者除了提供dll以外,還要提供頭文件,須要在頭文件里加上這extern」C」和調用約定,由於使用者須要跟製造者遵照一樣的規則,除非使用者和製造者使用的是一樣的編譯器並對調用約定無特殊要求)。
舉例def文件格式:
LIBRARY XX(dll名稱這個並非必須的,但必須確保跟生成的dll名稱同樣)
EXPORTS
[函數名] @ [函數序號]
編寫好以後加入到VC的項目中,就能夠了。
另外,要注意的是,若是要使用__stdcall,那麼就必須在代碼裏使用上__stdcall,由於*.def文件只負責修改函數名稱,不負責調用約定。
也就是說,def文件只管函數名,無論函數平衡堆棧的方式。
若是把*.def文件加入到工程以後,連接的時候並無自動把它加進去。那麼能夠這樣作:
手動的在link添加:
1)工程的propertiesàConfiguration PropertiesàLinkeràCommand Lineà在「Additional options」里加上:/def:[完整文件名].def
2)工程的propertiesàConfiguration PropertiesàLinkeràInputàModule Definition File里加上[完整文件名].def
注意到:即使是使用C的名稱修飾方式,最終產生的函數名稱也多是會被修飾的。例如,在VC下,_stdcall的調用方式,就會對函數名稱進行修飾,前面加‘_’,後面加上參數相關的其餘東西。因此使用*.def文件對函數進行命名頗有用,很重要。
每個動態連接庫都會有一個DllMain函數。若是在編程的時候沒有定義DllMain函數,那麼編譯器會給你加上去。
DllMain函數格式:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("\nprocess attach of dll");
break;
case DLL_THREAD_ATTACH:
printf("\nthread attach of dll");
break;
case DLL_THREAD_DETACH:
printf("\nthread detach of dll");
break;
case DLL_PROCESS_DETACH:
printf("\nprocess detach of dll");
break;
}
return TRUE;
}
編寫dll可使用.def文件對導出的函數名進行命名。
由於導出的函數儘量使用__stdcall的調用方式。而__stdcall的調用方式,不管是C的Name Mangling,仍是C++的Name Mangling都會對函數名進行修飾。因此,採用__stdcall調用方式以後,必須使用*.def文件對函數名重命名,否則就不能使用GetProcAddress()經過函數名獲取函數指針。
由於使用靜態裝入,須要有頭文件聲明這個要被使用的dll中的函數,若是聲明中指定了__stdcall或者extern 「C」,那麼在調用這個函數的時候,編譯器就經過Name Mangling以後的函數名去.lib中找這個函數,*.def中的內容是對*.lib裏函數的名稱不產生做用,*.def文件裏的函數重命名只對dll有用。這就有lib 跟dll裏函數名不一致的問題了,但並不會產生影響,DLL的製造者跟使用者採用的是一致函數聲明。
我看到一些代碼裏是沒有使用__stdcall的。若是不使用__stdcall,而使用默認的調用約定_cdecl,而且有extern 」C」。那麼VC是不會任何修飾的。這樣子生成的dll裏的函數名就是原來的函數名。也就能夠不使用.def文件了。
也有一些要求必須使用__stdcall,例如com相關的東西、系統的回調函數。具體看有沒有須要。
能夠在.def文件裏對函數名寫一個別名。
例如:
EXPORTS
cswuygTest(別名) = _showfun@4(要導出的函數)
或者:
#pragma comment(linker, "/export:[別名] =[NameMangling後的名稱]")
這樣作就能夠隨便修改別名了,不會出現找不到符號的錯誤。
若是採用VC默認的調用約定,能夠不用*.def文件,若是要採用__stdcall調用約定,又不想函數名被修飾,那就採用*.def文件吧,另外一種在代碼裏寫的重命名的方式不夠方便。
1)、隱式調用(經過lib)
若是dll的製造者跟dll的使用者採用一樣的語言、一樣編程環境,那麼就不須要考慮函數重命名。使用者在調用函數的時候,經過Name Mangling後的函數名能在lib裏找到該函數。
若是dll的製造者跟dll使用不一樣的語言、或者不一樣的編譯器,那就須要考慮重命名了。
2)、顯示調用(經過GetProcessAddress)
這絕對是必須考慮函數重命名的。
總的來講,在編寫DLL的時候,寫個頭文件,頭文件裏聲明函數的NameMingling方式、調用約定(主要是爲了隱式調用)。再寫個*.def文件把函數重命名了(主要是爲了顯式調用)。提供*.DLL\*.lib\*.h給dll的使用者,這樣不管是隱式的調用,仍是顯式的調用,均可以方便的進行。
附:
一個簡單DLL導出函數的例子:http://files.cnblogs.com/cswuyg/%E7%BC%96%E5%86%99DLL%E6%89%80%E5%AD%A6%E6%89%80%E6%80%9D.rar
http://www.cnblogs.com/dongzhiquan/archive/2009/08/04/1994764.html
http://topic.csdn.net/u/20081126/14/70ac75b3-6e79-4c48-b9fe-918dce147484.html