基于
Visual C++6.0
的
DLL
編程實(shí)現(xiàn)
一、前言
自從微軟推出
16
位的
Windows
操作系統(tǒng)起,此后每種版本的
Windows
操作系統(tǒng)都非常依賴(lài)于動(dòng)態(tài)鏈接庫(kù)
(DLL)
中的函數(shù)和數(shù)據(jù),實(shí)際上
Windows
操作系統(tǒng)中幾乎所有的內(nèi)容都由
DLL
以一種或另外一種形式代表著,例如顯示的字體和圖標(biāo)存儲(chǔ)在
GDI DLL
中、顯示
Windows
桌面和處理用戶(hù)的輸入所需要的代碼被存儲(chǔ)在一個(gè)
User DLL
中、
Windows
編程所需要的大量的
API
函數(shù)也被包含在
Kernel DLL
中。
在
Windows
操作系統(tǒng)中使用
DLL
有很多優(yōu)點(diǎn),最主要的一點(diǎn)是多個(gè)應(yīng)用程序、甚至是不同語(yǔ)言編寫(xiě)的應(yīng)用程序可以共享一個(gè)
DLL
文件,真正實(shí)現(xiàn)了資源
"
共享
"
,大大縮小了應(yīng)用程序的執(zhí)行代碼,更加有效的利用了內(nèi)存;使用
DLL
的另一個(gè)優(yōu)點(diǎn)是
DLL
文件作為一個(gè)單獨(dú)的程序模塊,封裝性、獨(dú)立性好,在軟件需要升級(jí)的時(shí)候,開(kāi)發(fā)人員只需要修改相應(yīng)的
DLL
文件就可以了,而且,當(dāng)
DLL
中的函數(shù)改變后,只要不是參數(shù)的改變
,
程序代碼并不需要重新編譯。這在編程時(shí)十分有用,大大提高了軟件開(kāi)發(fā)和維護(hù)的效率。
既然
DLL
那么重要,所以搞清楚什么是
DLL
、如何在
Windows
操作系統(tǒng)中開(kāi)發(fā)使用
DLL
是程序開(kāi)發(fā)人員不得不解決的一個(gè)問(wèn)題。本文針對(duì)這些問(wèn)題,通過(guò)一個(gè)簡(jiǎn)單的例子,即在一個(gè)
DLL
中實(shí)現(xiàn)比較最大、最小整數(shù)這兩個(gè)簡(jiǎn)單函數(shù),全面地解析了在
Visual C++
編譯環(huán)境下編程實(shí)現(xiàn)
DLL
的過(guò)程,文章中所用到的程序代碼在
Windows98
系統(tǒng)、
Visual C++6.0
編譯環(huán)境下通過(guò)。
二、
DLL
的概念
DLL
是建立在客戶(hù)
/
服務(wù)器通信的概念上,包含若干函數(shù)、類(lèi)或資源的庫(kù)文件,函數(shù)和數(shù)據(jù)被存儲(chǔ)在一個(gè)
DLL
(服務(wù)器)上并由一個(gè)或多個(gè)客戶(hù)導(dǎo)出而使用,這些客戶(hù)可以是應(yīng)用程序或者是其它的
DLL
。
DLL
庫(kù)不同于靜態(tài)庫(kù),在靜態(tài)庫(kù)情況下,函數(shù)和數(shù)據(jù)被編譯進(jìn)一個(gè)二進(jìn)制文件(通常擴(kuò)展名為
*.LIB
),
Visual C++
的編譯器在處理程序代碼時(shí)將從靜態(tài)庫(kù)中恢復(fù)這些函數(shù)和數(shù)據(jù)并把他們和應(yīng)用程序中的其他模塊組合在一起生成可執(zhí)行文件。這個(gè)過(guò)程稱(chēng)為
"
靜態(tài)鏈接
"
,此時(shí)因?yàn)閼?yīng)用程序所需的全部?jī)?nèi)容都是從庫(kù)中復(fù)制了出來(lái),所以靜態(tài)庫(kù)本身并不需要與可執(zhí)行文件一起發(fā)行。
在動(dòng)態(tài)庫(kù)的情況下,有兩個(gè)文件,一個(gè)是引入庫(kù)(
.LIB
)文件,一個(gè)是
DLL
文件,引入庫(kù)文件包含被
DLL
導(dǎo)出的函數(shù)的名稱(chēng)和位置,
DLL
包含實(shí)際的函數(shù)和數(shù)據(jù),應(yīng)用程序使用
LIB
文件鏈接到所需要使用的
DLL
文件,庫(kù)中的函數(shù)和數(shù)據(jù)并不復(fù)制到可執(zhí)行文件中,因此在應(yīng)用程序的可執(zhí)行文件中,存放的不是被調(diào)用的函數(shù)代碼,而是
DLL
中所要調(diào)用的函數(shù)的內(nèi)存地址,這樣當(dāng)一個(gè)或多個(gè)應(yīng)用程序運(yùn)行是再把程序代碼和被調(diào)用的函數(shù)代碼鏈接起來(lái),從而節(jié)省了內(nèi)存資源。從上面的說(shuō)明可以看出,
DLL
和
.LIB
文件必須隨應(yīng)用程序一起發(fā)行,否則應(yīng)用程序?qū)?huì)產(chǎn)生錯(cuò)誤。
微軟的
Visual C++
支持三種
DLL
,它們分別是
Non-MFC Dll
(非
MFC
動(dòng)態(tài)庫(kù))、
Regular Dll
(常規(guī)
DLL
)、
Extension Dll
(擴(kuò)展
DLL
)。
Non-MFC DLL
指的是不用
MFC
的類(lèi)庫(kù)結(jié)構(gòu),直接用
C
語(yǔ)言寫(xiě)的
DLL
,其導(dǎo)出的函數(shù)是標(biāo)準(zhǔn)的
C
接口,能被非
MFC
或
MFC
編寫(xiě)的應(yīng)用程序所調(diào)用。
Regular DLL:
和下述的
Extension Dlls
一樣,是用
MFC
類(lèi)庫(kù)編寫(xiě)的,它的一個(gè)明顯的特點(diǎn)是在源文件里有一個(gè)繼承
CWinApp
的類(lèi)(注意:此類(lèi)
DLL
雖然從
CWinApp
派生,但沒(méi)有消息循環(huán))
,
被導(dǎo)出的函數(shù)是
C
函數(shù)、
C++
類(lèi)或者
C++
成員函數(shù)(注意不要把術(shù)語(yǔ)
C++
類(lèi)與
MFC
的微軟基礎(chǔ)
C++
類(lèi)相混淆),調(diào)用常規(guī)
DLL
的應(yīng)用程序不必是
MFC
應(yīng)用程序,只要是能調(diào)用類(lèi)
C
函數(shù)的應(yīng)用程序就可以,它們可以是在
Visual C++
、
Dephi
、
Visual Basic
、
Borland C
等編譯環(huán)境下利用
DLL
開(kāi)發(fā)應(yīng)用程序。
常規(guī)
DLL
又可細(xì)分成靜態(tài)鏈接到
MFC
和動(dòng)態(tài)鏈接到
MFC
上的,這兩種常規(guī)
DLL
的區(qū)別將在下面介紹。與常規(guī)
DLL
相比,使用擴(kuò)展
DLL
用于導(dǎo)出增強(qiáng)
MFC
基礎(chǔ)類(lèi)的函數(shù)或子類(lèi),用這種類(lèi)型的動(dòng)態(tài)鏈接庫(kù),可以用來(lái)輸出一個(gè)從
MFC
所繼承下來(lái)的類(lèi)。
擴(kuò)展
DLL
是使用
MFC
的動(dòng)態(tài)鏈接版本所創(chuàng)建的,并且它只被用
MFC
類(lèi)庫(kù)所編寫(xiě)的應(yīng)用程序所調(diào)用。例如你已經(jīng)創(chuàng)建了一個(gè)從
MFC
的
CtoolBar
類(lèi)的派生類(lèi)用于創(chuàng)建一個(gè)新的工具欄,為了導(dǎo)出這個(gè)類(lèi),你必須把它放到一個(gè)
MFC
擴(kuò)展的
DLL
中。擴(kuò)展
DLL
和常規(guī)
DLL
不一樣,它沒(méi)有一個(gè)從
CWinApp
繼承而來(lái)的類(lèi)的對(duì)象,所以,開(kāi)發(fā)人員必須在
DLL
中的
DllMain
函數(shù)添加初始化代碼和結(jié)束代碼。
三、動(dòng)態(tài)鏈接庫(kù)的創(chuàng)建
在
Visual C++6.0
開(kāi)發(fā)環(huán)境下,打開(kāi)
FileNewProject
選項(xiàng),可以選擇
Win32 Dynamic-Link Library
或
MFC AppWizard[dll]
來(lái)以不同的方式來(lái)創(chuàng)建
Non-MFC Dll
、
Regular Dll
、
Extension Dll
等不同種類(lèi)的動(dòng)態(tài)鏈接庫(kù)。
1
.
Win32 Dynamic-Link Library
方式創(chuàng)建
Non-MFC DLL
動(dòng)態(tài)鏈接庫(kù)
每一個(gè)
DLL
必須有一個(gè)入口點(diǎn),這就象我們用
C
編寫(xiě)的應(yīng)用程序一樣,必須有一個(gè)
WINMAIN
函數(shù)一樣。在
Non-MFC DLL
中
DllMain
是一個(gè)缺省的入口函數(shù),你不需要編寫(xiě)自己的
DLL
入口函數(shù),用這個(gè)缺省的入口函數(shù)就能使動(dòng)態(tài)鏈接庫(kù)被調(diào)用時(shí)得到正確的初始化。如果應(yīng)用程序的
DLL
需要分配額外的內(nèi)存或資源時(shí),或者說(shuō)需要對(duì)每個(gè)進(jìn)程或線程初始化和清除操作時(shí),需要在相應(yīng)的
DLL
工程的
.CPP
文件中對(duì)
DllMain()
函數(shù)按照下面的格式書(shū)寫(xiě)。
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved) { switch( ul_reason_for_call ) { case DLL_PROCESS_ATTACH: ....... case DLL_THREAD_ATTACH: ....... case DLL_THREAD_DETACH: ....... case DLL_PROCESS_DETACH: ....... } return TRUE; }
|
參數(shù)中,
hMoudle
是動(dòng)態(tài)庫(kù)被調(diào)用時(shí)所傳遞來(lái)的一個(gè)指向自己的句柄
(
實(shí)際上,它是指向
_DGROUP
段的一個(gè)選擇符
)
;
ul_reason_for_call
是一個(gè)說(shuō)明動(dòng)態(tài)庫(kù)被調(diào)原因的標(biāo)志,當(dāng)進(jìn)程或線程裝入或卸載動(dòng)態(tài)鏈接庫(kù)的時(shí)候,操作系統(tǒng)調(diào)用入口函數(shù),并說(shuō)明動(dòng)態(tài)鏈接庫(kù)被調(diào)用的原因,它所有的可能值為:
DLL_PROCESS_ATTACH:
進(jìn)程被調(diào)用、
DLL_THREAD_ATTACH:
線程被調(diào)用、
DLL_PROCESS_DETACH:
進(jìn)程被停止、
DLL_THREAD_DETACH:
線程被停止;
lpReserved
為保留參數(shù)。到此為止,
DLL
的入口函數(shù)已經(jīng)寫(xiě)了,剩下部分的實(shí)現(xiàn)也不難,你可以在
DLL
工程中加入你所想要輸出的函數(shù)或變量了。
我們已經(jīng)知道
DLL
是包含若干個(gè)函數(shù)的庫(kù)文件,應(yīng)用程序使用
DLL
中的函數(shù)之前,應(yīng)該先導(dǎo)出這些函數(shù),以便供給應(yīng)用程序使用。要導(dǎo)出這些函數(shù)有兩種方法,一是在定義函數(shù)時(shí)使用導(dǎo)出關(guān)鍵字
_declspec(dllexport)
,另外一種方法是在創(chuàng)建
DLL
文件時(shí)使用模塊定義文件
.Def
。需要讀者注意的是在使用第一種方法的時(shí)候,不能使用
DEF
文件。下面通過(guò)兩個(gè)例子來(lái)說(shuō)明如何使用這兩種方法創(chuàng)建
DLL
文件。
1
)使用導(dǎo)出函數(shù)關(guān)鍵字
_declspec(dllexport)
創(chuàng)建
MyDll.dll
,該動(dòng)態(tài)鏈接庫(kù)中有兩個(gè)函數(shù),分別用來(lái)實(shí)現(xiàn)得到兩個(gè)數(shù)的最大和最小數(shù)。在
MyDll.h
和
MyDLL.cpp
文件中分別輸入如下原代碼:
//MyDLL.h extern "C" _declspec(dllexport) int Max(int a, int b); extern "C" _declspec(dllexport) int Min(int a, int b); //MyDll.cpp #i nclude #i nclude"MyDll.h" int Max(int a, int b) { if(a>=b)return a; else return b; } int Min(int a, int b) { if(a>=b)return b; else return a; }
|
該動(dòng)態(tài)鏈接庫(kù)編譯成功后,打開(kāi)
MyDll
工程中的
debug
目錄,可以看到
MyDll.dll
、
MyDll.lib
兩個(gè)文件。
LIB
文件中包含
DLL
文件名和
DLL
文件中的函數(shù)名等,該
LIB
文件只是對(duì)應(yīng)該
DLL
文件的
"
映像文件
"
,與
DLL
文件中,
LIB
文件的長(zhǎng)度要小的多,在進(jìn)行隱式鏈接
DLL
時(shí)要用到它。讀者可能已經(jīng)注意到在
MyDll.h
中有關(guān)鍵字
"extern C"
,它可以使其他編程語(yǔ)言訪問(wèn)你編寫(xiě)的
DLL
中的函數(shù)。
2
)用
.def
文件創(chuàng)建工程
MyDll
為了用
.def
文件創(chuàng)建
DLL
,請(qǐng)先刪除上個(gè)例子創(chuàng)建的工程中的
MyDll.h
文件,保留
MyDll.cpp
并在該文件頭刪除
#i nclude MyDll.h
語(yǔ)句,同時(shí)往該工程中加入一個(gè)文本文件,命名為
MyDll.def
,再在該文件中加入如下代碼:
LIBRARY MyDll
EXPORTS
Max
Min
其中
LIBRARY
語(yǔ)句說(shuō)明該
def
文件是屬于相應(yīng)
DLL
的,
EXPORTS
語(yǔ)句下列出要導(dǎo)出的函數(shù)名稱(chēng)。我們可以在
.def
文件中的導(dǎo)出函數(shù)后加
@n
,如
Max@1
,
Min@2
,表示要導(dǎo)出的函數(shù)順序號(hào),在進(jìn)行顯式連時(shí)可以用到它。該
DLL
編譯成功后,打開(kāi)工程中的
Debug
目錄,同樣也會(huì)看到
MyDll.dll
和
MyDll.lib
文件。
2
.
MFC AppWizard[dll]
方式生成常規(guī)
/
擴(kuò)展
DLL
在
MFC AppWizard[dll]
下生成
DLL
文件又有三種方式,在創(chuàng)建
DLL
是,要根據(jù)實(shí)際情況選擇創(chuàng)建
DLL
的方式。一種是常規(guī)
DLL
靜態(tài)鏈接到
MFC
,另一種是常規(guī)
DLL
動(dòng)態(tài)鏈接到
MFC
。兩者的區(qū)別是:前者使用的是
MFC
的靜態(tài)鏈接庫(kù),生成的
DLL
文件長(zhǎng)度大,一般不使用這種方式,后者使用
MFC
的動(dòng)態(tài)鏈接庫(kù),生成的
DLL
文件長(zhǎng)度小;動(dòng)態(tài)鏈接到
MFC
的規(guī)則
DLL
所有輸出的函數(shù)應(yīng)該以如下語(yǔ)句開(kāi)始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( )) //
此語(yǔ)句用來(lái)正確地切換
MFC
模塊狀態(tài)
|
最后一種是
MFC
擴(kuò)展
DLL
,這種
DLL
特點(diǎn)是用來(lái)建立
MFC
的派生類(lèi),
Dll
只被用
MFC
類(lèi)庫(kù)所編寫(xiě)的應(yīng)用程序所調(diào)用。前面我們已經(jīng)介紹過(guò),
Extension DLLs
和
Regular DLLs
不一樣,它沒(méi)有一個(gè)從
CWinApp
繼承而來(lái)的類(lèi)的對(duì)象,編譯器默認(rèn)了一個(gè)
DLL
入口函數(shù)
DLLMain()
作為對(duì)
DLL
的初始化,你可以在此函數(shù)中實(shí)現(xiàn)初始化
,
代碼如下:
BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll
,
DWORD reason
,
LPVOID flmpload) { switch(reason) { ……………//
初始化代碼;
} return true; }
|
參數(shù)
hinstDll
存放
DLL
的句柄,參數(shù)
reason
指明調(diào)用函數(shù)的原因,
lpReserved
是一個(gè)被系統(tǒng)所保留的參數(shù)。對(duì)于隱式鏈接是一個(gè)非零值,對(duì)于顯式鏈接值是零。
在
MFC
下建立
DLL
文件,會(huì)自動(dòng)生成
def
文件框架,其它與建立傳統(tǒng)的
Non-MFC DLL
沒(méi)有什么區(qū)別,只要在相應(yīng)的頭文件寫(xiě)入關(guān)鍵字
_declspec(dllexport)
函數(shù)類(lèi)型和函數(shù)名等,或在生成的
def
文件中
EXPORTS
下輸入函數(shù)名就可以了。需要注意的是在向其它開(kāi)發(fā)人員分發(fā)
MFC
擴(kuò)展
DLL
時(shí),不要忘記提供描述
DLL
中類(lèi)的頭文件以及相應(yīng)的
.LIB
文件和
DLL
本身,此后開(kāi)發(fā)人員就能充分利用你開(kāi)發(fā)的擴(kuò)展
DLL
了。
應(yīng)用程序使用DLL可以采用兩種方式:一種是隱式鏈接,另一種是顯式鏈接。在使用DLL之前首先要知道DLL中函數(shù)的結(jié)構(gòu)信息。Visual C++6.0在VCin目錄下提供了一個(gè)名為Dumpbin.exe的小程序,用它可以查看DLL文件中的函數(shù)結(jié)構(gòu)。另外,Windows系統(tǒng)將遵循下面的搜索順序來(lái)定位DLL: 1.包含EXE文件的目錄,2.進(jìn)程的當(dāng)前工作目錄, 3.Windows系統(tǒng)目錄, 4.Windows目錄,5.列在Path環(huán)境變量中的一系列目錄。
1.隱式鏈接
隱式鏈接就是在程序開(kāi)始執(zhí)行時(shí)就將DLL文件加載到應(yīng)用程序當(dāng)中。實(shí)現(xiàn)隱式鏈接很容易,只要將導(dǎo)入函數(shù)關(guān)鍵字_declspec(dllimport)函數(shù)名等寫(xiě)到應(yīng)用程序相應(yīng)的頭文件中就可以了。下面的例子通過(guò)隱式鏈接調(diào)用MyDll.dll庫(kù)中的Min函數(shù)。首先生成一個(gè)項(xiàng)目為TestDll,在DllTest.h、DllTest.cpp文件中分別輸入如下代碼:
//Dlltest.h #pragma comment(lib
,
"MyDll.lib") extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b); //TestDll.cpp #i nclude #i nclude"Dlltest.h" void main() {int a; a=min(8,10) printf("
比較的結(jié)果為
%d "
,
a); }
|
在創(chuàng)建
DllTest.exe
文件之前,要先將
MyDll.dll
和
MyDll.lib
拷貝到當(dāng)前工程所在的目錄下面,也可以拷貝到
windows
的
System
目錄下。如果
DLL
使用的是
def
文件,要?jiǎng)h除
TestDll.h
文件中關(guān)鍵字
extern "C"
。
TestDll.h
文件中的關(guān)鍵字
Progam commit
是要
Visual C+
的編譯器在
link
時(shí),鏈接到
MyDll.lib
文件,當(dāng)然,開(kāi)發(fā)人員也可以不使用
#pragma comment(lib
,
"MyDll.lib")
語(yǔ)句,而直接在工程的
Setting->Link
頁(yè)的
Object/Moduls
欄填入
MyDll.lib
既可。
2
.顯式鏈接
顯式鏈接是應(yīng)用程序在執(zhí)行過(guò)程中隨時(shí)可以加載
DLL
文件,也可以隨時(shí)卸載
DLL
文件,這是隱式鏈接所無(wú)法作到的,所以顯式鏈接具有更好的靈活性,對(duì)于解釋性語(yǔ)言更為合適。不過(guò)實(shí)現(xiàn)顯式鏈接要麻煩一些。在應(yīng)用程序中用
LoadLibrary
或
MFC
提供的
AfxLoadLibrary
顯式的將自己所做的動(dòng)態(tài)鏈接庫(kù)調(diào)進(jìn)來(lái),動(dòng)態(tài)鏈接庫(kù)的文件名即是上述兩個(gè)函數(shù)的參數(shù),此后再用
GetProcAddress()
獲取想要引入的函數(shù)。自此,你就可以象使用如同在應(yīng)用程序自定義的函數(shù)一樣來(lái)調(diào)用此引入函數(shù)了。在應(yīng)用程序退出之前,應(yīng)該用
FreeLibrary
或
MFC
提供的
AfxFreeLibrary
釋放動(dòng)態(tài)鏈接庫(kù)。下面是通過(guò)顯式鏈接調(diào)用
DLL
中的
Max
函數(shù)的例子。
#i nclude #i nclude void main(void) { typedef int(*pMax)(int a,int b); typedef int(*pMin)(int a,int b); HINSTANCE hDLL; PMax Max HDLL=LoadLibrary("MyDll.dll");//
加載動(dòng)態(tài)鏈接庫(kù)
MyDll.dll
文件;
Max=(pMax)GetProcAddress(hDLL,"Max"); A=Max(5,8); Printf("
比較的結(jié)果為
%d "
,
a); FreeLibrary(hDLL);//
卸載
MyDll.dll
文件;
}
|
在上例中使用類(lèi)型定義關(guān)鍵字
typedef
,定義指向和
DLL
中相同的函數(shù)原型指針,然后通過(guò)
LoadLibray()
將
DLL
加載到當(dāng)前的應(yīng)用程序中并返回當(dāng)前
DLL
文件的句柄,然后通過(guò)
GetProcAddress()
函數(shù)獲取導(dǎo)入到應(yīng)用程序中的函數(shù)指針,函數(shù)調(diào)用完畢后,使用
FreeLibrary()
卸載
DLL
文件。在編譯程序之前,首先要將
DLL
文件拷貝到工程所在的目錄或
Windows
系統(tǒng)目錄下。
使用顯式鏈接應(yīng)用程序編譯時(shí)不需要使用相應(yīng)的
Lib
文件。另外,使用
GetProcAddress()
函數(shù)時(shí),可以利用
MAKEINTRESOURCE()
函數(shù)直接使用
DLL
中函數(shù)出現(xiàn)的順序號(hào),如將
GetProcAddress(hDLL,"Min")
改為
GetProcAddress(hDLL, MAKEINTRESOURCE(2))
(函數(shù)
Min()
在
DLL
中的順序號(hào)是
2
),這樣調(diào)用
DLL
中的函數(shù)速度很快,但是要記住函數(shù)的使用序號(hào),否則會(huì)發(fā)生錯(cuò)誤。