C++編寫DLL
雖然能用DLL實現的功能都可以用COM來替代,但DLL的優點確實不少,它更容易建立。本文將討論如何利用VC MFC來建立不同型別的DLL,以及如何使用他們。一、DLL的不同型別 使用 V C++可以生成兩種型別的DLL:MFC擴充套件DLL和常規DLL。常規DLL有可以分為動態連線和靜態連線。Visual C++還可以生成WIN32 DLL,但不是這裡討論的主要物件。
1、MFC擴充套件DLL 每個DLL都有某種型別的介面:變數、指標、函式、客戶程式訪問的類。它們的作用是讓客戶程式使用DLL,MFC擴充套件DLL可以有C++的介面。也就是它可以匯出C++類給客戶端。匯出的函式可以使用C++/MFC資料型別做引數或返回值,匯出一個類時客戶端能建立類物件或者派生這個類。同時,在DLL中也可以使用DLL和MFC。 Visual C++使用的MFC類庫也是儲存在一個DLL中,MFC擴充套件DLL動態連線到MFC程式碼庫的DLL,客戶程式也必須要動態連線到MFC程式碼庫的DLL。(這裡談到的兩個DLL,一個是我們自己編寫的DLL,一個裝MFC類庫的DLL)現在MFC程式碼庫的DLL也存在多個版本,客戶程式和擴充套件DLL都必須使用相同版本的MFC程式碼DLL。所以為了讓MFC擴充套件DLL能很好的工作,擴充套件DLL和客戶程式都必須動態連線到MFC程式碼庫DLL。而這個DLL必須在客戶程式執行的計算機上。 2、常規DLL 使用MFC擴充套件DLL的一個問題就是DLL僅能和MFC客戶程式一起工作,如果需要一個使用更廣泛的DLL,最好採用常規DLL,因為它不受MFC的某些限制。常規DLL也有缺點:它不能和客戶程式傳送指標或MFC派生類和物件的引用。一句話就是常規DLL和客戶程式的介面不能使用MFC,但在DLL和客戶程式的內部還是可以使用MFC。 當在常規DLL的內部使用MFC程式碼庫的DLL時,可以是動態連線/靜態連線。如果是動態連線,也就是常規DLL需要的MFC程式碼沒有構建到DLL中,這種情況有點和擴充套件DLL類似,在DLL執行的計算機上必須要MFC程式碼庫的DLL。如果是靜態連線,常規DLL裡面已經包含了需要的MFC程式碼,這樣DLL的體積將比較大,但它可以在沒有MFC程式碼庫DLL的計算機上正常執行。
二、建立DLL 利用Visual C++提供的嚮導功能可以很容易建立一個不完成任何實質任務的DLL,這裡就不多講了,主要的任務是如何給DLL新增功能,以及在客戶程式中利用這個DLL 1、匯出類 用嚮導建立好框架後,就可以新增需要匯出類的.cpp .h檔案到DLL中來,或者用嚮導建立C++ Herder File/C++ Source File。為了能匯出這個類,在類宣告的時候要加“_declspec(dllexport)”,如: class _declspec(dllexport) CMyClass { ...//宣告 } 如果建立的MFC擴充套件DLL,可以使用巨集:AFX_EXT_CLASS: class AFX_EXT_CLASS CMyClass { ...//宣告 } 這樣匯出類的方法是最簡單的,也可以採用.def檔案匯出,這裡暫不詳談。 2、匯出變數、常量、物件
很多時候不需要匯出一個類,可以讓DLL匯出一個變數、常量、物件,匯出它們只需要進行簡單的宣告:_declspec(dllexport) int MyInt; _declspec(dllexport) extern const COLORREF MyColor=RGB(0,0,0); _declspec(dllexport) CRect rect(10,10,20,20); 要匯出一個常量時必須使用關鍵字extern,否則會發生連線錯誤。 注意:如果客戶程式識別這個類而且有自己的標頭檔案,則只能匯出一個類物件。如果在DLL中建立一個類,客戶程式不使用標頭檔案就無法識別這個類。 當匯出一個物件或者變數時,載入DLL的每個客戶程式都有一個自己的拷貝。也就是如果兩個程式使用的是同一個DLL,一個應用程式所做的修改不會影響另一個應用程式。 我們在匯出的時候只能匯出DLL中的全域性變數或物件,而不能匯出區域性的變數和物件,因為它們過了作用域也就不存在了,那樣DLL就不能正常工作。如:
MyFunction() { _declspec(dllexport) int MyInt; _declspec(dllexport) CMyClass object; } 3、匯出函式 匯出函式和匯出變數/物件類似,只要把_declspec(dllexport)加到函式原型開始的位置: _declspec(dllexport) int MyFunction(int); 如果是常規DLL,它將和C寫的程式使用,宣告方式如下: extern "c" _declspec(dllexport) int MyFunction(int); 實現: extern "c" _declspec(dllexport) int MyFunction(int x) { ...//操作 } 如果建立的是動態連線到MFC程式碼庫DLL的常規DLL,則必須插入AFX_MANAGE_STATE作為匯出函式的首行,因此定義如下: extern "c" _declspec(dllexport) int MyFunction(int x) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); ...//操作 } 有時候為了安全起見,在每個常規DLL裡都加上,也不會有任何問題,只是在靜態連線的時候這個巨集無效而已。這是匯出函式的方法,記住只有MFC擴充套件DLL才能讓引數和返回值使用MFC的資料型別。 4、匯出指標 匯出指標的方式如下: _declspec(dllexport) int *pint; _declspec(dllexport) CMyClass object = new CMyClass; 如果宣告的時候同時初始化了指標,就需要找到合適的地方類釋放指標。在擴充套件DLL中有個函式DllMain()。(注意函式名中的兩個l要是小寫字母),可以在這個函式中處理指標: # include "MyClass.h" _declspec(dllexport) CMyClass *pobject = new CMyClass; DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved) { if(dwReason == DLL_PROCESS_ATTACH) { .....// } else if(dwReason == DLL_PROCESS_DETACH) { delete pobject; } } 常規DLL有一個從CWinApp派生的類物件處理DLL的開和關,可以使用類嚮導新增InitInstance/ExitInstance函式。 int CMyDllApp::ExitInstance() { delete pobject; return CWinApp::ExitInstance(); }三、在客戶程式中使用DLL 編譯一個DLL時將建立兩個檔案.dll檔案和.lib檔案。首先將這兩個檔案複製到客戶程式專案的資料夾裡,這裡需要注意DLL和客戶程式的版本問題,儘量使用相同的版本,都使用RELEASE或者都是DEBUG版本。 接著就需要在客戶程式中設定LIB檔案,開啟Project Settings--- >Link--->Object/library Modules中輸入LIB的檔名和路徑。如:Debug/SampleDll.lib。除了DLL和LIB檔案外,客戶程式需要針對匯出類、函式、物件和變數的標頭檔案,現在進行匯入新增的關鍵字就是:_declspec(dllimport),如: _declspec(dllimport) int MyFunction(int); _declspec(dllimport) int MyInt; _declspec(dllimport) CMyClass object; extern "C" _declspec(dllimport) int MyFunction(int); 在有的時候為了匯入類,要把相應類的標頭檔案新增到客戶程式中,不同的是要修改類宣告的標誌: class _declspec(dllimport) CMyClass,如果建立的是擴充套件DLL,兩個位置都是: class AFX_EXT_CLASS CMyClass。
使用DLL的一個比較嚴重的問題就是編譯器之間的相容性問題。不同的編譯器對c++函式在二進位制級別的實現方式是不同的。所以對基於C++的DLL,如果編譯器不同就有很麻煩的。如果建立的是MFC擴充套件DLL,就不會存在問題,因為它只能被動態連線到MFC的客戶應用程式。這裡不是本文討論的重點。一、重新編譯問題 我們先來看一個在實際中可能遇到的問題:
比如現在建立好了一個DLL匯出了CMyClass類,客戶也能正常使用這個DLL,假設CMyClass物件的大小為30位元組。如果我們需要修改DLL中的CMyClass類,讓它有相同的函式和成員變數,但是給增加了一個私有的成員變數int型別,現在CMyClass物件的大小就是34位元組了。當直接把這個新的DLL給客戶使用替換掉原來30位元組大小的DLL,客戶應用程式期望的是30位元組大小的物件,而現在卻變成了一個34位元組大小的物件,糟糕,客戶程式出錯了。 類似的問題,如果不是匯出CMyClass類,而在匯出的函式中使用了CMyClass,改變物件的大小仍然會有問題的。這個時候修改這個問題的唯一辦法就是替換客戶程式中的CMyClass的標頭檔案,全部重新編譯整個應用程式,讓客戶程式使用大小為34位元組的物件。 這就是一個嚴重的問題,有的時候如果沒有客戶程式的原始碼,那麼我們就不能使用這個新的DLL了。
二、解決方法 為了能避免重新編譯客戶程式,這裡介紹兩個方法:(1)使用介面類。(2)使用建立和銷燬類的靜態函式。 1、使用介面類 介面類的也就是建立第二個類,它作為要匯出類的介面,所以在匯出類改變時,也不需要重新編譯客戶程式,因為介面類沒有發生變化。 假設匯出的CMyClass類有兩個函式FunctionA FunctionB。現在建立一個介面類CMyInterface,下面就是在DLL中的CMyInterface類的標頭檔案的程式碼: # include "MyClass.h" class _declspec(dllexport) CMyInterface { CMyClass *pmyclass; CMyInterface(); ~CMyInterface(); public: int FunctionA(int); int FunctionB(int); }; 而在客戶程式中的標頭檔案稍不同,不需要INCLUDE語句,因為客戶程式沒有它的拷貝。相反,使用一個CMyClass的向前宣告,即使沒有標頭檔案也能編譯: class _declspec(dllexport) CMyInterface { class CMyClass;//向前宣告 CMyClass *pmyclass; CMyInterface(); ~CMyInterface(); public: int FunctionA(int); int FunctionB(int); }; 在DLL中的CMyInterface的實現如下: CMyInterface::CMyInterface() { pmyclass = new CMyClass(); } CMyInterface::~CMyInterface() { delete pmyclass; } int CMyInterface::FunctionA() { return pmyclass->FunctionA(); } int CMyInterface::FunctionB() { return pmyclass->FunctionB(); } ..... 對匯出類CMyClass的每個成員函式,CMyInterface類都提供自己的對應的函式。客戶程式與CMyClass沒有聯絡,這樣任意改CMyClass也不會有問題,因為CMyInterface類的大小沒有發生變化。即使為了能訪問CMyClass中的新增變數而給CMyInterface類加了函式也不會有問題的。 但是這種方法也存在明顯的問題,對匯出類的每個函式和成員變數都要對應實現,有的時候這個介面類會很龐大。同時增加了客戶程式呼叫所需要的時間。增加了程式的開銷。
2、使用靜態函式 還可以使用靜態函式來建立和銷燬類物件。建立一個匯出類的時候,增加兩個靜態的公有函式CreateMe()/DestroyMe(),標頭檔案如下: class _declspec(dllexport) CMyClass { CMyClass(); ~CMyClass(); public: static CMyClass *CreateMe(); static void DestroyMe(CMyClass *ptr); }; 實現函式就是: CMyClass * CMyClass::CMyClass() { return new CMyClass; } void CMyClass::DestroyMe(CMyClass *ptr) { delete ptr; } 然後象其他類一樣匯出CMyClass類,這個時候在客戶程式中使用這個類的方法稍有不同了。如若想建立一個CMyClass物件,就應該是: CMyClass x; CMyClass *ptr = CMyClass::CreateMe(); 在使用完後刪除: CMyClass::DestroyMe(ptr);