MFC DLL 導出函數的定義方式


    一直在鼓搗DLL,每天的工作都是調試一個一個的DLL,往DLL里面添加自己的代碼,但是對於DLL一直不太了解啊!今天一查資料,才發現自己對於DLL編寫的一些基本知識也不了解。要學習,這篇文章先總結DLL的導出函數的方法。

        1. 首先說一下如何建立一個普通的DLL工程!(以VS2008為例)

        New Project  -->  Win32 標簽 --> 填寫工程名稱 -->  點 OK,進入創建 Widzard  -->  Next 進入第二步 -->  Application Type 中選擇選擇Dll 單選按鈕。--> 在Additional Options中可以根據自己需求,確定是生成空工程,是否需要導出符號。

        都不勾選,后期完全自己添加,看到的是一個DLL框架被創建,只有一個DLL函數,在DLL被加載時,由線程調用該函數,做初始化。如下圖所示:

        

        很簡單,不多說了!

        勾選不同的額外選項的情況,不再說明,自己可以嘗試創建一下,看有什么區別。

        2. 導出模塊中符號的三種方法

        在說三種導出方式之前,先說一點關於VS編譯器的東西。VS編譯器,默認創建出來的文件為 .cpp,即它默認的編譯方式是C++。因此一般情況下,寫函數時隨時聲明函數,隨時使用,這是C++編譯方式的好處,也是很多書上所提倡的變量使用方式,到使用時再定義,聲明。而如果是 .c 文件,會被VS按照C的方式進行編譯。會有什么區別呢?就是函數,變量名字的命名區別。編譯器編譯源文件后,會將源文件中用到的符號進行修改,C的方式是在符號(Symbols,函數名稱,變量名稱等)的前面加下划線,對於函數名稱,后面會跟着@Num,Num為傳參所占用的大小。而C++為了實現函數重載,對函數實行了變名機制(具體機制不再詳解釋了,可以搜索一下),比如函數名稱,在變名中會體現函數源代碼中名稱,函數參數個數,參數類型,返回值等信息。因此用C方式編譯的DLL庫是無法使用C++方式編譯的程序直接連接調用的;同理反過來亦如此。在編程中就有了extern "C" 的出現。如果C++項目中,要調用以C編譯方式編譯出來的DLL庫,則需要將要導入的函數用extern "C" 的方式聲明,以便這些函數在C++項目中被編譯為C編譯方式的函數名稱,在C的lib庫中可以找到相應的符號,鏈接。反過來,對於C++方式編譯的DLL,則無法以靜態鏈接的方式在C程序中調用;主要還是因為C編譯方式編譯出來的函數,無法鏈接到C++方式編譯的DLL,及相應的lib上。  C++和C中的變名具體的內容,還和函數的調用方式相關__cdecl / __stdcall / _fastcall 幾個方式的修飾符號也不相同。

        但是在這種情況下,還是可以通過動態引入的方式,使用GetProcAddress 函數根據函數名稱獲取函數地址,然后調用相應的函數。              

        在X64上又有不同的解釋,X64編譯的函數不變名。前面即沒有下划線,函數名結尾處也沒有參數空間大小,當然也不會類似C++的變名方式。

        通過導出符號導出

 

[cpp] view plain copy
 
 
print?
  1. _declspec(dllexport) int MyFunction(int a, char c, char* pInfo)  
  2. {  
  3.     char szInfo[MAX_PATH] = {};  
  4.     sprintf(szInfo, "a=%d, c=%c, Info: %s", a, c, pInfo);  
  5.     OutputDebugStringA(szInfo);  
  6.   
  7.     return 0;  
  8. }  
_declspec(dllexport) int MyFunction(int a, char c, char* pInfo)
{
	char szInfo[MAX_PATH] = {};
	sprintf(szInfo, "a=%d, c=%c, Info: %s", a, c, pInfo);
	OutputDebugStringA(szInfo);

	return 0;
}

        按照如上的方式,MyFunction 函數將被DLL導出,C/C++編譯方式下,分別為導出為如下的函數(可以使用PE解析工具查看):

        如下為C++的編譯方式導出的函數,可看到函數名稱開始處有一個"?",結尾處為兩個@@加上YAHHDPAD@Z ,他們共同表示了函數的參數,以及返回值。@@YA表示函數調用方式為 __cdecl,默認的調用方式。H表示返回值為Int,后面依次為參數列表的類型。(詳細的內容可以搜索一下)

        

        如下為C編譯方式導出的函數,僅有函數名稱。

        

        如上所述,通常為了能夠使得C++編譯的模塊能夠在C和C++程序中都能調用,通常定義如下的宏,用於聲明函數。一方面將函數導出,同時使得無論C++程序編譯,還是C程序編譯的DLL都按照C的編譯方式導出函數。這樣就可以滿足C 和C++ 程序都可以調用。

        #define DLLEXPORT_API extern "C" _declspec(dllexport)

        或者

 

