DLL的優勢
簡單的說,dll有如下幾個優勢:html
1) 節省內存。同一個軟件模塊,如果以源代碼的形式重用,則會被編譯到不一樣的可執行程序中,同時運行這些exe時這些模塊的二進制碼會被重複加載到內存中。若是使用dll,則只在內存中加載一次,全部使用該dll的進程會共享此塊內存(固然,像dll中的全局變量這種東西是會被每一個進程複製一份的)。ios
2) 便於不需編譯的軟件系統升級,若一個軟件系統使用了dll,則該dll被改變(函數名不變)時,系統升級只須要更換此dll便可,不須要從新編譯整個系統。事實上,不少軟件都是以這種方式升級的。例如咱們常常玩的星際、魔獸等遊戲也是這樣進行版本升級的。c++
3) Dll庫能夠供多種編程語言使用,例如用c編寫的dll能夠在vb中調用。這一點上DLL還作得很不夠,所以在dll的基礎上發明了COM技術,更好的解決了一系列問題。編程
最簡單的dll
開始寫dll以前,你須要一個c/c++編譯器和連接器,並關閉你的IDE。是的,把你的VC和C++ BUILDER之類的東東都關掉,並打開你以往只用來記電話的記事本程序。不這樣作的話,你可能一生也不明白dll的真諦。我使用了VC自帶的cl編譯器和link連接器,它們通常都在vc的bin目錄下。(若你沒有在安裝vc的時候選擇註冊環境變量,那麼就馬上將它們的路徑加入path吧)若是你仍是由於離開了IDE而懼怕到哭泣的話,你能夠關閉這個頁面並繼續去看《VC++技術內幕》之類無聊的書了。windows
最簡單的dll並不比c的helloworld難,只要一個DllMain函數便可,包含objbase.h頭文件(支持COM技術的一個頭文件)。若你以爲這個頭文件名字難記,那麼用windows.H也能夠。源代碼以下:dll_nolib.cppapi
#include <objbase.h>編程語言
#include <iostream.h>函數
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)工具
{編碼
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
cout<<"Dll is attached!"<<endl;
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
cout<<"Dll is detached!"<<endl;
g_hModule=NULL;
break;
}
return true;
}
其中DllMain是每一個dll的入口函數,如同c的main函數同樣。DllMain帶有三個參數,hModule表示本dll的實例句柄(聽不懂就不理它,寫過windows程序的天然懂),dwReason表示dll當前所處的狀態,例如DLL_PROCESS_ATTACH表示dll剛剛被加載到一個進程中,DLL_PROCESS_DETACH表示dll剛剛從一個進程中卸載。固然還有表示加載到線程中和從線程中卸載的狀態,這裏省略。最後一個參數是一個保留參數(目前和dll的一些狀態相關,可是不多使用)。
從上面的程序能夠看出,當dll被加載到一個進程中時,dll打印"Dll is attached!"語句;當dll從進程中卸載時,打印"Dll is detached!"語句。
編譯dll須要如下兩條命令:
cl /c dll_nolib.cpp
這條命令會將cpp編譯爲obj文件,若不使用/c參數則cl還會試圖繼續將obj連接爲exe,可是這裏是一個dll,沒有main函數,所以會報錯。沒關係,繼續使用連接命令。
Link /dll dll_nolib.obj
這條命令會生成dll_nolib.dll。
注意,由於編譯命令比較簡單,因此本文不討論nmake,有興趣的可使用nmake,或者寫個bat批處理來編譯連接dll。
加載DLL(顯式調用)
使用dll大致上有兩種方式,顯式調用和隱式調用。這裏首先介紹顯式調用。編寫一個客戶端程序:dll_nolib_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
//加載咱們的dll
HINSTANCE hinst=::LoadLibrary("dll_nolib.dll");
if (NULL != hinst)
{
cout<<"dll loaded!"<<endl;
}
return 0;
}
注意,調用dll使用LoadLibrary函數,它的參數就是dll的路徑和名稱,返回值是dll的句柄。 使用以下命令編譯連接客戶端:
Cl dll_nolib_client.cpp
並執行dll_nolib_client.exe,獲得以下結果:
Dll is attached!
dll loaded!
Dll is detached!
以上結果代表dll已經被客戶端加載過。可是這樣僅僅可以將dll加載到內存,不能找到dll中的函數。
使用dumpbin命令查看DLL中的函數
Dumpbin命令能夠查看一個dll中的輸出函數符號名,鍵入以下命令:
Dumpbin –exports (#add /exports亦可) dll_nolib.dll
經過查看,發現dll_nolib.dll並無輸出任何函數。
如何在dll中定義輸出函數
整體來講有兩種方法,一種是添加一個def定義文件,在此文件中定義dll中要輸出的函數;第二種是在源代碼中待輸出的函數前加上__declspec(dllexport)關鍵字。
Def文件
首先寫一個帶有輸出函數的dll,源代碼以下:dll_def.cpp
#include <objbase.h>
#include <iostream.h>
void FuncInDll (void)
{
cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
g_hModule=NULL;
break;
}
return TRUE;
}
這個dll的def文件以下:dll_def.def
;
; dll_def module-definition file
;
LIBRARY dll_def.dll
DESCRIPTION '(c)2007-2009 Wang Xuebin'
EXPORTS
FuncInDll @1 PRIVATE
你會發現def的語法很簡單,首先是LIBRARY關鍵字,指定dll的名字;而後一個可選的關鍵字DESCRIPTION,後面寫上版權等信息(不寫也能夠);最後是EXPORTS關鍵字,後面寫上dll中全部要輸出的函數名或變量名,而後接上@以及依次編號的數字(從1到N),最後接上修飾符。
用以下命令編譯連接帶有def文件的dll:
Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def
再調用dumpbin查看生成的dll_def.dll:
Dumpbin –exports dll_def.dll
獲得以下結果:
Dump of file dll_def.dll
File Type: DLL
Section contains the following exports for dll_def.dll
0 characteristics
46E4EE98 time date stamp Mon Sep 10 15:13:28 2007
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 00001000 FuncInDll
Summary
2000 .data
1000 .rdata
1000 .reloc
6000 .text
觀察這一行
1 0 00001000 FuncInDll
會發現該dll輸出了函數FuncInDll。
顯式調用DLL中的函數
寫一個dll_def.dll的客戶端程序:dll_def_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
//定義一個函數指針
typedef void (* DLLWITHLIB )(void);
//定義一個函數指針變量
DLLWITHLIB pfFuncInDll = NULL;
//加載咱們的dll
HINSTANCE hinst=::LoadLibrary("dll_def.dll");
if (NULL != hinst)
{
cout<<"dll loaded!"<<endl;
}
//找到dll的FuncInDll函數
pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll");
//調用dll裏的函數
if (NULL != pfFuncInDll)
{
(*pfFuncInDll)();
}
return 0;
}
有兩個地方值得注意,第一是函數指針的定義和使用,不懂的隨便找本c++書看看;第二是GetProcAddress的使用,這個API是用來查找dll中的函數地址的,第一個參數是DLL的句柄,即LoadLibrary返回的句柄,第二個參數是dll中的函數名稱,即dumpbin中輸出的函數名(注意,這裏的函數名稱指的是編譯後的函數名,不必定等於dll源代碼中的函數名)。
編譯連接這個客戶端程序,並執行會獲得:
dll loaded!
FuncInDll is called!
這代表客戶端成功調用了dll中的函數FuncInDll。
__declspec(dllexport)
爲每一個dll寫def顯得很繁雜,目前def使用已經比較少了,更多的是使用__declspec(dllexport)在源代碼中定義dll的輸出函數。
Dll寫法同上,去掉def文件,並在每一個要輸出的函數前面加上聲明__declspec(dllexport),例如:
__declspec(dllexport) void FuncInDll (void)
這裏提供一個dll源程序dll_withlib.cpp,而後編譯連接。連接時不須要指定/DEF:參數,直接加/DLL參數便可,
Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj
而後使用dumpbin命令查看,獲得:
1 0 00001000 ?FuncInDll@@YAXXZ
可知編譯後的函數名爲?FuncInDll@@YAXXZ,而並非FuncInDll,這是由於c++編譯器基於函數重載的考慮,會更改函數名,這樣使用顯式調用的時候,也必須使用這個更改後的函數名,這顯然給客戶帶來麻煩。爲了避免這種現象,可使用extern 「C」指令來命令c++編譯器以c編譯器的方式來命名該函數(#add extern 「C」不是必須,但爲了支持多語言通常採用此形式,但若是有重載函數要導出則不用extern 「C」)。修改後的函數聲明爲:
extern "C" __declspec(dllexport) void FuncInDll (void)
dumpbin命令結果:
1 0 00001000 FuncInDll
這樣,顯式調用時只需查找函數名爲FuncInDll的函數便可成功。
extern 「C」
使用extern 「C」關鍵字實際上至關於一個編譯器的開關,它能夠將c++語言的函數編譯爲c語言的函數名稱。即保持編譯後的函數符號名等於源代碼中的函數名稱。
隱式調用DLL
顯式調用顯得很是複雜,每次都要LoadLibrary,而且每一個函數都必須使用GetProcAddress來獲得函數指針,這對於大量使用dll函數的客戶是一種困擾。而隱式調用可以像使用c函數庫同樣使用dll中的函數,很是方便快捷。
下面是一個隱式調用的例子:dll包含兩個文件dll_withlibAndH.cpp和dll_withlibAndH.h。
代碼以下:dll_withlibAndH.h
extern "C" __declspec(dllexport) void FuncInDll (void);
dll_withlibAndH.cpp
#include <objbase.h>
#include <iostream.h>
#include "dll_withLibAndH.h"//看到沒有,這就是咱們增長的頭文件
extern "C" __declspec(dllexport) void FuncInDll (void)
{
cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
g_hModule=NULL;
break;
}
return TRUE;
}
編譯連接命令:
Cl /c dll_withlibAndH.cpp
Link /dll dll_withlibAndH.obj
在進行隱式調用的時候須要在客戶端引入頭文件,並在連接時指明dll對應的lib文件(dll只要有函數輸出,則連接的時候會產生一個與dll同名的lib文件)位置和名稱。而後如同調用api函數庫中的函數同樣調用dll中的函數,不須要顯式的LoadLibrary和GetProcAddress。使用最爲方便。客戶端代碼以下:dll_withlibAndH_client.cpp
#include "dll_withLibAndH.h"
//注意路徑,加載 dll的另外一種方法是 Project | setting | link 設置裏
#pragma comment(lib,"dll_withLibAndH.lib")
int main(void)
{
FuncInDll();//只要這樣咱們就能夠調用dll裏的函數了
return 0;
}
__declspec(dllexport)和__declspec(dllimport)配對使用
上面一種隱式調用的方法很不錯,可是在調用DLL中的對象和重載函數時會出現問題。由於使用extern 「C」修飾了輸出函數,所以重載函數確定是會出問題的,由於它們都將被編譯爲同一個輸出符號串(c語言是不支持重載的)。
事實上不使用extern 「C」是可行的,這時函數會被編譯爲c++符號串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ),當客戶端也是c++時,也能正確的隱式調用。
這時要考慮一個狀況:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函數,但同時DLL2也是一個DLL,也要輸出一些函數供Client.CPP使用。那麼在DLL2中如何聲明全部的函數,其中包含了從DLL1中引入的函數,還包括本身要輸出的函數。這個時候就須要同時使用__declspec(dllexport)和__declspec(dllimport)了。前者用來修飾本dll中的輸出函數,後者用來修飾從其它dll中引入的函數。
全部的源代碼包括DLL1.H,DLL1.CPP,DLL2.H,DLL2.CPP,Client.cpp。源代碼能夠在下載的包中找到。你能夠編譯連接並運行試試。
值得關注的是DLL1和DLL2中都使用的一個編碼方法,見DLL2.H
#ifdef DLL_DLL2_EXPORTS
#define DLL_DLL2_API __declspec(dllexport)
#else
#define DLL_DLL2_API __declspec(dllimport)
#endif
DLL_DLL2_API void FuncInDll2(void);
DLL_DLL2_API void FuncInDll2(int);
在頭文件中以這種方式定義宏DLL_DLL2_EXPORTS和DLL_DLL2_API,能夠確保DLL端的函數用__declspec(dllexport)修飾,而客戶端的函數用__declspec(dllimport)修飾。固然,記得在編譯dll時加上參數/D 「DLL_DLL2_EXPORTS」,或者乾脆就在dll的cpp文件第一行加上#define DLL_DLL2_EXPORTS。
VC生成的代碼也是這樣的!事實證實,我是抄襲它的,hoho!
DLL中的全局變量和對象
解決了重載函數的問題,那麼dll中的全局變量和對象都不是問題了,只是有一點語法須要注意。如源代碼所示:dll_object.h
#ifdef DLL_OBJECT_EXPORTS
#define DLL_OBJECT_API __declspec(dllexport)
#else
#define DLL_OBJECT_API __declspec(dllimport)
#endif
DLL_OBJECT_API void FuncInDll(void);
extern DLL_OBJECT_API int g_nDll;
class DLL_OBJECT_API CDll_Object {
public:
CDll_Object(void);
show(void);
// TODO: add your methods here.
};
Cpp文件dll_object.cpp以下:
#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream.h>
#include "dll_object.h"
DLL_OBJECT_API void FuncInDll(void)
{
cout<<"FuncInDll is called!"<<endl;
}
DLL_OBJECT_API int g_nDll = 9;
CDll_Object::CDll_Object()
{
cout<<"ctor of CDll_Object"<<endl;
}
CDll_Object::show()
{
cout<<"function show in class CDll_Object"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
g_hModule=NULL;
break;
}
return TRUE;
}
編譯連接完後Dumpbin一下,能夠看到輸出了5個符號:
1 0 00001040 ??0CDll_Object@@QAE@XZ
2 1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z
3 2 00001020 ?FuncInDll@@YAXXZ
4 3 00008040 ?g_nDll@@3HA
5 4 00001069 ?show@CDll_Object@@QAEHXZ
它們分別表明類CDll_Object,類的構造函數,FuncInDll函數,全局變量g_nDll和類的成員函數show。下面是客戶端代碼:dll_object_client.cpp
#include "dll_object.h"
#include <iostream.h>
//注意路徑,加載 dll的另外一種方法是 Project | setting | link 設置裏
#pragma comment(lib,"dll_object.lib")
int main(void)
{
cout<<"call dll"<<endl;
cout<<"call function in dll"<<endl;
FuncInDll();//只要這樣咱們就能夠調用dll裏的函數了
cout<<"global var in dll g_nDll ="<<g_nDll<<endl;
cout<<"call member function of class CDll_Object in dll"<<endl;
CDll_Object obj;
obj.show();
return 0;
}
運行這個客戶端能夠看到:
call dll
call function in dll
FuncInDll is called!
global var in dll g_nDll =9
call member function of class CDll_Object in dll
ctor of CDll_Object
function show in class CDll_Object
可知,在客戶端成功的訪問了dll中的全局變量,並建立了dll中定義的C++對象,還調用了該對象的成員函數。
中間的小結
牢記一點,說到底,DLL是對應C語言的動態連接技術,在輸出C函數和變量時顯得方便快捷;而在輸出C++類、函數時須要經過各類手段,並且也並無完美的解決方案,除非客戶端也是c++。
記住,只有COM是對應C++語言的技術。
下面開始對各各問題一一小結。
顯式調用和隱式調用
什麼時候使用顯式調用?什麼時候使用隱式調用?我認爲,只有一個時候使用顯式調用是合理的,就是當客戶端不是C/C++的時候。這時是沒法隱式調用的。例如用VB調用C++寫的dll。(VB我不會,因此沒有例子)(#add 隱式調用一次把全部函數都會導入,這也許是其缺點,因有時候並不須要所有導入。)
Def和__declspec(dllexport)
其實def的功能至關於extern 「C」 __declspec(dllexport),因此它也僅能處理C函數,而不能處理重載函數。而__declspec(dllexport)和__declspec(dllimport)配合使用可以適應任何狀況,所以__declspec(dllexport)是更爲先進的方法。因此,目前廣泛的見解是不使用def文件,我也贊成這個見解。
從其它語言調用DLL
從其它編程語言中調用DLL,有兩個最大的問題,第一個就是函數符號的問題,前面已經屢次提過了。這裏有個兩難選擇,若使用extern 「C」,則函數名稱保持不變,調用較方便,可是不支持函數重載等一系列c++功能;若不使用extern 「C」,則調用前要查看編譯後的符號,很是不方便。
第二個問題就是函數調用壓棧順序的問題,即__cdecl和__stdcall的問題。__cdecl是常規的C/C++調用約定,這種調用約定下,函數調用後棧的清理工做是由調用者完成的。__stdcall是標準的調用約定,即這些函數將在返回到調用者以前將參數從棧中刪除。
這兩個問題DLL都不能很好的解決,只能說湊合着用。可是在COM中,都獲得了完美的解決。因此,要在Windows平臺實現語言無關性,仍是隻有使用COM中間件。
總而言之,除非客戶端也使用C++,不然dll是不便於支持函數重載、類等c++特性的。DLL對c函數的支持很好,我想這也是爲何windows的函數庫使用C加dll實現的理由之一。
在VC中編寫DLL
在VC中建立、編譯、連接dll是很是方便的,點擊fileNewProjectWin32 Dynamic-Link Library,輸入dll名稱dll_InVC而後點擊肯定。而後選擇A DLL that export some symbols,點擊Finish。便可獲得一個完整的DLL。
附:DEF文件格式:
def文件的規則爲:
(1)LIBRARY語句說明.def文件相應的DLL;
(2)EXPORTS語句後列出要導出函數的名稱。能夠在.def文件中的導出函數名後加@n,表示要導出函數的序號爲n(在進行函數調用時,這個序號將發揮其做用);
(3).def 文件中的註釋由每一個註釋行開始處的分號 (;) 指定,且註釋不能與語句共享一行。
導出 definitions 的語法爲:
EXPORTS
entryname[=internalname] [@ordinal [NONAME]] [PRIVATE] [DATA]
entryname 是要導出的函數名或變量名.這是必選項.若是導出的名稱與 DLL 中的名稱不一樣,則經過 internalname 指定 DLL 中導出的名稱.例如,若是 DLL 導出函數 func1(),要將它用做 func2(),則應指定:
EXPORTS
func2=func1
@ordinal 容許指定是序號而不是函數名將進入 DLL 的導出表.這有助於最小化 DLL 的大小..LIB 文件將包含序號與函數之間的映射,這使您得以像一般在使用 DLL 的項目中那樣使用函數名.
可選的 NONAME 關鍵字容許只按序號導出,並減少結果 DLL 中導出表的大小.可是,若是要在 DLL 上使用 GetProcAddress,則必須知道序號,由於名稱將無效.
可選的 PRIVATE 關鍵字禁止將 entryname 放到由 LINK 生成的導入庫中.它對一樣是由 LINK 生成的圖像中的導出無效.
可選的 DATA 關鍵字指定導出的是數據,而不是代碼.例如,能夠導出數據變量,以下所示:
EXPORTS
i DATA
當對同一導出使用 PRIVATE 和 DATA 時,PRIVATE 必須位於 DATA 的前面.
DLL的種類
微軟的Visual C++支持三種DLL,它們分別是Non-MFC Dll(非MFC動態庫)、Regular Dll(常規DLL)、ExteNSion Dll(擴展DLL)。
一、Non-MFCDLL(非MFC動態庫)
這種動態連接庫指的是不用MFC的類庫結構,直接用C語言寫的DLL(#add DllMain可有,也可沒有),其導出的函數是標準的C接口,能被非MFC或MFC編寫的應用程序所調用。若是創建的DLL不須要使用MFC,那麼應該創建Non-MFCDLL,由於使用MFC會增大用戶庫的大小,從而浪費用戶的磁盤和內存空間。
二、RegularDLL(常規DLL)
這種動態連接庫和下述的ExtensionDll同樣,是用MFC類庫編寫的,它的一個明顯的特色是在源文件裏有一個繼承CWinApp的類(注意:此類DLL雖然從CWinApp派生,但沒有消息循環),被導出的函數是C函數、C++類或者C++成員函數(#add 應該有錯,常規dll只能導出c函數,不能導出C++類、成員函數、重載函數)(注意不要把術語C++類與MFC的微軟基礎C++類相混淆),調用常規DLL的應用程序沒必要是MFC應用程序,只要是能調用類C函數的應用程序就能夠,它們能夠是在VisualC++、Delphi、VisualBasic、BorlandC等編譯環境下利用DLL開發應用程序。常規DLL又可細分紅靜態連接到MFC和動態連接到MFC兩種:
(1)靜態鏈接到MFC的動態鏈接庫只被VC的專業般和企業版所支持。該類DLL裏的輸出函數能夠被任意Win32程序使用,包括使用MFC的應用程序。輸出函數有以下形式:
extern"C"EXPORTYourExportedFunction();
若是沒有extern"C"修飾,輸出函數僅僅能從C++代碼中調用。
(2)動態連接到MFC的常規DLL裏的輸出函數能夠被任意Win32程序使用,包括使用MFC的應用程序。全部從DLL輸出的函數應該以以下語句開始:
AFX_MANAGE_STATE(AfxGetStaticModuleState())
此語句用來正確地切換MFC模塊狀態。
三、ExtensionDll(擴展DLL)
這種動態連接庫是使用MFC的動態連接版本所建立的,而且它只被用MFC類庫所編寫的應用程序所調用。例如你已經建立了一個從MFC的CtoolBar類的派生類用於建立一個新的工具欄,爲了導出這個類,你必須把它放到一個MFC擴展的DLL中。擴展DLL和常規DLL不同,它沒有一個從CWinApp繼承而來的類的對象,因此,開發人員必須在DLL中的DllMain函數添加初始化代碼和結束代碼。與常規DLL相比,擴展的DLL有以下不一樣點:
1)它沒有一個從CWinApp派生的對象;
2)它必須有一個DLLMain函數;
3)DLLMain調用AfxInitExtensionModule函數,必須檢查該函數的返回值,若是返回0,DLLMmain也返回0;
4)若是它但願輸出CRuntimeClass類型的對象或者資源(Resources),則須要提供一個初始化函數來建立一個CDynLinkLibrary對象。而且,有必要把初始化函數輸出;
5)使用擴展DLL的MFC應用程序必須有一個從CWinApp派生的類,並且,通常在InitInstance裏調用擴展DLL的初始化函數。