C++ DLL導出類 知識大全 編寫DLL所學所思(1)——導出函數

在公司使用C++ 做開發,公司的大拿搭了一個C++的跨平臺開發框架。在C++開發領域我還是個新手,有很多知識要學,比如Dll庫的開發。

   參考了很多這方面的資料,對DLL有一個基本全面的瞭解。有一個問題讓我有點困惑,普通的導入導出C++類的方式都是使用_declspec(dllexport) /_declspec(dllimport)來導入導出類,但是在公司的開發中我們沒有導入導出,而是定義了一些只有純虛函數的抽象類,然後定義了一個工廠類,將這個工廠類註冊到框架的服務中心中,使用時從服務中心拿到這個工廠類,就可以創建Dll中的其它類。對這種使用方式我不太理解,google+百度搜索了很多這方面的內容,很多blog講到了這種使用方式,但是也沒有講清楚這樣使用的原理,後來找到了一篇老外寫的blog,講得比較清楚。

   使用只有純虛函數的抽象類之所以不需要導出,是因爲純虛函數的虛表使然。下面是同老外的bkig中抽出來的一個示例。

   純虛函數類的定義如下:

[cpp] view plain copy
 
  1. // The abstract interface for Xyz object.  
  2. // No extra specifiers required.  
  3. struct IXyz  
  4. {  
  5.     virtual int Foo(int n) = 0;  
  6.     virtual void Release() = 0;  
  7. };  
  8.   
  9. // Factory function that creates instances of the Xyz object.  
  10. extern "C" XYZAPI IXyz* APIENTRY GetXyz();  

 

   使用dll的代碼如下:

 

[cpp] view plain copy
 
  1. #include "XyzLibrary.h"  
  2.   
  3. ...  
  4. IXyz* pXyz = ::GetXyz();  
  5.   
  6. if(pXyz)  
  7. {  
  8.     pXyz->Foo(42);  
  9.   
  10.     pXyz->Release();  
  11.     pXyz = NULL;  
  12. }  

該示例中導出了一個方法來創建IXyz對象,但是並沒有導出IXyz對象,IXyz類中只有純虛函數。這是如何實現的呢?我所知道的是,需要將Dll中的類導出,導出的符號將放到導出符號表中,在鏈接的時候根據這些符號來定位函數的地址,這個IXyz類沒有聲明導出,當然類中的函數就不回生成在導出符號表中,那麼怎麼定位到函數的地址呢?下面這張原文中的圖給出了很清晰的解釋:

 

圖中的僞代碼部分解釋了函數的調用過程,是通過虛表來定位函數的。因爲定義的是隻有純虛函數的抽象類,這樣的類編譯之後會有一個純粹的虛表,可以通過這張純粹的虛表來進行函數調用,所以通過這種方式來使用dll的第一步是應以只帶純虛函數的抽象類,或者說接口。
更多內容,參見下面:

 

 

一、導出類的簡單方式

這種方式是比較簡單的,同時也是不建議採用的不合適方式。

只需要在導出類加上__declspec(dllexport),就可以實現導出類。對象空間還是在使用者的模塊裏,dll只提供類中的函數代碼。不足的地方是:使用者需要知道整個類的實現,包括基類、類中成員對象,也就是說所有跟導出類相關的東西,使用者都要知道。通過Dependency Walker可以看到,這時候的dll導出的是跟類相關的函數:如構造函數、賦值操作符、析構函數、其它函數,這些都是使用者可能會用到的函數。

這種導出類的方式,除了導出的東西太多、使用者對類的實現依賴太多之外,還有其它問題:必須保證使用同一種編譯器。導出類的本質是導出類裏的函數,因爲語法上直接導出了類,沒有對函數的調用方式、重命名進行設置,導致了產生的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部分。

二、導出類的較好方式

這種方式是比較合適的,跟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部分。

三、總結

導出類是比較簡單的,比較容易混淆的概念上一篇總結已經說完了。本質上來說,跟導出函數沒差別。使用VS2005自動生成的代碼可以省去很多力氣,比起以前做練習什麼都是自己動手寫方便多了。要注意一下工程的設置,熟悉它們的作用可以加快編程速度。