[cpp] view plain copy
 
 
print?
  1. #ifdef __cplusplus  
  2. extern "C"{  
  3. #endif  
  4. 的函數名稱  
  5. #ifdef __cplusplus  
  6. };  
  7. #endif  
        #ifdef __cplusplus
        extern "C"{
        #endif
		// 要導出的函數名稱
        #ifdef __cplusplus
        };
        #endif

        通過Link選項設置導出函數

        通過linker 選項來導出函數,通過這種方式可以將函數進行改名,即將在C++編譯方式下導出的名稱導出為一個指定的名稱,而不用帶有? @ 等字符的函數原名。

 

[cpp] view plain copy
 
 
print?
  1. #pragma comment(linker, "/EXPORT:MyFunction=?MyFunction@@YAHHDPAD@Z")  
  2.   
  3. int MyFunction(int a, char c, char* pInfo)  
  4. {  
  5.     char szInfo[MAX_PATH] = {};  
  6.     sprintf(szInfo, "Dll module: a=%d, c=%c, Info: %s\n", a, c, pInfo);  
  7.     OutputDebugStringA(szInfo);  
  8.   
  9.     return 0;  
  10. }  
#pragma comment(linker, "/EXPORT:MyFunction=?MyFunction@@YAHHDPAD@Z")

int MyFunction(int a, char c, char* pInfo)
{
	char szInfo[MAX_PATH] = {};
	sprintf(szInfo, "Dll module: a=%d, c=%c, Info: %s\n", a, c, pInfo);
	OutputDebugStringA(szInfo);

	return 0;
}

        如下圖所示,上面代碼中的函數,以C++的方式編譯完成后,和之前的以C++方式導出函數的名稱相同,我們使用 #pragma comment的形式,將該函數以函數原名稱的形式導出。這樣可以方便引用。

        

        通過.def 文件導出函數

 

        最正規的方法,還是通過定義.def 文件來導出函數。將要導出的函數,寫到.def 文件 EXPORTS 字段下,即可將函數導出。在連接器階段,使用/def 連接器選項調用.def 文件。def的內容對應於link 選項,.def 的語法如下所述:

        ×  語句,屬性,關鍵字和指定的標識符區分大小寫

        ×  使用一個,多個空格,制表符或換行符將語句關鍵字同其他參數分開和將語句分開。指定參數的冒號或等號前后可以有零個,多個空格,制表符或換行。

        ×  NAME 和 LIBRARY 語句,必須位於所有的其他的語句之前。

        ×  注釋以 分號 開始

        EXPORTS 語句的語法如下:

                EXPORTS

                        entryname[=internalname] [@ordinal  [NONAME] ]  [PRIVATE]  [DATA]

                entryname 是要導出的函數名或變量名稱。這一項必須有。

                =internalname 表示將一個名字為internalname的函數導出為entryname的函數。

                @ordinal  允許指定是序號導出函數,而不是以函數名導出。.lib 文件中包含了序號和函數之間的映射。

                NONAME為可選項,該關鍵字指明,只允許按照序號導出。

                PRIVATE 可選項,表示禁止將entryname放到LINK生成的導入庫中。

                DATA 可選項,表示導出的為數據,而非代碼。

        示例:

 

[cpp] view plain copy
 
 
print?
  1. EXPORTS      
  2.     DllCanUnloadNow         @1          PRIVATE     DATA      
  3.     DllWindowName  = Name                           DATA      
  4.     DllGetClassObject       @4  NONAME      PRIVATE      
  5.     DllRegisterServer       @7  
  6.     DllUnregisterServer  
EXPORTS    
	DllCanUnloadNow			@1			PRIVATE		DATA    
	DllWindowName  = Name							DATA    
	DllGetClassObject		@4	NONAME		PRIVATE    
	DllRegisterServer		@7
	DllUnregisterServer

        
        LIBRARY 選項

                LIBRARY  [library] [BASE=address]

                library 表示讓link創建DLL,同時創建導入庫。

                BASE=address 設置操作系統用來加載DLL的基址。

        SECTIOINS 選項

                SECTIONS

                        definitions

                用於指定一些節區的屬性,比如設置節區為共享節區等。

        

        

        3. 基於MFC框架的DLL

        可以很方便地到處全局變量,導出函數,導出類。有了MFC的支持,方便不少。這塊已經不怎么用了吧,我平時很難涉及到,不想再總結了。有興趣的可以找資料看一下。

        ATL同樣也有很多,基於ATL的COM模塊,基本都是建立一個DLL模塊。這塊也不想多說了,無非是將ATL的框架,模板集成進去了,DLL作為容器而已。其實這個和DLL的知識沒什么關系,完全不同的兩部分內容,需要分開學。我覺得是醬紫的,哈哈哈哈!

        沒准以后工作中會遇到,遇到再說吧!^_^! 以夠用為原則,學多了也記不住!

 

        By  AndyGuo @ 2015-08-11 午


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM