|
【轉(zhuǎn)原文地址】http://www./Lee7/archive/2008/07/07/55543.html 1.概論 先來闡述一下DLL(Dynamic Linkable Library)的概念,你可以簡(jiǎn)單的把DLL看成一種倉庫,它提供給你一些可以直接拿來用的變量、函數(shù)或類。在倉庫的發(fā)展史上經(jīng)歷了“無庫-靜態(tài)鏈接庫-動(dòng)態(tài)鏈接庫”的時(shí)代。 靜態(tài)鏈接庫與動(dòng)態(tài)鏈接庫都是共享代碼的方式,如果采用靜態(tài)鏈接庫,則無論你愿不愿意,lib中的指令都被直接包含在最終生成的EXE文件中了。但是若使用DLL,該DLL不必被包含在最終EXE文件中,EXE文件執(zhí)行時(shí)可以“動(dòng)態(tài)”地引用和卸載這個(gè)與EXE獨(dú)立的DLL文件。靜態(tài)鏈接庫和動(dòng)態(tài)鏈接庫的另外一個(gè)區(qū)別在于靜態(tài)鏈接庫中不能再包含其他的動(dòng)態(tài)鏈接庫或者靜態(tài)庫,而在動(dòng)態(tài)鏈接庫中還可以再包含其他的動(dòng)態(tài)或靜態(tài)鏈接庫。 對(duì)動(dòng)態(tài)鏈接庫,我們還需建立如下概念: (1)DLL 的編制與具體的編程語言及編譯器無關(guān) 只要遵循約定的DLL接口規(guī)范和調(diào)用方式,用各種語言編寫的DLL都可以相互調(diào)用。譬如Windows提供的系統(tǒng)DLL(其中包括了Windows的API),在任何開發(fā)環(huán)境中都能被調(diào)用,不在乎其是Visual Basic、Visual C++還是Delphi。 ?。?)動(dòng)態(tài)鏈接庫隨處可見 我們?cè)赪indows目錄下的system32文件夾中會(huì)看到kernel32.dll、user32.dll和gdi32.dll,windows的大多數(shù)API都包含在這些DLL中。kernel32.dll中的函數(shù)主要處理內(nèi)存管理和進(jìn)程調(diào)度;user32.dll中的函數(shù)主要控制用戶界面;gdi32.dll中的函數(shù)則負(fù)責(zé)圖形方面的操作。 一般的程序員都用過類似MessageBox的函數(shù),其實(shí)它就包含在user32.dll這個(gè)動(dòng)態(tài)鏈接庫中。由此可見DLL對(duì)我們來說其實(shí)并不陌生。 (3)VC動(dòng)態(tài)鏈接庫的分類 Visual C++支持三種DLL,它們分別是Non-MFC DLL(非MFC動(dòng)態(tài)庫)、MFC Regular DLL(MFC規(guī)則DLL)、MFC Extension DLL(MFC擴(kuò)展DLL)。 非MFC動(dòng)態(tài)庫不采用MFC類庫結(jié)構(gòu),其導(dǎo)出函數(shù)為標(biāo)準(zhǔn)的C接口,能被非MFC或MFC編寫的應(yīng)用程序所調(diào)用; MFC規(guī)則DLL 包含一個(gè)繼承自CWinApp的類,但其無消息循環(huán); MFC擴(kuò)展DLL采用MFC的動(dòng)態(tài)鏈接版本創(chuàng)建,它只能被用MFC類庫所編寫的應(yīng)用程序所調(diào)用。 由于本文篇幅較長(zhǎng),內(nèi)容較多,勢(shì)必需要先對(duì)閱讀本文的有關(guān)事項(xiàng)進(jìn)行說明,下面以問答形式給出。 問:本文主要講解什么內(nèi)容? 答:本文詳細(xì)介紹了DLL編程的方方面面,努力學(xué)完本文應(yīng)可以對(duì)DLL有較全面的掌握,并能編寫大多數(shù)DLL程序。 問:如何看本文? 答:本文每一個(gè)主題的講解都附帶了源代碼例程,可以隨文下載(每個(gè)工程都經(jīng)WINRAR壓縮)。所有這些例程都由筆者編寫并在VC++6.0中調(diào)試通過。 當(dāng)然看懂本文不是讀者的最終目的,讀者應(yīng)親自動(dòng)手實(shí)踐才能真正掌握DLL的奧妙。 問:學(xué)習(xí)本文需要什么樣的基礎(chǔ)知識(shí)? 答:如果你掌握了C,并大致掌握了C++,了解一點(diǎn)MFC的知識(shí),就可以輕松地看懂本文。 2.靜態(tài)鏈接庫 對(duì)靜態(tài)鏈接庫的講解不是本文的重點(diǎn),但是在具體講解DLL之前,通過一個(gè)靜態(tài)鏈接庫的例子可以快速地幫助我們建立“庫”的概念。
圖1 建立一個(gè)靜態(tài)鏈接庫 如圖1,在VC++6.0中new一個(gè)名稱為libTest的static library工程(單擊此處下載本工程附件),并新建lib.h和lib.cpp兩個(gè)文件,lib.h和lib.cpp的源代碼如下: //文件:lib.h #ifndef LIB_H #define LIB_H extern "C" int add(int x,int y); //聲明為C編譯、連接方式的外部函數(shù) #endif![]() //文件:lib.cpp #include "lib.h" int add(int x,int y) { return x + y; }編譯這個(gè)工程就得到了一個(gè).lib文件,這個(gè)文件就是一個(gè)函數(shù)庫,它提供了add的功能。將頭文件和.lib文件提交給用戶后,用戶就可以直接使用其中的add函數(shù)了。 #include <stdio.h> #include "..\lib.h" #pragma comment( lib, "..\\debug\\libTest.lib" ) //指定與靜態(tài)庫一起連接 int main(int argc, char* argv[]) { printf( "2 + 3 = %d", add( 2, 3 ) ); }如果不用#pragma comment指定,則可以直接在VC++中設(shè)置,如圖2,依次選擇tools、options、directories、library files菜單或選項(xiàng),填入庫文件路徑。圖2中加紅圈的部分為我們添加的libTest.lib文件的路徑。
圖2 在VC中設(shè)置庫文件路徑 這個(gè)靜態(tài)鏈接庫的例子至少讓我們明白了庫函數(shù)是怎么回事,它們是哪來的。我們現(xiàn)在有下列模糊認(rèn)識(shí)了:
圖3 庫的調(diào)試與“運(yùn)行” 通常有比上述做法更好的調(diào)試途徑,那就是將庫工程和應(yīng)用工程(調(diào)用庫的工程)放置在同一VC工作區(qū),只對(duì)應(yīng)用工程進(jìn)行調(diào)試,在應(yīng)用工程調(diào)用庫中函數(shù)的語句處設(shè)置斷點(diǎn),執(zhí)行后按下F11,這樣就單步進(jìn)入了庫中的函數(shù)。第2節(jié)中的libTest和libCall工程就放在了同一工作區(qū),其工程結(jié)構(gòu)如圖4所示。
圖4 把庫工程和調(diào)用庫的工程放入同一工作區(qū)進(jìn)行調(diào)試 上述調(diào)試方法對(duì)靜態(tài)鏈接庫和動(dòng)態(tài)鏈接庫而言是一致的。所以本文提供下載的所有源代碼中都包含了庫工程和調(diào)用庫的工程,這二者都被包含在一個(gè)工作區(qū)內(nèi),這是筆者提供這種打包下載的用意所在。
圖5 用Depends查看DLL 當(dāng)然Depends工具也可以顯示DLL的層次結(jié)構(gòu),若用它打開一個(gè)可執(zhí)行文件則可以看出這個(gè)可執(zhí)行文件調(diào)用了哪些DLL。
圖6 建立一個(gè)非MFC DLL 在建立的工程中添加lib.h及l(fā)ib.cpp文件,源代碼如下:
/* 文件名:lib.h */ #ifndef LIB_H #define LIB_H extern "C" int __declspec(dllexport)add(int x, int y); #endif![]() /* 文件名:lib.cpp */ #include "lib.h"![]() int add(int x, int y) { return x + y; } #include <stdio.h> #include <windows.h> typedef int(*lpAddFun)(int, int); //宏定義函數(shù)指針類型![]() int main(int argc, char *argv[]) { HINSTANCE hDll; //DLL句柄 lpAddFun addFun; //函數(shù)指針 hDll = LoadLibrary("..\\Debug\\dllTest.dll"); if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, "add"); if (addFun != NULL) { int result = addFun(2, 3); printf("%d", result); } FreeLibrary(hDll); } return 0; }(1)DLL導(dǎo)出函數(shù),可供應(yīng)用程序調(diào)用; (2) DLL內(nèi)部函數(shù),只能在DLL程序使用,應(yīng)用程序無法調(diào)用它們。 而應(yīng)用程序?qū)Ρ綝LL的調(diào)用和對(duì)第2節(jié)靜態(tài)鏈接庫的調(diào)用卻有較大差異,下面我們來逐一分析。 首先,語句typedef int ( * lpAddFun)(int,int)定義了一個(gè)與add函數(shù)接受參數(shù)類型和返回值均相同的函數(shù)指針類型。隨后,在main函數(shù)中定義了lpAddFun的實(shí)例addFun; 其次,在函數(shù)main中定義了一個(gè)DLL HINSTANCE句柄實(shí)例hDll,通過Win32 Api函數(shù)LoadLibrary動(dòng)態(tài)加載了DLL模塊并將DLL模塊句柄賦給了hDll; 再次,在函數(shù)main中通過Win32 Api函數(shù)GetProcAddress得到了所加載DLL模塊中函數(shù)add的地址并賦給了addFun。經(jīng)由函數(shù)指針addFun進(jìn)行了對(duì)DLL中add函數(shù)的調(diào)用; 最后,應(yīng)用工程使用完DLL后,在函數(shù)main中通過Win32 Api函數(shù)FreeLibrary釋放了已經(jīng)加載的DLL模塊。 通過這個(gè)簡(jiǎn)單的例子,我們獲知DLL定義和調(diào)用的一般概念: (1)DLL中需以某種特定的方式聲明導(dǎo)出函數(shù)(或變量、類); (2)應(yīng)用工程需以某種特定的方式調(diào)用DLL的導(dǎo)出函數(shù)(或變量、類)。 下面我們來對(duì)“特定的方式進(jìn)行”闡述。 4.2 聲明導(dǎo)出函數(shù) ; lib.def : 導(dǎo)出DLL函數(shù)![]() LIBRARY dllTest![]() EXPORTS![]() add @ 1.def文件的規(guī)則為: #pragma comment(lib,"dllTest.lib") ![]() //.lib文件中僅僅是關(guān)于其對(duì)應(yīng)DLL文件中函數(shù)的重定位信息![]() extern "C" __declspec(dllimport) add(int x,int y); ![]() int main(int argc, char* argv[]) { int result = add(2,3); printf("%d",result); return 0; }
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: printf("\nprocess attach of dll"); break; case DLL_THREAD_ATTACH: printf("\nthread attach of dll"); break; case DLL_THREAD_DETACH: printf("\nthread detach of dll"); break; case DLL_PROCESS_DETACH: printf("\nprocess detach of dll"); break; } return TRUE; } DllMain函數(shù)在DLL被加載和卸載時(shí)被調(diào)用,在單個(gè)線程啟動(dòng)和終止時(shí),DLLMain函數(shù)也被調(diào)用,ul_reason_for_call指明了被調(diào)用的原因。原因共有4種,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和THREAD_DETACH,以switch語句列出。 hDll = LoadLibrary("..\\Debug\\dllTest.dll"); if (hDll != NULL) { addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1)); //MAKEINTRESOURCE直接使用導(dǎo)出文件中的序號(hào) if (addFun != NULL) { int result = addFun(2, 3); printf("\ncall add in dll:%d", result); } FreeLibrary(hDll); }![]() 我們看到輸出順序?yàn)椋?br> process attach of dll #define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i))) #define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))![]() #ifdef UNICODE #define MAKEINTRESOURCE MAKEINTRESOURCEW #else #define MAKEINTRESOURCE MAKEINTRESOURCEA
#define CALLBACK __stdcall //這就是傳說中的回調(diào)函數(shù) #define WINAPI __stdcall //這就是傳說中的WINAPI #define WINAPIV __cdecl #define APIENTRY WINAPI //DllMain的入口就在這里 #define APIPRIVATE __stdcall #define PASCAL __stdcall在lib.h中,應(yīng)這樣聲明add函數(shù): int __stdcall add(int x, int y);在應(yīng)用工程中函數(shù)指針類型應(yīng)定義為: typedef int(__stdcall *lpAddFun)(int, int); 若在lib.h中將函數(shù)聲明為__stdcall調(diào)用,而應(yīng)用工程中仍使用typedef int (* lpAddFun)(int,int),運(yùn)行時(shí)將發(fā)生錯(cuò)誤(因?yàn)轭愋筒黄ヅ?,在?yīng)用工程中仍然是缺省的__cdecl調(diào)用),彈出如圖7所示的對(duì)話框。
圖7 調(diào)用約定不匹配時(shí)的運(yùn)行錯(cuò)誤 圖8中的那段話實(shí)際上已經(jīng)給出了錯(cuò)誤的原因,即“This is usually a result of …”。 /* 文件名:lib.h */ #ifndef LIB_H #define LIB_H extern int dllGlobalVar; #endif![]() /* 文件名:lib.cpp */![]() #include "lib.h" #include <windows.h>![]() int dllGlobalVar;![]() BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: dllGlobalVar = 100; //在dll被加載時(shí),賦全局變量為100 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }![]() ![]() ;文件名:lib.def ;在DLL中導(dǎo)出變量 LIBRARY "dllTest" EXPORTS dllGlobalVar CONSTANT ;或dllGlobalVar DATA GetGlobalVar 變量名 CONSTANT //過時(shí)的方法 變量名 DATA //VC++提示的新方法 #include <stdio.h> #pragma comment(lib,"dllTest.lib") extern int dllGlobalVar;![]() int main(int argc, char *argv[]) { printf("%d ", *(int*)dllGlobalVar); *(int*)dllGlobalVar = 1; printf("%d ", *(int*)dllGlobalVar); return 0; } dllGlobalVar = 1; 其結(jié)果是dllGlobalVar指針的內(nèi)容發(fā)生變化,程序中以后再也引用不到DLL中的全局變量了。 #include <stdio.h> #pragma comment(lib,"dllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)導(dǎo)入![]() int main(int argc, char *argv[]) { printf("%d ", dllGlobalVar); dllGlobalVar = 1; //這里就可以直接使用, 無須進(jìn)行強(qiáng)制指針轉(zhuǎn)換 printf("%d ", dllGlobalVar); return 0; } 通過_declspec(dllimport)方式導(dǎo)入的就是DLL中全局變量本身而不再是其地址了,筆者建議在一切可能的情況下都使用這種方式。 //文件名:point.h,point類的聲明![]() #ifndef POINT_H #define POINT_H![]() #ifdef DLL_FILE class _declspec(dllexport) point //導(dǎo)出類point #else class _declspec(dllimport) point //導(dǎo)入類point #endif![]() { public: float y; float x; point(); point(float x_coordinate, float y_coordinate); };![]() #endif![]() //文件名:point.cpp,point類的實(shí)現(xiàn) #ifndef DLL_FILE #define DLL_FILE #endif #include "point.h" //類point的缺省構(gòu)造函數(shù)![]() point::point() { x = 0.0; y = 0.0; }![]() //類point的構(gòu)造函數(shù) point::point(float x_coordinate, float y_coordinate) { x = x_coordinate; y = y_coordinate; }![]() ![]() //文件名:circle.h,circle類的聲明 #ifndef CIRCLE_H #define CIRCLE_H #include "point.h" #ifdef DLL_FILE class _declspec(dllexport)circle //導(dǎo)出類circle #else class _declspec(dllimport)circle //導(dǎo)入類circle #endif { public: void SetCentre(const point ¢rePoint); void SetRadius(float r); float GetGirth(); float GetArea(); circle(); private: float radius; point centre; }; #endif![]() //文件名:circle.cpp,circle類的實(shí)現(xiàn) #ifndef DLL_FILE #define DLL_FILE #endif #include "circle.h" #define PI 3.1415926 //circle類的構(gòu)造函數(shù) circle::circle() { centre = point(0, 0); radius = 0; }![]() //得到圓的面積 float circle::GetArea() { return PI *radius * radius; } //得到圓的周長(zhǎng) float circle::GetGirth() { return 2 *PI * radius; } //設(shè)置圓心坐標(biāo) void circle::SetCentre(const point ¢rePoint) { centre = centrePoint; } //設(shè)置圓的半徑 void circle::SetRadius(float r) { radius = r; }類的引用: #include "..\circle.h" //包含類聲明頭文件 #pragma comment(lib,"dllTest.lib");![]() int main(int argc, char *argv[]) { circle c; point p(2.0, 2.0); c.SetCentre(p); c.SetRadius(1.0); printf("area:%f girth:%f", c.GetArea(), c.GetGirth()); return 0; }從上述源代碼可以看出,由于在DLL的類實(shí)現(xiàn)代碼中定義了宏DLL_FILE,故在DLL的實(shí)現(xiàn)中所包含的類聲明實(shí)際上為: class _declspec(dllexport) point //導(dǎo)出類point { … }和 class _declspec(dllexport) circle //導(dǎo)出類circle { … }而在應(yīng)用工程中沒有定義DLL_FILE,故其包含point.h和circle.h后引入的類聲明為: class _declspec(dllimport) point //導(dǎo)入類point { … }和 class _declspec(dllimport) circle //導(dǎo)入類circle { … }不錯(cuò),正是通過DLL中的 class _declspec(dllexport) class_name //導(dǎo)出類circle { … }與應(yīng)用程序中的 class _declspec(dllimport) class_name //導(dǎo)入類 { … } 匹對(duì)來完成類的導(dǎo)出和導(dǎo)入的! #ifdef DLL_FILE class _declspec(dllexport) class_name //導(dǎo)出類 #else class _declspec(dllimport) class_name //導(dǎo)入類 #endif 實(shí)際上,在MFC DLL的講解中,您將看到比這更簡(jiǎn)便的方法,而此處僅僅是為了說明_declspec(dllexport)與_declspec(dllimport)匹對(duì)的問題。 posted on 2008-07-07 15:39 isabc 閱讀(25813) 評(píng)論(0) 編輯 收藏 引用 所屬分類: C++基礎(chǔ) |
|
|