Windows下的VC++動態連結庫程式設計
VC++動態連結庫程式設計
1、基礎概念
1.1 連結庫的概述
動態連結庫DLL(DynamicLinkable Library),你可以簡單的把它看成一種倉庫,它提供給你一些可以直接拿來用的變數、函式或類。在庫的發展史上經歷了“無庫-靜態連結庫-動態連結庫”的時代。靜態連結庫與動態連結庫都是共享程式碼的方式,如果採用靜態連結庫,則無論你願不願意,lib 中的指令都被直接包含在最終生成的EXE檔案中了。但是若使用DLL,該DLL不必被包含在最終EXE檔案中,EXE檔案執行時可以“動態”地引用和解除安裝這個與EXE獨立的DLL檔案。靜態連結庫和動態連結庫的另外一個區別在於靜態連結庫中不能再包含其他的動態連結庫或者靜態庫,而在動態連結庫中還可以再包含其他的動態或靜態連結庫。
對動態連結庫,我們還需建立如下概念:
(1)DLL的編制與具體的程式語言及編譯器無關
只要遵循約定的DLL介面規範和呼叫方式,用各種語言編寫的DLL都可以相互呼叫。譬如Windows提供的系統DLL(其中包括了Windows的API),在任何開發環境中都能被呼叫,不在乎其是VisualBasic、VisualC++還是Delphi。
(2)動態連結庫隨處可見
我們在Windows目錄下的system32資料夾中會看到kernel32.dll、user32.dll和gdi32.dll,windows的大多數API都包含在這些DLL中。kernel32.dll中的函式主要處理記憶體管理和程序排程;user32.dll中的函式主要控制使用者介面;gdi32.dll中的函式則負責圖形方面的操作。一般的程式設計師都用過類似MessageBox的函式,其實它就包含在user32.dll這個動態連結庫中。由此可見DLL對我們來說其實並不陌生。
(3)VC 動態連結庫的分類
VisualC++支援三種DLL,它們分別是Non-MFCDLL(非MFC動態庫)、MFC RegularDLL(MFC規則DLL)、MFC Extension DLL(MFC擴充套件DLL)。
非MFC動態庫不採用MFC類庫結構,其匯出函式為標準的C介面,能被非MFC或MFC編寫的應用程式所呼叫;MFC規則DLL包含一個繼承自CWinApp的類,但其無訊息迴圈;
MFC擴充套件DLL採用MFC的動態連結版本建立,它只能被用MFC類庫所編寫的應用程式所呼叫。
1.2 靜態庫與動態庫的區別
靜態連結庫Lib(Static Link Library),是在編譯的連結階段將庫函式嵌入到應用程式的內部。如果系統中執行的多個應用程式都包含所用到的公共庫函式,則必然造成很大的浪費。這樣即增加了連結器的負擔,也增大了可執行程式的大小,還加大了記憶體的消耗。Lib的好處是應用程式可以獨立執行,而不需要在作業系統中另外安裝對應的DLL。
而DLL採用動態連結,對公用的庫函式,系統只有一個拷貝(一般是位於系統目錄的*.DLL檔案),而且只有在應用程式真正呼叫時,才載入到記憶體。在記憶體中的庫函式,也只有一個拷貝,可供所有執行的程式呼叫。當再也沒有程式需要呼叫它時,系統會自動將其解除安裝,並釋放其所佔用的記憶體空間。參見圖1。
圖1 靜態庫函式與動態連結庫的區別
DLL的缺點是應用程式不能獨立執行,需要在作業系統中另外安裝對應的DLL。例如,如果你的MFC專案被設定成“在共享DLL中使用MFC”的,則雖然生成的可執行程式很小,但是在其他沒有安裝Visual C++(執行環境)的機器上是不能直接執行的,需要另外安裝MFC的動態連結庫(如mfc90.dll)。
1.2 靜態連結庫
對靜態連結庫的講解不是本文的重點,但是在具體講解DLL之前,通過一個靜態連結庫的例子可以快速地幫助我們建立“庫”的概念。
圖2
圖3
如圖2和圖3,使用VC++2008工具新建一個名稱為StaticLib的靜態庫工程(Win32控制檯應用程式或Win32專案均可),並新建lib.h 和lib.cpp 兩個檔案,原始碼如下:
//檔案:lib.h #ifndef _LIB_H_ #define _LIB_H_ extern"C" int add(int x,int y); //宣告為C編譯、連線方式的外部函式 #endif |
//檔案:lib.cpp #include "stdafx.h" #include"lib.h" int add(int x,int y) { return x+y; } |
圖4 生成的靜態庫檔案
編譯這個工程就得到了一個.lib檔案,這個檔案就是一個函式庫,它提供了add的功能。將標頭檔案和.lib 檔案提交給使用者後,使用者就可以直接使用其中的add函數了。
下面來看看怎麼使用這個庫,在StaticLib 工程所在的工作區(解決方案)內新建一個libCall 工程。libCall 工程僅包含一個main.cpp檔案,它演示了靜態連結庫的呼叫方法,其原始碼如下:
#include<stdio.h> #include"../StaticLib/lib.h" #pragma comment(lib, "../debug/StaticLib.lib") //指定與靜態庫一起連線 int main(int argc,char*argv[]) { printf("2 +3=%d",add(2,3)); return 0; } |
靜態連結庫的呼叫就是這麼簡單,或許我們每天都在用,可是我們沒有明白這個概念。程式碼中#pragmacomment(lib ,"..\debug\\StaticLib.lib")的意思是指本檔案生成的.obj檔案應與StaticLib.lib 一起連結。如果不用#pragma comment指定,則可以直接在VC++中設定,如圖5和6,依次選擇配置屬性->連結器->輸入->附加依賴項,填入庫檔案路徑和檔名。
圖5 設定連線的lib庫名稱
圖6 設定連結的lib庫所在的目錄
這個例子讓我們瞭解到:
(1)編寫庫的程式和編寫一般的程式區別不大,只是庫不能單獨執行;
(2)庫提供一些可以給別的程式呼叫的東西,別的程式要呼叫它必須以某種方式指明它要呼叫之。
1.3 庫的除錯與檢視
由於庫檔案不能單獨執行,因而在按下F5(開始debug模式執行)或CTRL+F5(執行)執行時,其彈出如圖7所示的對話方塊,要求使用者輸入可執行檔案的路徑來啟動庫函式的執行(如圖7),或者在屬性中設定可執行檔案的路徑(如圖8)。這個時候我們輸入要呼叫該庫的EXE檔案的路徑就可以對庫進行除錯了,其除錯技巧與一般應用工程的除錯一樣。
圖7 選擇可執行檔案
圖8 在屬性中設定可執行的檔案路徑
通常有比上述做法更好的除錯途徑,那就是將庫工程和應用工程(呼叫庫的工程)放置在同一VC工作區,只對應用工程進行除錯,在應用工程呼叫庫中函式的語句處設定斷點,執行後按下F11,這樣就單步進入了庫中的函式。第1.2節中的StaticLib 和LibCall工程就放在了同一工作區,其工程結構如圖9所示。
圖9 把庫工程和呼叫庫的工程放入同一工作區進行除錯
上述除錯方法對靜態連結庫和動態連結庫而言是一致的。動態連結庫中的匯出介面可以使用Visual C++的Depends工具進行檢視,讓我們用Depends開啟系統目錄中的user32.dll,看到了幾個版本的MessageBox了!
圖10 用Depends檢視user32.dll
1.3 MFC DLL的型別
使用MFC編寫的DLL,可以分成兩大類:
l 規則DLL——規則(regular)DLL中所包含的函式,可以被所有Windows應用程式使用;
n 共享MFC——DLL中不包含MFC庫函式,需要另外安裝MFC動態連結庫後才能使用;
n 靜態MFC——DLL中包含MFC庫函式,可以脫離MFC動態連結庫獨立使用。
l 擴充套件DLL——擴充套件(extension)DLL中所定義的類和函式,只能被MFC應用程式使用。而且擴充套件DLL中不能包含MFC庫函式,也需要另外安裝MFC動態連結庫後才能使用。
2、非MFC的DLL編寫
2.1 一個簡單的DLL
第1.3節給出了以靜態連結庫方式提供add函式介面的方法,接下來我們來看看怎樣用動態連結庫實現一個同樣功能的add函式。
圖11 新建DLL工程
如圖11,在VC++中新建一個Win32的DllTest(注意左側樹裡不要選擇MFC,因為後面將講述基於MFC的動態連結庫),在建立的工程中新增lib.h 及lib.cpp 檔案,原始碼如下:
/* 檔名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_ #ifdef DLLTEST_EXPORTS //在DllTest工程的預處理中定義 #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif LIB_API int add(int x,int y); #endif/*_LIB_H_*/ |
/* 檔名:lib.cpp */ #include"lib.h" int add(int x,int y) { return x+y; } |
分析上述程式碼,DllTest工程中的lib.cpp 檔案與第1.3節靜態連結庫版本完全相同,不同在於lib.h 對函式add的宣告前面添加了LIB_API巨集的定義。當DLLTEST_EXPORTS這個巨集有定義時,這個語句的含義是宣告函式add為DLL的extern"C" __declspec(dllexport)匯出函式,否則為extern"C" __declspec(dllimport)匯入函式。當我們在DllTest工程中新增.h和.cpp檔案的時候,VC會自動在編譯的“前處理器”中新增*_EXPORTS的定義,其中*為工程名稱,如圖12,這樣在DllTest工程內時,add就被定義成匯出函數了,當lib.h檔案給呼叫者使用時,由於呼叫者的工程中沒有該巨集的定義,所以它的add函式就被定義成了匯入函式。
圖12 *_EXPORTS巨集的定義位置
DLL內的函式分為兩種:
(1)DLL匯出函式,可供應用程式呼叫;
(2)DLL內部函式(非匯出),只能在DLL程式使用,應用程式無法呼叫它們。
2.2 DLL匯出函式
DLL中匯出函式的宣告有兩種方式:一種為2.1節例子中給出的在函式宣告中加上__declspec(dllexport),這裡不再舉例說明;另外一種方式是採用模組定義(.def) 檔案宣告,.def檔案為連結器提供了有關被連結程式的匯出、屬性及其他方面的資訊。
下面的程式碼演示了怎樣同.def檔案將函式add宣告為DLL匯出函式(需在DllTest工程中新增lib.def檔案):
; lib.def : 匯出DLL函式 LIBRARY DllTest EXPORTS add @ 1 |
.def檔案的規則為:
(1)LIBRARY語句說明.def檔案相應的DLL;
(2)EXPORTS語句後列出要匯出函式的名稱。可以在.def檔案中的匯出函式名後加@n,表示要匯出函式的序號為n(在後面進行顯示函式呼叫時,這個序號將發揮其作用);
(3).def 檔案中的註釋由每個註釋行開始處的分號(;) 指定,且註釋不能與語句共享一行。
由此可以看出,例子中lib.def檔案的含義為生成名為“DllTest”的動態連結庫,匯出其中的add函式,並指定add函式的序號為1。
2.3 DLL的呼叫方式
動態連結庫的呼叫方式包括兩種:隱式呼叫和顯式呼叫兩種。下面一一說來。
2.3.1 隱式呼叫(靜態呼叫)
隱式呼叫也被稱為靜態呼叫,是由編譯系統完成對DLL的載入和應用程式結束時DLL的解除安裝。當呼叫某DLL的應用程式結束時,若系統中還有其它程式使用該DLL,則Windows對DLL的應用記錄減1,直到所有使用該DLL的程式都結束時才釋放它。靜態呼叫方式同靜態連結庫的呼叫方式相同,特點是簡單實用,但不如動態呼叫方式靈活。
下面我們來看看靜態呼叫的例子,新增一個DllCall工程,並執行下列程式碼:
// main.cpp : 定義控制檯應用程式的入口點。 #include "stdafx.h" #include <stdio.h> #include "../DllTest/lib.h" #pragma comment(lib,"../Debug/DllTest.lib") int main(int argc,char*argv[]) { int result =add(2,3); printf("%d",result); return 0; } |
注意:在DLLCall工程中沒有對DLLTEST_EXPORTS巨集的定義,故add在lib.h標頭檔案中已經被定義稱為了extern"C" __declspec(dllimport)匯入函式。
2.3.2 顯式呼叫(動態呼叫)
顯式呼叫是指使用由“LoadLibrary-GetProcAddress-FreeLibrary”系統API提供的三位一體“DLL載入-DLL函式地址獲取-DLL釋放”方式,這種呼叫方式也被稱為DLL的動態呼叫。動態呼叫方式的特點是完全由程式設計者用API函式載入和解除安裝DLL,程式設計師可以決定DLL檔案何時載入或不載入,顯式連結在執行時決定載入哪個DLL檔案。
下面的程式碼展示了動態呼叫DLL中的函式add,其原始碼如下:
// main.cpp : 定義控制檯應用程式的入口點。 #include "stdafx.h" #include<stdio.h> #include<windows.h> typedef int(*lpAddFun)(int, int); //巨集定義函式指標型別 int main(int argc,char*argv[]) { HINSTANCE hDll;//DLL控制代碼 lpAddFun addFun;//函式指標 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; } |
注意:這裡需要指定DLL檔案的路徑。
2.3.3放置DLL的目錄
為了使需要動態連結庫的應用程式可以執行,需要將DLL檔案放在作業系統能夠找到的地方。Windows作業系統查詢DLL的目錄順序為:
1. 所在目錄——當前程序的可執行模組所在的目錄,即應用程式的可執行檔案(*.exe)所在的目錄。
2. 當前目錄——程序的當前目錄。
3. 系統目錄——Windows作業系統安裝目錄的系統子目錄,如C:\Windows\ System32。可用GetSystemDirectory函式檢索此目錄的路徑。
4. Windows目錄——Windows作業系統安裝目錄,如C:\Windows\。可用GetWindowsDirectory函式檢索此目錄的路徑。
5. 搜尋目錄——PATH環境變數中所包含的自動搜尋路徑目錄,一般包含C:\Windows\和C:\Windows\System32\等目錄。可在命令列用Path命令來檢視和設定,也可以通過(在“我的電腦”右鍵選單中選“屬性”選單項)“系統屬性”中的環境變數,來檢視或編輯“Path”系統變數和“PATH”使用者變數。
2.3.4 呼叫方式總結
由上述程式碼可以看出,靜態呼叫方式的順利進行需要完成兩個動作:
(1)告訴編譯器與DLL相對應的.lib檔案所在的路徑及檔名,#pragmacomment(lib,"../Debug/DllTest.lib")就是起這個作用。
程式設計師在建立一個DLL檔案時,聯結器會自動為其生成一個對應的.lib檔案,該檔案包含了DLL匯出函式的符號名及序號(並不含有實際的程式碼)。在應用程式裡,.lib檔案將作為DLL的替代檔案參與編譯。
(2)宣告匯入函式,extern"C" __declspec(dllimport) add(intx,inty)語句中的__declspec(dllimport)發揮這個作用,這裡是在lib.h檔案中的LIB_API巨集的定義來實現的。
靜態呼叫方式不再需要使用系統API來載入、解除安裝DLL以及獲取DLL中匯出函式的地址。這是因為,當程式設計師通過靜態連結方式編譯生成應用程式時,應用程式中呼叫的與.lib檔案中匯出符號相匹配的函式符號將進入到生成的EXE檔案中,.lib檔案中所包含的與之對應的DLL檔案的檔名也被編譯器儲存在EXE檔案內部。當應用程式執行過程中需要載入DLL檔案時,Windows將根據這些資訊發現並載入DLL,然後通過符號名實現對DLL函式的動態連結。這樣,EXE將能直接通過函式名呼叫DLL的輸出函式,就像呼叫程式內部的其他函式一樣。
2.4 DllMain函式
Windows在載入DLL的時候,需要一個入口函式,就如同控制檯或DOS程式需要main函式、WIN32程式需要WinMain函式一樣。在前面的例子中,DLL並沒有提供DllMain函式,應用工程也能成功引用DLL,這是因為Windows在找不到DllMain的時候,系統會從其它執行庫中引入一個不做任何操作的預設DllMain函式版本,並不意味著DLL可以放棄DllMain函式。
根據編寫規範,Windows必須查詢並執行DLL裡的DllMain函式作為載入DLL的依據,它使得DLL得以保留在記憶體裡。這個函式並不屬於匯出函式,而是DLL的內部函式。這意味著不能直接在應用工程中引用DllMain函式,DllMain是自動被呼叫的。
我們來看一個DllMain函式的例子:
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved) { switch(ul_reason_for_call) { case DLL_PROCESS_ATTACH: printf("process attach of dll\n "); break; case DLL_THREAD_ATTACH: printf("thread attach of dll\n "); break; case DLL_THREAD_DETACH: printf("thread detach of dll\n "); break; case DLL_PROCESS_DETACH: printf("process detach of dll\n "); break; } return TRUE; } |
DllMain函式在DLL被載入和解除安裝時被呼叫,在單個執行緒啟動和終止時,DLLMain函式也被呼叫,
ul_reason_for_call指明瞭被呼叫的原因。原因共有4種,即PROCESS_ATTACH、PROCESS_DETACH、
THREAD_ATTACH和THREAD_DETACH,以switch語句列出。來仔細解讀一下DllMain的函式頭BOOLAPIENTRY DllMain(HANDLE hModule,WORD ul_reason_for_call,LPVOID lpReserved ):
(1) APIENTRY被定義為__stdcall,它意味著這個函式以標準Pascal的方式進行呼叫,也就是WINAPI方式;
(2) 程序中的每個DLL模組被全域性唯一的32位元組的HINSTANCE控制代碼標識,只有在特定的程序內部有效,控制代碼代表了DLL模組在程序虛擬空間中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,這兩種型別可以替換使用,這就是函式引數hModule的來歷。
(3) 執行下列程式碼:
hDll=LoadLibrary("..\\Debug\\dllTest.dll");
if (hDll != NULL)
{
addFun=(lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
//MAKEINTRESOURCE直接使用匯出檔案中的序號
if (addFun!= NULL)
{
int result =addFun(2,3);
printf("\ncall add in dll:%d",result);
}
FreeLibrary(hDll);
}
我們看到輸出順序為:
process attach of dll
call add in dll:5
process detach of dll
這一輸出順序驗證了DllMain被呼叫的時機。
程式碼中的GetProcAddress(hDll,MAKEINTRESOURCE(1) )值得留意,它直接通過.def檔案中為add函式指定的順序號訪問add函式,具體體現在MAKEINTRESOURCE( 1),MAKEINTRESOURCE是一個通過序號獲取函式名的巨集,定義為(節選自winuser.h):
#defineMAKEINTRESOURCEA(i)(LPSTR)((DWORD)((WORD)(i)))
#defineMAKEINTRESOURCEW(i)(LPWSTR)((DWORD)((WORD)(i)))
#ifdefUNICODE
#defineMAKEINTRESOURCE MAKEINTRESOURCEW
#else
#defineMAKEINTRESOURCE MAKEINTRESOURCEA
2.4 __stdcall約定
如果通過VC++編寫的DLL欲被其他語言編寫的程式呼叫,應將函式的呼叫方式宣告為__stdcall方式,WINAPI都採用這種方式,而C/C++預設的呼叫方式卻為__cdecl。__stdcall方式與__cdecl對函式名最終生成符號的方式不同。若採用C編譯方式(在C++中需將函式宣告為extern"C"),__stdcall呼叫約定在輸出函式名前面加下劃線,後面加“@”符號和引數的位元組數,形如[email protected];而__cdecl呼叫約定僅在輸出函式名前面加下劃線,形如_functionname。Windows程式設計中常見的幾種函式型別宣告巨集都是與__stdcall和__cdecl有關的(節選自windef.h):
#define CALLBACK__stdcal l//這就是傳說中的回撥函式
#define WINAPI__stdcall //這就是傳說中的WINAPI
#define WINAPIV__cdecl
#define APIENTRY WINAPI //DllMain的入口就在這裡
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
在lib.h 中,應這樣宣告add函式:
int __stdcall add(int x,int y);
在應用工程中函式指標型別應定義為:
typedef int(__stdcall *lpAddFun)(int,int);
若在lib.h 中將函式宣告為__stdcall呼叫,而應用工程中仍使用typedefint (*lpAddFun)(int,int),執行時將發生錯誤(因為型別不匹配,在應用工程中仍然是預設的__cdecl呼叫),彈出如圖12所示的對話方塊。
圖13呼叫約定不匹配時的執行錯誤
圖13中的那段話實際上已經給出了錯誤的原因,即“This is usually are result of …”。
2.5 DLL匯出變數
DLL定義的全域性變數可以被呼叫程序訪問;DLL也可以訪問呼叫程序的全域性資料,我們來看看在應用工程中引用DLL中變數的例子。
/* 檔名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_ extern int dllGlobalVar; #endif /*_LIB_H_*/ |
/* 檔名: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被載入時,賦全域性變數為100 break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } |
;檔名:lib.def,在DLL中匯出變數 LIBRARY "DllTest" EXPORTS dllGlobalVar DATA ;或dllGlobalVar CONSTANT |
從lib.h 和lib.cpp 中可以看出,全域性變數在DLL中的定義和使用方法與一般的程式設計是一樣的。若要匯出某全域性變數,我們需要在.def檔案的EXPORTS後新增:
變數名 CONSTANT //過時的方法
或
變數名 DATA //VC++提示的新方法
在主函式中引用DLL中定義的全域性變數:
#include<stdio.h> #pragmacomment(lib,"DllTest.lib") extern int dllGlobalVar; int main(intargc,char*argv[]) { printf("%d", *(int*)dllGlobalVar); *(int*)dllGlobalVar=1; printf("%d", *(int*)dllGlobalVar); return 0; } |
特別要注意的是用extern int dllGlobalVar宣告所匯入的並不是DLL中全域性變數本身,而是其地址,應用程式必須通過強制指標轉換來使用DLL中的全域性變數。這一點,從*(int*)dllGlobalVar可以看出。因此在採用這種方式引用DLL全域性變數時,千萬不要進行這樣的賦值操作:
dllGlobalVar=1;
其結果是dllGlobalVar指標的內容發生變化,程式中以後再也引用不到DLL中的全域性變量了。在應用工程中引用DLL中全域性變數的一個更好方法是:
#include<stdio.h> #pragma comment(lib,"DllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)匯入 int main(intvargc,char*vargv[]) { printf("%d", dllGlobalVar); dllGlobalVar=1; //這裡就可以直接使用, 無須進行強制指標轉換 printf("%d", dllGlobalVar); return 0; } |
通過_declspec(dllimport)方式匯入的就是DLL中全域性變數本身而不再是其地址了,故建議採用如下的標頭檔案定義方式:
/* 檔名:lib.h */ #ifndef _LIB_H_ #define _LIB_H_ #ifdef DLLTEST_EXPORTS #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif LIB_API int dllGlobalVar; #endif/*_LIB_H_*/ |
2.6 DLL匯出類
DLL中定義的類可以在應用工程中使用。下面的例子裡,我們在DLL中定義了point類,並在應用工程中引用了它。
/*Point.h檔案:類Point的宣告*/ #ifndef _POINT_H_ #define _POINT_H_ #ifdef DLLTEST_EXPORTS #define CLASS_EXPORT __declspec(dllexport) #else #define CLASS_EXPORT __declspec(dllimport) #endif /*DLLTEST_EXPORTS*/ class CLASS_EXPORT Point { public: float y; float x; public: Point(void); ~Point(void); Point(float xx,float yy); }; #endif /*_POINT_H_*/ |
/*Point.cpp類的實現檔案*/ #include "Point.h" Point::Point(void) :x(0.),y(0.) { } Point::~Point(void) { } Point::Point(float xx,float yy) :x(xx),y(yy) { } |
類在工程中的使用,新增一個DllCall工程,寫入如下程式碼:
// main.cpp : 定義控制檯應用程式的入口點。 #include "stdafx.h" #include <stdio.h> #include "../DllTest/Point.h" #pragma comment(lib,"../Debug/DllTest.lib") int main(int argc,char*argv[]) { Point p(1,2); printf("p.x=%f,p.y=%f\n",p.x,p.y); return 0; } |
從上述原始碼可以看出,由於在Point.h檔案程式碼中定義了巨集DLLTEST_EXPORTS,故CLASS_EXPORT被定義為_declspec(dllexport),所以在DLL的類宣告實際上為:
class _declspec(dllexport) point //匯出類point
{
…
}
而在應用工程DllCall中沒有定義DLLTEST_EXPORTS,故CLASS_EXPORT被定義為__declspec(dllimport),所以DLL中引入的類宣告為:
class _declspec(dllimport) point //匯入類point
{
…
}
不錯,正是通過DLL中的
class _declspec(dllexport) class_name //匯出類point
{
…
}
與應用程式中的
class _declspec(dllimport) class_name //匯入類
{
…
}
匹對來完成類的匯出和匯入的!
2.7 DLL總結
由上述可見,應用工程中幾乎可以看到DLL中的一切,包括函式、變數以及類,這就是DLL所要提供的強大能力。只要DLL釋放這些介面,應用程式使用它就將如同使用本工程中的程式一樣!
3、MFC規則DLL編寫
3.1 MFC規則DLL概述
使用MFC編寫的規則DLL,雖然只能匯出函式而不能匯出整個類,但是其匯出的函式卻可以其他被非MFC應用程式所呼叫。下面我們仍通過上面的四則運算的例子,看看如何用關鍵字__declspec(dllexport)和extern "C"來編寫和使用匯出若干(全域性)C函式的規則MFC DLL。
MFC規則DLL的概念體現在兩方面:
(1)它是MFC的
“是MFC的”意味著可以在這種DLL的內部使用MFC;
(2)它是規則的
“是規則的”意味著它不同於MFC擴充套件DLL,在MFC規則DLL的內部雖然可以使用MFC,但是其與應用程式的介面不能是MFC。而MFC擴充套件DLL與應用程式的介面可以是MFC,可以從MFC擴充套件DLL中匯出一個MFC類的派生類。
Regular DLL能夠被所有支援DLL技術的語言所編寫的應用程式呼叫,當然也包括使用MFC的應用程式。在這種動態連線庫中,包含一個從CWinApp繼承下來的類,DllMain函式則由MFC自動提供。
Regular DLL分為兩類:
(1)靜態連結到MFC的規則DLL
靜態連結到MFC的規則DLL與MFC庫(包括MFC擴充套件DLL)靜態連結,將MFC庫的程式碼直接生成在.dll檔案中。在呼叫這種DLL的介面時,MFC使用DLL的資源。因此,在靜態連結到MFC的規則DLL中不需要進行模組狀態的切換。使用這種方法生成的規則DLL其程式較大,也可能包含重複的程式碼。
(2)動態連結到MFC的規則DLL
動態連結到MFC的規則DLL可以和使用它的可執行檔案同時動態連結到MFCDLL和任何MFC擴充套件DLL。在使用了MFC共享庫的時候,預設情況下,MFC使用主應用程式的資源控制代碼來載入資源模板。這樣,當DLL和應用程式中存在相同ID的資源時(即所謂的資源重複問題),系統可能不能獲得正確的資源。因此,對於共享MFCDLL的規則DLL,我們必須進行模組切換以使得MFC能夠找到正確的資源模板。
我們可以在Visual C++中設定MFC規則DLL是靜態連結到MFC DLL還是動態連結到MFC DLL。如圖14。
圖14 連結到MFC的方式
3.2 MFC的DLL函式匯出
使用MFC建立DLL時,從專案中匯出(export)函式到DLL檔案的方法有:
l 使用模組定義檔案(.def)。
l 使用__declspec(dllexport)關鍵字或其替代巨集AFX_EXT_CLASS。
這兩種方法是互斥的,對每個函式只需用一種方法即可。另外,DEF檔案只能用來匯出函式,不能用於匯出整個類。匯出C++類,必須用__declspec(dllexport)關鍵字或其替代巨集AFX_EXT_CLASS。
1.DEF檔案
同2.2節,模組定義(moduledefinition)檔案(.def)是包含一個或多個描述DLL各種屬性的模組語句的文字檔案。DEF檔案必須至少包含下列模組定義語句:
l 檔案中的第一個語句必須是LIBRARY語句。此語句將.def檔案標識為屬於DLL。LIBRARY語句的後面是DLL的名稱(預設為DLL專案名)。連結器將此名稱放到DLL的匯入庫中。
l EXPORTS語句列出名稱,可能的話還會列出DLL匯出函式的序號值。通過在函式名的後面加上@符和一個數字,給函式分配序號值。當指定序號值時,序號值的範圍必須是從1到N,其中N是DLL匯出函式的個數。
即,DEF檔案的格式為:(在這兩個語句之間,還可以加上可選的描述語句:DESCRIPTION "庫描述串"。分號;後的文字內容行為註釋)
; 庫名.def
LIBRARY 庫名
EXPORTS
函式名1 @1
函式名2 @2
……
函式名n @n
在使用MFC DLL嚮導建立MFC DLL專案時,VC會自動建立一個與專案同名但沒有任何函式匯出項的DEF檔案(專案名.def),格式為:
; 專案名.def : 宣告 DLL 的模組引數。
LIBRARY "專案名"
EXPORTS
; 此處可以是顯式匯出
例如,專案名為RegDll的DEF檔案(RegDll.def)的內容為:
; RegDll.def : 宣告 DLL 的模組引數。
LIBRARY "RegDll"
EXPORTS
; 此處可以是顯式匯出
如果生成擴充套件DLL並使用.def檔案匯出,則將下列程式碼放在包含匯出類的標頭檔案的開頭和結尾:
#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <你的標頭檔案體>
#undef AFX_DATA
#define AFX_DATA
這些程式碼行確保內部使用的MFC變數或新增到類的變數是從擴充套件DLL匯出(或匯入)的。例如,當使用DECLARE_DYNAMIC派生類時,該巨集擴充套件以將CRuntimeClass成員變數新增到類。省去這四行程式碼可能會導致不能正確編譯或連結DLL,或在客戶端應用程式連結到DLL時導致錯誤。
當生成DLL時,連結器使用.def檔案建立匯出(.exp)檔案和匯入庫(.lib)檔案。然後,連結器使用匯出檔案生成DLL檔案。隱式連結到DLL的可執行檔案在生成時連結到匯入庫。請注意,MFC本身就是使用.def檔案從MFCx0.dll匯出函式和類的。
2.關鍵字或巨集
除了使用DEF檔案來匯出函式外,還可以在源程式中使用__declspec(dllexport)關鍵字或其替代巨集AFX_EXT_CLASS:
#define AFX_EXT_CLASS AFX_CLASS_EXPORT(定義在標頭檔案afxv_dll.h中)
#define AFX_CLASS_EXPORT __declspec(dllexport) (定義在標頭檔案afxver_.h中)
來匯出函式和整個C++類。
具體的格式為:
l 匯出整個類:
class AFX_EXT_CLASS 類名[ : public基類]
{
……
}
l 匯出類的成員函式:
class 類名[ : public基類]
{
AFX_EXT_CLASS 返回型別 函式名1(……) ;
AFX_EXT_CLASS 返回型別 函式名2(……) ;
……
}
l 匯出外部C格式的(全域性)函式:
extern "C" __declspec(dllexport) 返回型別 函式名(……)
{
……
}
如果希望用MFC(C++)編寫的規則DLL中的函式,也能夠被非MFC程式來呼叫,需要為函式宣告指定extern "C"。不然,C++編譯器會使用C++型別安全命名約定(也稱作名稱修飾)和C++呼叫約定(使用此呼叫約定從C呼叫會很困難)。
為了使用方便,可以定義巨集:
#define DllExport extern "C" __declspec(dllexport)
然後再使用它,例如:
DllExport int Add(int d1, int d2) {……}
3.3 MFC規則DLL的建立
我們來一步步講述使用MFC嚮導建立MFC規則DLL的過程。建立一個名為RegDll的規則DLL的“Visual C++”之“MFC”的“MFC DLL”專案,注意需選中“建立解決方案的目錄”複選框,參見圖15。
圖15 新建MFC DLL專案RegDll的對話方塊
按“確定”鈕,彈出“MFC DLL嚮導”對話方塊。在“DLL型別”欄中,選中“使用共享MFC DLL的規則DLL”單選鈕,參見圖16。按“完成”鈕,建立RegDll解決方案和專案。
圖16 選擇規則DLL的MFC DLL嚮導對話方塊
1區域處也可以選擇“帶靜態連結MFC的規則DLL”,差別是所生成的DLL中會包含MFC庫,當然所生成的庫檔案也會大一些(但因此可不用另外安裝MFC動態連結庫)。例如,在此例中,選共享MFC所生成的RegDll.dll檔案只有13KB大,而選擇靜態MFC的則有199KB。
規則DLL專案是使用共享MFC還是使用靜態MFC,也可以在生成DLL專案之後,通過專案屬性對話方塊的“配置屬性->常規”頁中的“MFC的使用”欄中的下拉式列表選項來切換,這一點與普通MFC應用程式專案的類似。
2區選擇是否支援automation(自動化)技術,automation允許使用者在一個應用程式中操縱另外一個應用程式或元件。例如,我們可以在應用程式中利用MicrosoftWord或MicrosoftExcel的工具,而這種使用對使用者而言是透明的。自動化技術可以大大簡化和加快應用程式的開發。
3區選擇是否支援Windows Sockets,當選擇此專案時,應用程式能在TCP/IP網路上進行通訊。CWinApp派生類的InitInstance 成員函式會初始化通訊端的支援,同時工程中的StdAfx.h檔案會自動include <AfxSock.h>標頭檔案。新增socket通訊支援後的InitInstance成員函式如下:
BOOL CRegularDllApp::InitInstance()
{
if(!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
returnFALSE;
}
returnTRUE;
}
3.4一個簡單的MFC規則DLL
這個DLL的例子(屬於靜態連結到MFC的規則DLL)中提供了一個如圖11所示的對話方塊。在DLL中新增對話方塊的方式與在MFC應用程式中是一樣的。在圖17所示DLL中的對話方塊的Hello按鈕上點選時將MessageBox一個“Hello,您好”訊息框。
圖17 示例
(1)在3.3節所建立的RegDll工程的資源檢視上,新增一個對話方塊資源,並在對話方塊上新增一個“hello”按鈕,如下圖所示:
圖18 新建資源視窗
(2)在視窗上滑鼠右鍵,選擇“新增類“,在類名稱中輸入“CDllDialog”,如下圖:
圖19 新增視窗類
(3)新增“Hello”按鈕的雙擊響應事件。
void CDllDialog::OnBnClickedButton1() { // TODO: 在此新增控制元件通知處理程式程式碼 MessageBox(_T("Hello,您好"),_T("提示資訊")); } |
(4)編寫匯出函式,注意這裡的巨集REGDLL_EXPORTS是在RegDll工程的預處理中定義的。
圖20 程式碼
3.5 原始碼分析
第一組檔案:CWinApp繼承類的宣告與實現
// RegDll.h : RegDll DLL 的主標頭檔案 // #pragma once #ifndef __AFXWIN_H__ #error "在包含此檔案之前包含“stdafx.h”以生成 PCH 檔案" #endif #include "resource.h" // 主符號 // CRegDllApp // 有關此類實現的資訊,請參閱 RegDll.cpp // class CRegDllApp : public CWinApp { public: CRegDllApp(); // 重寫 public: virtual BOOL InitInstance(); DECLARE_MESSAGE_MAP() }; |
// RegDll.cpp : 定義 DLL 的初始化例程。 // #include "stdafx.h" #include "RegDll.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // //TODO: 如果此 DLL 相對於 MFC DLL 是動態連結的, // 則從此 DLL 匯出的任何調入 // MFC 的函式必須將 AFX_MANAGE_STATE 巨集新增到 // 該函式的最前面。 // // 例如: // // extern "C" BOOL PASCAL EXPORT ExportedFunction() // { // AFX_MANAGE_STATE(AfxGetStaticModuleState()); // // 此處為普通函式體 // } // // 此巨集先於任何 MFC 呼叫 // 出現在每個函式中十分重要。這意味著 // 它必須作為函式中的第一個語句 // 出現,甚至先於所有物件變數宣告, // 這是因為它們的建構函式可能生成 MFC // DLL 呼叫。 // // 有關其他詳細資訊, // 請參閱 MFC 技術說明 33 和 58。 // // CRegDllApp BEGIN_MESSAGE_MAP(CRegDllApp, CWinApp) END_MESSAGE_MAP() // CRegDllApp 構造 CRegDllApp::CRegDllApp() { // TODO: 在此處新增構造程式碼, // 將所有重要的初始化放置在 InitInstance 中 } // 唯一的一個 CRegDllApp 物件 CRegDllApp theApp; // CRegDllApp 初始化 BOOL CRegDllApp::InitInstance() { CWinApp::InitInstance(); return TRUE; } |
分析:
在這一組檔案中定義了一個繼承自CWinApp的類CRegularDllApp,並同時定義了其的一個例項theApp。乍一看,您會以為它是一個MFC應用程式,因為MFC應用程式也包含這樣的在工程名後新增“App”組成類名的類(並繼承自CWinApp類),也定義了這個類的一個全域性例項theApp。
我們知道,在MFC應用程式中CWinApp取代了SDK程式中WinMain的地位,SDK程式WinMain所完成的工作由CWinApp的三個函式完成:
virtualBOOLInitApplication( );
virtualBOOLInitInstance( );
virtualBOOLRun(); //傳說中MFC程式的“活水源頭”
但是MFC規則DLL並不是MFC應用程式,它所繼承自CWinApp的類不包含訊息迴圈。這是因為,MFC規則DLL不包含CWinApp::Run機制,主訊息泵仍然由應用程式擁有。如果DLL生成無模式對話方塊或有自己的主框架視窗,則應用程式的主訊息泵必須呼叫從DLL匯出的函式來呼叫PreTranslateMessage成員函式。另外,MFC規則DLL與MFC應用程式中一樣,需要將所有DLL中元素的初始化放到InitInstance 成員函式中
第二組檔案自定義對話方塊類宣告及實現
#pragma once // CDllDialog 對話方塊 class CDllDialog : public CDialog { DECLARE_DYNAMIC(CDllDialog) public: CDllDialog(CWnd* pParent = NULL); // 標準建構函式 virtual ~CDllDialog(); // 對話方塊資料 enum { IDD = IDD_DIALOG1 }; protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支援 DECLARE_MESSAGE_MAP() public: afx_msg void OnBnClickedButton1(); }; |
// DllDialog.cpp : 實現檔案 // #include "stdafx.h" #include "RegDll.h" #include "DllDlg.h" // CDllDialog 對話方塊 IMPLEMENT_DYNAMIC(CDllDialog, CDialog) CDllDialog::CDllDialog(CWnd* pParent /*=NULL*/) : CDialog(CDllDialog::IDD, pParent) { } CDllDialog::~CDllDialog() { } void CDllDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); } BEGIN_MESSAGE_MAP(CDllDialog, CDialog) ON_BN_CLICKED(IDC_BUTTON1, &CDllDialog::OnBnClickedButton1) END_MESSAGE_MAP() // CDllDialog 訊息處理程式 void CDllDialog::OnBnClickedButton1() { // TODO: 在此新增控制元件通知處理程式程式碼 MessageBox("Hello,您好","提示資訊"); } |
分析:
這一部分的程式設計與一般的應用程式根本沒有什麼不同,我們照樣可以利用MFC類嚮導來自動為對話方塊上的控制元件新增事件。MFC類嚮導照樣會生成類似ON_BN_CLICKED (IDC_ BUTTON1, OnBnClickedButton1) 的訊息對映巨集。
第三組檔案DLL中的資原始檔
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by RegDll.rc // #define IDD_DIALOG1 4000 #define IDC_BUTTON1 4000 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 4001 #define _APS_NEXT_COMMAND_VALUE 32771 #define _APS_NEXT_CONTROL_VALUE 4001 #define _APS_NEXT_SYMED_VALUE 4000 #endif #endif |
分析:
在MFC規則DLL中使用資源也與在MFC應用程式中使用資源沒有什麼不同,我們照樣可以用VisualC++的資源編輯工具進行資源的新增、刪除和屬性的更改。
第四組檔案MFC規則DLL介面函式
#ifndef _LIB_H_ #define _LIB_H_ #ifdef REGDLL_EXPORTS #define LIB_API extern "C" __declspec(dllexport) #else #define LIB_API extern "C" __declspec(dllimport) #endif LIB_API void ShowDlg(void); #endif /*_LIB_H_*/ |
#include"StdAfx.h" #include "resource.h" #include"DllDialog.h" #include "lib.h" LIB_API void ShowDlg(void) { CDllDialog dllDlg; dllDlg.DoModal(); } |
分析:
這個介面並不使用MFC,但是在其中卻可以呼叫MFC擴充套件類CDllDialog的函式,這體現了“規則”的概類。
與非MFC DLL完全相同,我們可以使用__declspec(dllexport)宣告或在.def 中引出的方式匯出MFC規則DLL中的介面。
3.5 MFC規則DLL的呼叫
在這裡,新建一個“MFC應用程式”工程DllCall來呼叫3.4節所編寫的RegDll庫。下面21是在這個程式的對話方塊上點選“Call DLL”按鈕時彈出3.2節MFC規則DLL中的對話方塊。
圖21 示例視窗
“Call DLL”按鈕的訊息處理函式如下:
//方法一:隱式靜態呼叫方法 #include "../RegDll/lib.h" #pragma comment(lib,"../Debug/RegDll.lib") //…………… void CDllCallDlg::OnBnClickedButton1() { // TODO: 在此新增控制元件通知處理程式程式碼 ShowDlg(); } |
或者:
//方法二:顯式動態呼叫方法 void CDllCallDlg::OnBnClickedButton1() { // TODO: 在此新增控制元件通知處理程式程式碼 typedef void(*lpFun)(void); HINSTANCE hDll; //DLL 控制代碼 hDll=LoadLibrary(_T("../Debug/RegDll.dll")); if (NULL==hDll) { MessageBox(_T("DLL載入失敗")); return; } lpFun pShowDlg=(lpFun)GetProcAddress(hDll,"ShowDlg"); if (NULL==pShowDlg) { MessageBox(_T("DLL中函式尋找失敗")); FreeLibrary(hDll); return ; } pShowDlg(); FreeLibrary(hDll); } |
注意:在3.4節所建立的是“使用共享MFCDLL的規則DLL(D)”,其工程屬性如下圖所示,否則在呼叫的時候會出現失敗,原因家3.6節。
圖22 設定MFC連結方式
3.6共享MFC規則DLL的模組切換
應用程式程序本身及其呼叫的每個DLL模組都具有一個全域性唯一的HINSTANCE控制代碼,它們代表了DLL或EXE模組在程序虛擬空間中的起始地址。程序本身的模組控制代碼一般為0x400000,而DLL模組的預設控制代碼為0x10000000。如果程式同時載入了多個DLL,則每個DLL模組都會有不同的HINSTANCE。應用程式在載入DLL時對其進行了重定位。
共享MFC DLL(或MFC擴充套件DLL)的規則DLL涉及到HINSTANCE控制代碼問題,HINSTANCE控制代碼對於載入資源特別重要。EXE和DLL都有其自己的資源,而且這些資源的ID可能重複,應用程式需要通過資源模組的切換來找到正確的資源。如果應用程式需要來自於DLL的資源,就應將資源模組控制代碼指定為DLL的模組控制代碼;如果需要EXE檔案中包含的資源,就應將資源模組控制代碼指定為EXE的模組控制代碼。
這次我們建立一個動態連結到MFCDLL的規則DLL,在其中包含如圖23的對話方塊。
圖23 DLL中視窗
另外,在與這個DLL相同的工作區中生成一個基於對話方塊的MFC程式,其對話方塊與圖23完全一樣。但是在此工程中我們另外添加了一個如圖14的對話方塊。
圖24 EXE中視窗
圖23和圖24中的對話方塊除了caption不同(以示區別)以外,其它的都相同。尤其值得特別注意,在DLL和EXE中我們對圖23和圖24的對話方塊使用了相同的資源ID=2000,在DLL和EXE工程的resource.h 中分別有如下的巨集:
//DLL中對話方塊的ID
#define IDD_DLL_DIALOG 2000
//EXE中對話方塊的ID
#define IDD_EXE_DIALOG 2000
與3.5節靜態連結MFC DLL的規則DLL相同,我們還是在規則DLL中定義介面函式ShowDlg,原型如下:
圖25 程式碼
而為應用工程主對話方塊的“Call DLL”的單擊事件新增如下訊息處理函式:
圖26 呼叫程式碼
我們以為單擊“呼叫DLL”會彈出如圖23所示DLL中的對話方塊,可是可怕的事情發生
了,我們看到是圖24所示EXE中的對話方塊!
產生這個問題的根源在於應用程式與MFC規則DLL共享MFC DLL(或MFC擴充套件DLL)的程式總是預設使用EXE的資源,我們必須進行資源模組控制代碼的切換,其實現方法有三種。
方法一:在DLL介面函式中使用:AFX_MANAGE_STATE(AfxGetStaticModuleState());
我們將DLL中的介面函式ShowDlg改為:
voidShowDlg(void)
{
//方法1:在函式開始處變更,在函式結束時恢復
//將AFX_MANAGE_STATE(AfxGetStaticModuleState());作為介面函式的第一
//條語句進行模組狀態切換
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialogdlg(IDD_DLL_DIALOG);//開啟ID為2000的對話方塊
dlg.DoModal();
}
這次我們再點選EXE程式中的“Call DLL”按鈕,彈出的是DLL中的如圖13的對話方塊!彈出了正確的對話方塊資源。
AfxGetStaticModuleState是一個函式,其原型為:
AFX_MODULE_STATE*AFXAPIAfxGetStaticModuleState();
該函式的功能是在棧上(這意味著其作用域是區域性的)建立一個AFX_MODULE_STATE類(模組全域性資料也就是模組狀態)的例項,對其進行設定,並將其指標pModuleState返回。AFX_MODULE_STATE類的原型如下:
//AFX_MODULE_STATE(globaldataforamodule)
classAFX_MODULE_STATE: public CNoTrackObject
{
public:
#ifdef_AFXDLL
AFX_MODULE_STATE(BOOL bDLL,WNDPROCpfnAfxWndProc,DWORD dwVersion);
AFX_MODULE_STATE(BOOL bDLL,WNDPROCpfnAfxWndProc,DWORD dwVersion,BOOL bSystem);
#else
AFX_MODULE_STATE(BOOLbDLL);
#endif
~AFX_MODULE_STATE();
CWinApp*m_pCurrentWinApp;
HINSTANCEm_hCurrentInstanceHandle;
HINSTANCEm_hCurrentResourceHandle;
LPCTSTRm_lpszCurrentAppName;
…//省略後面的部分
}
AFX_MODULE_STATE類利用其建構函式和解構函式進行儲存模組狀態現場及恢復現場的工作,類似彙編中call指令對pc指標和sp暫存器的儲存與恢復、中斷服務程式的中斷現場壓棧與恢復以及作業系統執行緒排程的任務控制塊儲存與恢復。
AFX_MANAGE_STATE是一個巨集,其原型為:
AFX_MANAGE_STATE(AFX_MODULE_STATE*pModuleState)
該巨集用於將pModuleState設定為當前的有效模組狀態。當離開該巨集的作用域時(也就離開了pModuleState所指向棧上物件的作用域),先前的模組狀態將由AFX_MODULE_STATE的解構函式恢復。
方法二:在DLL介面函式中使用
AfxGetResourceHandle();
AfxSetResourceHandle(HINSTANCExxx);
AfxGetResourceHandle用於獲取當前資源模組控制代碼,而AfxSetResourceHandle則用於設定程式目前要使用的資源模組控制代碼。我們將DLL中的介面函式ShowDlg改為:
extern CRegDllApp theApp; //需要宣告theApp 外部全域性變數
void ShowDlg(void)
{
//方法2的狀態變更
HINSTANCE save_hInstance=AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
CDialogdlg(IDD_DLL_DIALOG);//開啟ID為2000的對話方塊
dlg.DoModal();
//方法2的狀態還原
AfxSetResourceHandle(save_hInstance);
}
通過AfxGetResourceHandle和AfxSetResourceHandle的合理變更,我們能夠靈活地設定程式的資源模組控制代碼,而方法一則只能在DLL介面函式退出的時候才會恢復模組控制代碼。方法二則不同,如果將ShowDlg改為:
extern CRegDllApp theApp; //需要宣告theApp 外部全域性變數
void ShowDlg(void)
{
//方法2的狀態變更
HINSTANCE save_hInstance=AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
CDialog dlg(IDD_DLL_DIALOG);//開啟ID為2000的