一、為什么需要dll
代碼復用是提高軟件開發效率的重要途徑。一般而言,只要某部分代碼具有通用性,就可將它構造成相對獨立的功能模塊並在之后的項目中重復使用。
比較常見的例子是各種應用程序框架,如ATL、MFC等,它們都以源代碼的形式發布。由於這種復用是“源碼級別”的,源代碼完全暴露給了程序員,因
而稱之為“白盒復用”。“白盒復用”的缺點比較多,總結起來有4點。
- 暴露了源代碼;
- 容易與程序員的“普通”代碼發生命名沖突;
- 多份拷貝,造成存儲浪費;
- 更新功能模塊比較困難。
實際上,以上4點概括起來就是“暴露的源代碼”造成“代碼嚴重耦合”。為了彌補這些不足,就提出了“二進制級別”的代碼復用。使用二進制級別的代碼復用
一定程度上隱藏了源代碼,對於緩解代碼耦合現象起到了一定的作用。這樣的復用被稱為“黑盒復用”。
在Windows操作系統中有兩種可執行文件,其后綴名分別為.exe和.dll。它們的區別在於,.exe文件可被獨立的裝載於內存中運行;.dll文件卻不能,它只能被
其它進程調用。然而無論什么格式,它們都是二進制文件。上面說到的“二進制級別”的代碼復用,可以使用.dll來實現。
與白盒復用相比,.dll很大程度上彌補了上述4大缺陷。.dll是二進制文件,因此隱藏了源代碼;如果采用“顯式調用”(后邊將會提到),一般不會發生命名沖
突;由於.dll是動態鏈接到應用程序中去的,它並不會在鏈接生成程序時被原原本本拷貝進去;.dll文件相對獨立的存在,因此更新功能模塊是可行的。
說明:實現“黑盒復用”的途徑不只dll一種,靜態鏈接庫甚至更高級的COM組件都是。本文只對dll進行討論。
二、創建dll
接下來用一個簡單的例子來說明創建dll的方法。本例采用VS2010,使用C++編程語言,具體操作步驟如下。
通過Start Page或者File菜單欄,新建一個Project,將會彈出新建項目對話框。選擇Win32 Project向導,項目名為CreateDLL,解決方案名為DLLTEST(注意
Create directories for solution是勾選上的),點擊OK,接着點擊Next,到Application Settings,選擇應用程序類型為dll,並勾選“Export Symbols”,點擊Finish。
完成這一步之后,VS界面上左邊的Solution Explorer中將會看到向導自動生成的文件列表,如圖1所示。
圖1 wizard自動生成的文件列表
在VS界面的編輯窗口中,展示了自動生成的CreateDLL.cpp的代碼。
- // CreateDLL.cpp : Defines the exported functions for the DLL application.
- //
- #include "stdafx.h"
- #include "CreateDLL.h"
- // This is an example of an exported variable
- CREATEDLL_API int nCreateDLL = 0;
- // This is an example of an exported function.
- CREATEDLL_API int fnCreateDLL(void)
- {
- return 42;
- }
- // This is the constructor of a class that has been exported.
- // see CreateDLL.h for the class definition
- CCreateDLL::CCreateDLL()
- {
- return;
- }
這里有3種類型的example,分別為導出變量nCreateDLL、導出函數fnCreateDLL以及導出類CCreateDLL。為了簡化起見,本例只考慮導出函數。
修改CreateDLL.h文件為:
- #ifdef CREATEDLL_EXPORTS
- #define CREATEDLL_API __declspec(dllexport)
- #else
- #define CREATEDLL_API __declspec(dllimport)
- #endif
- CREATEDLL_API void printMax(int&,int&);
- CREATEDLL_API void printMax(int&,int&,int&);
修改CreateDLL.cpp文件為:
- CREATEDLL_API void printMax(int& a,int& b)
- {
- std::cout<<"Among ("<<a<<","<<b<<"), the Max Number is "<<(a>b?a:b)<<"\n";
- }
- CREATEDLL_API void printMax(int& a,int& b,int& c)
- {
- std::cout<<"Among ("<<a<<","<<b<<","<<c<<"), the Max Number is "<<(((a>b?a:b)>c)?(a>b?a:b):c)<<"\n";
- }
不難發現,printMax函數的作用就是打印出兩個整數或三個整數中的最大值。需要說明的是,這里故意使用同名函數是為了引出導出函數的修飾名稱,
具體將在第四節中闡述。
接下來,選擇菜單Build->Build CreateDLL,Output窗口提示CreateDLL.dll文件生成成功,如圖2所示。
圖2 CreateDLL.dll成功生成
三、使用dll
本例采用“顯式調用”的方式使用CreateDLL.dll。顯式調用方式相比於”隱式調用“有好有壞。顯式調用只需要一個.dll文件就可以了,靈活性更好,
更新模塊方便;相對的,程序員需要做的事情更多,使用方法更為復雜。
右鍵單擊Solution Explorer中的Solution 'DLLTEST',在彈出的菜單中選擇Add->New Project,選擇Win32 Console Application,輸入項目名為UseDLL,
點擊OK,接着點擊Next,在Application Settings界面勾選EmptyProject並點擊Finish。右鍵單擊項目UseDLL,給它添加源文件UseDLL.cpp。這樣操
作之后,Solution Explorer的信息如圖3所示。
圖3 向Solution'DLLTEST'添加項目UseDLL
編寫UseDLL.cpp的代碼為:
- /*--UseDLL.cpp
- *Author: ume(李優米)
- *Use CreateDLL.dll explicitly
- */
- #include<Windows.h>
- #include<iostream>
- typedef void(*FUNA)(int&,int&);
- typedef void(*FUNB)(int&,int&,int&);
- int main()
- {
- const char* dllName = "CreateDLL.dll";
- const char* funName1 = "printMax";
- const char* funName2 = "printMax";
- int x(100), y(100), z(100);
- HMODULE hDLL = LoadLibrary(dllName);
- if(hDLL != NULL)
- {
- FUNA fp1 = FUNA(GetProcAddress(hDLL,funName1));
- if(fp1 != NULL)
- {
- std::cout<<"Input 2 Numbers:";
- std::cin>>x>>y;
- fp1(x,y);
- }
- else
- {
- std::cout<<"Cannot Find Function "<<funName1<<std::endl;
- }
- FUNB fp2 = FUNB(GetProcAddress(hDLL,funName2));
- if(fp2 != NULL)
- {
- std::cout<<"Input 3 Numbers:";
- std::cin>>x>>y>>z;
- fp2(x,y,z);
- }
- else
- {
- std::cout<<"Cannot Find Function "<<funName2<<std::endl;
- }
- FreeLibrary(hDLL);
- }
- else
- {
- std::cout<<"Cannot Find "<<dllName<<std::endl;
- }
- return 1;
- }
代碼比較長,但是並不難理解,這里僅說明代碼中的一些要點。
- 包含頭文件Windows.h,原因在於程序中用到了LoadLibrary、FreeLibrary、GetProcAddress等Win32 API函數。
- FUNA和FUNB是函數指針類型的聲明。
- 當程序不再使用dll時,應該調用FreeLibrary及時釋放它占用的內存空間。
- 如果在const char* dllName和funName底部出現紅色波浪線提示,說明采用的字符集不匹配,需要修改項目UseDLL的屬性CharaterSet為Not Set。
- 為方便項目的調試,建議修改解決方案的Startup Project屬性為Single startup project並以UseDLL為首選。
然而,這個程序還有錯誤。編譯並運行,結果如圖4所示。
圖4 UseDLL的運行結果
這並不是期望中的結果。實際上,正如第二節提到的那樣,造成這種錯誤的原因正是導出函數的修飾名稱。雖然在CreateDLL.cpp中兩個printMax函數
有相同的名稱,但在dll二進制文件中,經過編譯器的“加工”,它們實際上各自有不同的名稱了。這也是函數重載機制得以實現的一個技術支持。
使用VS2010附帶工具dumpbin,查看CreateDLL.dll的導出函數名,結果如圖5所示。
圖5 查看CreateDLL.dll的導出函數名
觀察圖5可以發現,CreateDLL.dll導出函數名為?printMax@@YAXAAH00@Z和?printMax@@YAXAAH0@Z。它們分別對應着三個整數的printMax
和兩個整數的printMax。因此,Use.DLL中funName應當相應修改為:
- const char* funName1 = "?printMax@@YAXAAH0@Z";
- const char* funName2 = “?printMax@@YAXAAH00@Z”;
修改之后,再次編譯運行,結果正確,如圖6所示。
圖6 UseDLL正常運行
四、dll導出函數名稱規范化
創建、使用dll並不復雜,走過前三節,相信讀者肯定有這樣的體會。然而,一個問題仍然值得思考:導出函數的修飾名稱太“奇怪”,
為dll的使用帶來了不便,能不能讓導出函數的修飾名稱規范一些?答案是肯定的,而且方法至少有兩種:一是運用extern "C"修飾printMax;
二是運用模塊定義文件.def。后者的效果更好,所以本節將使用.def來規范化導出函數的修飾名稱。
CreateDLL.dll導出的兩個函數功能很簡單,根據功能描述,理想的函數名稱是pMaxA2和pMaxA3。在CreateDLL項目中添加CreateDLL.def文件:
- LIBRARY CreateDLL
- EXPORTS
- pMaxA2 = ?printMax@@YAXAAH0@Z
- pMaxA3 = ?printMax@@YAXAAH00@Z
重新build項目CreateDLL,使用dumpbin再次查看CreateDLL.dll的導出函數名稱,結果如圖7所示。
圖7 規范化的函數名,奇怪的修飾名稱還存在
出現了期望的結果,但仍有小缺憾:奇怪的修飾名稱仍然存在。能否去掉這些不太規范的修飾名稱呢?當然是可以的。只需要將CreateDLL.h
中#define CREATEDLL_API __declspec(dllexport) 修改為#define CREATEDLL_API即可。修改之后重新編譯生成CreateDLL.dll,使用dumpbin
查看導出函數名稱,結果如圖8所示。
圖8 規范化的函數名,去除了奇怪的修飾名稱
回到UseDLL.cpp,修改funName:
- const char* funName1 = "pMaxA2";
- const char* funName2 = "pMaxA3";
重新編譯運行UseDLL,結果正確,與圖6類似。
五、dll的不足
動態鏈接庫雖然一定程度上實現了“黑盒復用”,但仍存在着諸多不足,筆者能夠想到的有下面幾點。
- dll節省了編譯期的時間,但相應延長了運行期的時間,因為在使用dll的導出函數時,不但要加載dll,而且程序將會在模塊間跳轉,
- 降低了cache的命中率。
- 若采用隱式調用,仍然需要.h、.lib、.dll文件(“三件套”),並不能有效支持模塊的更新。
- 顯式調用雖然很好地支持模塊的更新,但卻不能導出類和變量。
- dll不支持Template。
二進制級別的代碼復用相比源碼級別的復用已經有了很大的進步,但在二進制級別的代碼復用中,dll顯得太古老。想真正完美實現跨平台、
跨語言的黑盒復用,采用COM才是正確的選擇。
http://blog.csdn.net/wondergdf/article/details/7870491?reload