DLL的相關理解
一種優雅的動態鏈接庫DLL的使用
1. 什麽是DLL(動態鏈接庫)?
動態鏈接庫(DLL)是從C語言函數庫和Pascal庫單元的概念發展而來的。所有的C語言標準庫函數都存放在某一函數庫中。在鏈接應用程序的過程中,鏈接器從庫文件中拷貝程序調用的函數代碼,並把這些函數代碼添加到可執行文件中。這種方法同只把函數儲存在已編譯的OBJ文件中相比更有利於代碼的重用。但隨著Windows這樣的多任務環境的出現,函數庫的方法顯得過於累贅。如果為了完成屏幕輸出、消息處理、內存管理、對話框等操作,每個程序都不得不擁有自己的函數,那麽Windows程序將變得非常龐大。Windows的發展要求允許同時運行的幾個程序共享一組函數的單一拷貝
DLL是一個包含可由多個程序同時使用的代碼和數據的庫。例如:在Windows操作系統中,Comdlg32 DLL執行與對話框有關的常見函數。
因此,每個程序都可以使用該DLL中包含的功能來實現“打開”對話框。這有助於促進代碼重用和內存的有效使用。這篇文章的目的就是讓你一次性就能了解和掌握DLL。
2. 為什麽要使用DLL(動態鏈接庫)?
代碼復用是提高軟件開發效率的重要途徑。
一般而言,只要某部分代碼具有通用性,就可以將它構造成相對獨立的功能模塊並在之後的項目中重復使用。
比較常見的例子是各種應用程序框架,它們都以源代碼的形式發布。由於這種復用是源代碼級別的,源代碼完全暴露給了程序員,因而稱之為“白盒復用”。白盒復用有以下三個缺點:
1.暴露源代碼,多份拷貝,造成存儲浪費;
2.容易與程序員的本地代碼發生命名沖突;
3.更新模塊功能比較困難,不利於問題的模塊化實現;
為了彌補這些不足,就提出了“二進制級別”的代碼復用了。使用二進制級別的代碼復用一定程度上隱藏了源代碼,對於“黑盒復用”的途徑不只DLL一種,靜態鏈接庫,甚至更高級的COM組件都是。
使用DLL主要有以下優點:
1.使用較少的資源;當多個程序使用同一函數庫時,DLL可以減少在磁盤和物理內存中加載的代碼的重復量。
這不僅可以大大影響在前臺運行的程序,而且可以大大影響其它在Windows操作系統上運行的程序;
2.推廣模塊式體系結構;
3.簡化部署與安裝。
優雅創建DLL,與優雅使用DLL庫 步驟
3.1 創建DLL
用VS2013新建工程,
、
點擊確定,新建工程完畢
這個時候VS2013自動幫我們添加了dllmain文件,
Windows在加載DLL時,需要一個入口函數,就像控制臺程序需要main函數一樣。有的時候,DLL並沒有提供DllMain函數,應用程序也能成功引用DLL,這是因為Windows在找不到DllMain的時候,系統會從其它運行庫中引入一個不做任何操作的默認DllMain函數版本,並不意味著DLL可以拋棄DllMain函數。(因此我們可以不使用該dllmain文件,或者刪除即可)根據編寫規範,Windows必須查找並執行DLL裏的DllMain函數作為加載DLL的依據,它使得DLL得以保留在內存裏。這個函數並不屬於導出函數,而是DLL的內部函數,這就說明不能在客戶端直接調用DllMain函數,DllMain函數是自動被調用的。
DllMain函數在DLL被加載和卸載時被調用,在單個線程啟動和終止時,DllMain函數也被調用。參數ul_reason_for_call指明了調用DllMain的原因,有以下四種情況:
DLL_PROCESS_ATTACH:當一個DLL被首次載入進程地址空間時,系統會調用該DLL的DllMain函數,傳遞的ul_reason_for_call參數值為DLL_PROCESS_ATTACH。這種情況只有首次映射DLL時才發生;
DLL_THREAD_ATTACH:該通知告訴所有的DLL執行線程的初始化。當進程創建一個新的線程時,系統會查看進程地址空間中所有的DLL文件映射,之後用DLL_THREAD_ATTACH來調用DLL中的DllMain函數。要註意的是,系統不會為進程的主線程使用值DLL_THREAD_ATTACH來調用DLL中的DllMain函數;
DLL_PROCESS_DETACH:當DLL從進程的地址空間解除映射時,參數ul_reason_for_call參數值為DLL_PROCESS_DETACH。當DLL處理DLL_PROCESS_DETACH時,DLL應該處理與進程相關的清理操作。如果進程的終結是因為系統中有某個線程調用了TerminateProcess來終結的,那麽系統就不會用DLL_PROCESS_DETACH來調用DLL中的DllMain函數來執行進程的清理工作。這樣就會造成數據丟失;
DLL_THREAD_DETACH:該通知告訴所有的DLL執行線程的清理工作。註意的是如果線程的終結是使用TerminateThread來完成的,那麽系統將不會使用值DLL_THREAD_DETACH來執行線程的清理工作,這也就是說可能會造成數據丟失,所以不要使用TerminateThread來終結線程。
3.2 編寫DLL的函數
編寫DLL時的函數與一般的函數方法基本一樣。但要對庫中的函數進行必要的聲明,以說明哪些函數是可以導出的,哪些函數是不可以導出的。
把DLL中的函數聲明為導出函數的方法有兩種:
一是使用關鍵字_declspec(dllexport)來聲明。
二是在.def文件中聲明。
一:使用關鍵字來聲明創建
(1)普通函數
1 #ifndef _MYCODE_H_ 2 #define _MYCODE_H_ 3 #ifdef DLLDEMO1_EXPORTS 4 #define EXPORTS_DEMO _declspec( dllexport ) 5 #else 6 #define EXPORTS_DEMO _declspec(dllimport) 7 #endif 8 EXPORTS_DEMO int Add (int a , int b); 9 #endif 10 11 或者: 12 13 #ifndef _MYCODE_H_ 14 #define _MYCODE_H_ 15 #ifdef DLLDEMO1_EXPORTS 16 #define EXPORTS_DEMO _declspec( dllexport ) 17 #else 18 #define EXPORTS_DEMO _declspec(dllimport) 19 #endif 20 extern "C" EXPORTS_DEMO int Add (int a , int b); 21 #endif
【1】這裏采用宏定義的方法進行聲明,這樣就可以把頭文件作為與 生成的DLL、lib 三者一起進行打包發送給使用者。使用者根據宏定義的使用,即可直接包含
【2】extern C
為了使一個用C++語言編寫的DLL函數可以在C語言編寫的應用程序中使用,在關鍵字_declspec(dllexport) 之前要附加另一個關鍵字:extern “C”,以通知編譯器采用C鏈接方式。
為什麽要使用extern “C”呢?C++之父在設計C++時,考慮到當時已經存在了大量的C代碼,為了支持原來的C代碼和已經寫好的C庫,需要在C++中盡可能的支持C,而extern “C”就是其中的一個策略。在聲明函數時,註意到我也使用了extern “C”,這裏要詳細的說說extern “C”。
extern “C”包含兩層含義,首先是它修飾的目標是”extern”的;其次,被它修飾的目標才是”C”的。先來說說extern;在C/C++中,extern用來表明函數和變量作用範圍(可見性)的關鍵字,這個關鍵字告訴編譯器,它申明的函數和變量可以在本模塊或其它模塊中使用。extern的作用總結起來就是以下幾點:
1.在一個文件內:
如果外部變量不在文件的開頭定義,其有效範圍只限定在從定義開始到文件的結束處。如果在定義前需要引用該變量,則要在引用之前用關鍵字”extern”對該變量做”外部變量聲明”,表示該變量是一個已經定義的外部變量。有了這個聲明,就可以從聲明處起合理地使用該變量了。
#include <iostream> using namespace std; int main(int argc, char *argv[]) { extern int a; cout<<a<<endl; } int a = 100;2.在多文件的程序中:
如果多個文件都要使用同一個外部變量,不能在各個文件中各定義一個外部變量,否則會出現“重復定義”的錯誤。
正確的做法是在任意一個文件中定義外部變量,其它文件用extern對變量做“外部變量聲明”。
在編譯和鏈接時,系統會知道該變量是一個已經在別處定義的外部變量,並把另一文件中外部變量的作用域擴展到本文件,這樣在本文件就可以合法地使用該外部變量了。寫過MFC程序的人都知道,在在CXXXApp類的頭文件中,就使用extern聲明了一個該類的變量,而該變量的實際定義是在CXXXApp類的實現文件中完成的;
3. 外部函數:
在定義函數時,如果在最左端加關鍵字extern,表示此函數是外部函數。C語言規定,如果在定義時省略extern,則隱含為外部函數。而內部函數必須在前面加static關鍵字。在需要調用此函數的文件中,用extern對函數作聲明,表明該函數是在其它文件中定義的外部函數。
綜上,因為是對C++ 的類的dll導出,則可以采用下述形式,且可以用添加extern c 的形式:
1 #ifndef AMF_H 2 #define AMF_H 3 4 #if defined(_WIN32) || defined(_WIN64) /*Windows*/ 5 #ifdef DLL_EXPORT 6 #define LIBSPEC __declspec(dllexport) 7 #elif defined(DLL_IMPORT) 8 #define LIBSPEC __declspec(dllimport) 9 #else 10 #define LIBSPEC //什麽都沒有定義,直接使用源碼 11 #endif 12 #else /*non-windows*/ 13 #define LIBSPEC /*TODO: Allow this header file to generate (and be distributed) with non-windows shared objects*/ 14 #endif 15 16 17 class LIBSPEC Amf /*LIBSPEC macro allows windows users the option of using this class in DLL form */ 18 { 19 public: 20 Amf(); 21 ~Amf(); 22 Amf(const Amf& In); 23 Amf& operator=(const Amf& In); 24 25 26 //Amf I/O 27 bool Save(std::string AmfFilePath, bool Compressed = true); 28 bool Load(std::string AmfFilePath, bool StrictLoad = true); 29 } 30 31 #undef LIBSPEC 32 #endif //AMF_WIN_H
把上述作為 CXXXXX.h 的頭文件內容,因為該文件最終要副帶給使用者因此,不合適在臨面進行函數實現。
而函數或者類的實現作為CXXXXX.cpp 源文件中。
1 extern "C" __declspec(dllexport) void SayHello() 2 3 { 4 5 ::MessageBoxW(NULL, L"Hello", L"fangyukuan", MB_OK); 6 7 }
或者
普通的類的實現源文件相同。
#include “CXXXX.h” //類的實現,且不需要額外修飾 CXXX::CXXX() { } CXXX::XXXX() { }
編譯即可生成相相應的DLL:
一般生成四個文件,其中最重要的額DLL是動態鏈接庫, lib是函數接口的映射,以及 包含導出生命的頭文件.h
4 使用DLL
基本上要把三個文件進行使用:
(1)XXX.dll 文件,放在程序生成exe的目錄下面;
(2)XXX.lib 文件,可以像在項目中進行加載,也可以放在程序使用的代碼中加載。
放在程序中加載:告訴連接器,lib文件的位置,以及需要連接加載的lib名稱。
放在代碼中加載:
1 #include <windows.h> 2 #include <iostream> 3 //(1)添加dll的頭文件 4 #include "..\\DLLDemo1\\CXXX.h" 5 using namespace std; 6 //(2)加載lib文件 7 #pragma comment(lib, "..\\debug\\DLLDemo1.lib") 8 // (3)dll放在 exe目錄即可 9 int main(int argc, char *argv[]) 10 { 11 cout<<Add(2, 3)<<endl; 12 return 0; 13 }
在使用上,一般沒什麽差異,直接利用include的頭文件中的類型進行使用即可。
5. 使用VS6中的工具進行檢測
depend.exe 可以用來打開dll文件,並查看dll的依賴文件,以及dll中的導出的函數接口。
註意:如果沒有使用extern c聲明的導出接口,其depends開發的效果如下:
即名字為 ?Add@@[email protected]
如果用extern C聲明, 可函數名稱就正常了。
不過,一般如果不采用直接對單個導出函數進行單獨使用的方式,上述兩種差異不大。
為了簡潔,且一般開發的C++ 的dll ,如果包含類,則沒必要使用 extern c 因為,C是沒辦法調用C++ 的dll 的。
endl;
DLL的相關理解