Demo代碼附件:

http://files.cnblogs.com/cswuyg/%E7%BC%96%E5%86%99DLL%E6%89%80%E5%AD%A6%E6%89%80%E6%80%9D(2)(%E5%AF%BC%E5%87%BA%E7%B1%BB).rar

參考資料:

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的編寫

編寫dll時,有個重要的問題需要解決,那就是函數重命名——Name-Mangling。解決方式有兩種,一種是直接在代碼裏解決採用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一種是採用def文件。

(1)編寫dll時,爲什麼有 extern “C”

原因:因爲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中的函數名被修飾了,就需要重命名。二、重命名的方式有兩種,要麼使用*.def文件,在文件外修正,要麼使用#pragma,在代碼裏給函數別名。

(2)_declspec(dllexport)和_declspec(dllimport)的作用

       _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頁)

(3)__stdcall帶來的影響

       這是一種函數的調用方式。默認情況下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")

 

(4)*.def文件的用途

指定導出函數,並告知編譯器不要以修飾後的函數名作爲導出函數名,而以指定的函數名導出函數(比如有函數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文件對函數進行命名很有用,很重要。

(5)、DllMain函數

每一個動態鏈接庫都會有一個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;

}

(6)、很多都還沒學,如:導出Class、導出變量、DLL更高級的應用。目前先了解點基礎知識。以後補上。

 

 

 

 

 

 

 

 

 

 

 2011-8-14補充

 

編寫dll可以使用.def文件對導出的函數名進行命名。

 

1、動態裝入dll,重命名(*.def)的必要性?

因爲導出的函數儘可能使用__stdcall的調用方式。而__stdcall的調用方式,無論是C的Name Mangling,還是C++的Name Mangling都會對函數名進行修飾。所以,採用__stdcall調用方式之後,必須使用*.def文件對函數名重命名,不然就不能使用GetProcAddress()通過函數名獲取函數指針。

 

2、隱式調用時,頭文件要注意的地方?

因爲使用靜態裝入,需要有頭文件聲明這個要被使用的dll中的函數,如果聲明中指定了__stdcall或者extern “C”,那麼在調用這個函數的時候,編譯器就通過Name Mangling之後的函數名去.lib中找這個函數,*.def中的內容是對*.lib裏函數的名稱不產生作用,*.def文件裏的函數重命名只對dll有用。這就有lib 跟dll裏函數名不一致的問題了,但並不會產生影響,DLL的製造者跟使用者採用的是一致函數聲明。

 

3、所以到底要不要使用__stdcall 呢?

我看到一些代碼裏是沒有使用__stdcall的。如果不使用__stdcall,而使用默認的調用約定_cdecl,並且有extern ”C”。那麼VC是不會任何修飾的。這樣子生成的dll裏的函數名就是原來的函數名。也就可以不使用.def文件了。

也有一些要求必須使用__stdcall,例如com相關的東西、系統的回調函數。具體看有沒有需要。

 

 

4、導出函數別名怎麼寫?

可以在.def文件裏對函數名寫一個別名。

例如:

EXPORTS

cswuygTest(別名) = _showfun@4(要導出的函數)

 

或者:

#pragma comment(linker, "/export:[別名] =[NameMangling後的名稱]")

 

這樣做就可以隨便修改別名了,不會出現找不到符號的錯誤。

 

5、用不用*.def文件?

如果採用VC默認的調用約定,可以不用*.def文件,如果要採用__stdcall調用約定,又不想函數名被修飾,那就採用*.def文件吧,另一種在代碼裏寫的重命名的方式不夠方便。

6、什麼情況下(不)需要考慮函數重命名的問題?

1)、隱式調用(通過lib)

如果dll的製造者跟dll的使用者採用同樣的語言、同樣編程環境,那麼就不需要考慮函數重命名。使用者在調用函數的時候,通過Name Mangling後的函數名能在lib裏找到該函數。

如果dll的製造者跟dll使用不同的語言、或者不同的編譯器,那就需要考慮重命名了。

2)、顯示調用(通過GetProcessAddress)

       這絕對是必須考慮函數重命名的。

7、總結

    總的來說,在編寫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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章