一種實現(xiàn)Win32窗口過程函數(shù)(Window Procedure)的新方法
基于Thunk實現(xiàn)的類成員消息處理函數(shù)
JERKII.SHANG (JERKII@HOTMAIL.COM)
MAR.10th - 31st, 2006
Windows是一個消息驅(qū)動的操作系統(tǒng),在系統(tǒng)中發(fā)生的所有消息均需要通過消息處理過程(或叫窗口過程)進(jìn)行處理。由于C++給我們在程序設(shè)計中帶來更多的靈活性(如繼承、重載、多態(tài)等),所以我們都希望能夠使用C++的類來封裝Windows中的窗口過程函數(shù),但是Windows規(guī)定了窗口過程函數(shù)必須定義為一個全局函數(shù),也就是說需要使用面向過程的方法來實現(xiàn),為了使用面向?qū)ο蟮募夹g(shù)來實現(xiàn)消息處理,我們必須另辟它徑。目前我們在網(wǎng)絡(luò)上見得比較多的方式是使用Thunk將即將傳遞給窗口過程的第一個參數(shù)(HWND hWnd)的值使用類對象的內(nèi)存地址(即this指針)進(jìn)行替換(ATL使用的也是這種方法)。這樣,在相應(yīng)的窗口過程中通過將hWnd強(qiáng)制轉(zhuǎn)換成類對象的指針,這樣就可以通過該指針調(diào)用給類中的成員函數(shù)了。但是該方法仍然需要將該消息處理函數(shù)定義成一個靜態(tài)成員函數(shù)或者全局函數(shù)。本文將介紹一種完全使用(非靜態(tài))類成員函數(shù)實現(xiàn)Win32的窗口過程函數(shù)和窗口過程子類化的新方法。雖然也是基于Thunk,但是實現(xiàn)方法完全不同于之前所說的那種,我所采用的是方法是——通過對Thunk的調(diào)用,將類對象的this指針直接傳遞給在類中定義的窗口處理函數(shù)(通過ECX或棧進(jìn)行傳遞),這樣就能夠使Windows直接成功地調(diào)用我們窗口過程函數(shù)了。另外,本文介紹一種使用C++模板進(jìn)行消息處理函數(shù)的“重載”,這種方法直接避免了虛函數(shù)的使用,因此所有基類及其派生類中均無虛函數(shù)表指針以及相應(yīng)的虛函數(shù)表(在虛函數(shù)較多的情況下,該數(shù)組的大小可是相當(dāng)可觀的)。從而為每個類的實例“節(jié)省”了不少內(nèi)存空間(相對于使用傳統(tǒng)的函數(shù)重載機(jī)制)。
關(guān)鍵字: C++ 模板,調(diào)用約定,Thunk,機(jī)器指令(編碼),內(nèi)嵌匯編
環(huán)境:VC7,VC8,32位Windows
內(nèi)容
前言
也許你是一位使用MFC或ATL進(jìn)行編程的高手,并且能在很短的時間內(nèi)寫出功能齊全的程序。但是,你是否曾經(jīng)花時間去想過“MFC或ATL是通過什么樣的途徑來調(diào)用我們的消息處理函數(shù)的呢?他們是怎樣將Windows產(chǎn)生的消息事件傳遞給我們的呢?”在MFC中定義一個從CWnd繼承而來的類,相應(yīng)的消息事件就會發(fā)送到我們定義的類中來,你不覺得這背后所隱藏的一切很奇怪嗎?如果你的感覺是這樣,那么本文將使用一種簡單并且高效的方法來揭開這個神秘的面紗以看個究竟,同時我將非常詳細(xì)地介紹需要使用到的各種知識,以便能讓更多初學(xué)者更容易掌握這些知識。
在Windows中,所有的消息均通過窗口過程函數(shù)進(jìn)行處理,窗口過程函數(shù)是我們和Windows操作系統(tǒng)建立聯(lián)系的唯一途徑,窗口過程函數(shù)的聲明均為:
LRESULT __stdcall WndProc
(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
MSDN有對該函數(shù)中的每個參數(shù)的詳細(xì)描述,如果你現(xiàn)在仍然對該函數(shù)存在疑問,那么請先參照MSDN中的相關(guān)內(nèi)容后,再繼續(xù)閱讀本文。通常,Windows均要求我們將消息處理函數(shù)定義為一個全局函數(shù),或者是一個類中的靜態(tài)成員函數(shù)。并且該函數(shù)必須采用__stdcall的調(diào)用約定(Calling Convention),因為__stdcall的函數(shù)在返回前會自己修正ESP的值(由于參數(shù)傳遞所導(dǎo)致的ESP的改變),從而使ESP的值恢復(fù)到函數(shù)調(diào)用之前的狀態(tài)。
全局函數(shù)或靜態(tài)成員函數(shù)的使用使得我們很難發(fā)揮C++的優(yōu)勢(除非你一直想使用C進(jìn)行Windows程序開發(fā),如果是這樣,那么你也就沒有必要再繼續(xù)閱讀本文了),因為在這種結(jié)構(gòu)下(即面向過程),我們不能很方便地在消息處理函數(shù)中使用我們的C++對象,因為在這樣的消息處理函數(shù)中,我們很難得到我們對象的指針,從而導(dǎo)致我們不能很方便的操作C++對象中的屬性。為了解決對Win32的封裝,Microsoft先后推出了MFC和ATL,可以說兩者都是非常優(yōu)秀的解決方案,并且一直為多數(shù)用戶所使用,為他們在Windows下的程序開發(fā)提供了很大的便利。
但是,MFC在我們大家的眼里都是一種比較笨重的方法,使用MFC開發(fā)出來的程序都必須要在MFC相關(guān)動態(tài)庫的支持下才能運行,并且這些動態(tài)庫的大小可不一般(VS2005中的mfc80.dll就有1.04M),更為甚者,CWnd中包含大量的虛函數(shù),所以每個從他繼承下來的子類都有一個數(shù)量相當(dāng)可觀的虛函數(shù)表(雖然通過在存在虛函數(shù)的類上使用sizeof得到的結(jié)果是該類對象的大小只增長了4個字節(jié),即虛函數(shù)表指針,但是該指針?biāo)赶虻奶摵瘮?shù)數(shù)組同樣需要內(nèi)存空間來存儲。一個虛函數(shù)在虛函數(shù)表中需占用4個字節(jié),如果在我們的程序中用到較多的CWnd的話,定會消耗不少內(nèi)存(sizeof(CWnd) = 84,sizeof(CDialog) = 116)。ATL與MFC完全不同,ATL采用模板來對Win32中的所有內(nèi)容進(jìn)行封裝,使用ATL開發(fā)出來的程序不需要任何其他動態(tài)庫的支持(當(dāng)然,除基本的Windows庫外),ATL使用Thunk將C++對象的指針通過消息處理函數(shù)的第一個參數(shù)(即hWnd)傳入,這樣,在消息處理函數(shù)中,我們就可以很方便地通過該指針來訪問C++對象中的屬性和成員函數(shù)了,但這種方法也必須要借助幾個靜態(tài)成員函數(shù)來實現(xiàn)。
本文將采用這幾種技術(shù)實現(xiàn)一種完全由C++類成員函數(shù)(非靜態(tài))實現(xiàn)的Windows消息處理機(jī)制,并最終開發(fā)一個封裝Windows消息處理的工具包(KWIN)。首先讓我們來看看怎樣使用KWIN來開發(fā)的一個簡單程序:
創(chuàng)建一個簡單的窗口程序:
#include "kwin.h"
class MyKWinApp : public KWindowImpl<MyKWinApp>
{
public:
MyKWinApp () : KWindowImpl<MyKWinApp> ("MyKWinAppClassName")
{}
/* Overriede the window procdure */
LRESULT KCALLBACK KWndProc (
HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{ /* Do anthing you want here */
return __super::KWndProc (hWnd, msg, wParam, lParam);
}
BOOL OnDestroy () { PostQuitMessage (0); return TRUE; }
/* Override other message handler */
};
INT __stdcall WinMain (
HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
MyKWinApp kapp;
kapp.CreateOverlappedWindow ("This is my first KWinApp");
MSG msg;
while (GetMessage (&msg, 0, 0, 0))
{ TranslateMessage (&msg);
DispatchMessage (&msg);
}
return msg.wParam;
}
創(chuàng)建一個簡單的對話框程序:
#include "kwin.h"
class MyKDlgApp : public KDialogImpl<MyKDlgApp>
{
public:
enum {IDD = IDD_DLG_MYFIRSTDIALOG };
BOOL OnCommand(WORD wNotifyCode, WORD wId, HWND hWndCtrl)
{ if (wId == IDOK) EndDialog (m_hWnd, wId); return TRUE; }
};
INT __stdcall WinMain (
HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
MyKDlgApp kapp;
kapp.DoModal ();
return 0;
}
怎么樣?使用KWIN開發(fā)包后,你的程序結(jié)構(gòu)是不是變得更加清晰了呢?你可以在你的類中(如MyKWinApp或MyKDlgApp)“重載”更多的消息處理函數(shù),你甚至可以“重載”窗口過程函數(shù)(如MyKWinApp)。這里的重載跟通常意義上的重載可不是一個意思,這里的“重載”是使用C++模板機(jī)制實現(xiàn)的,而傳統(tǒng)意義上的重載需要通過虛函數(shù)(更準(zhǔn)確地說應(yīng)該是虛函數(shù)表)來實現(xiàn)。好,你現(xiàn)在是不是很想知道,這個KWIN的內(nèi)部到底是怎樣實現(xiàn)的了吧?好,讓我來一步一步帶你步入KWIN的內(nèi)部中去吧,現(xiàn)在你唯一需要的就是耐心,因為這篇文章寫得似乎太長了些^_^ ...
傳統(tǒng)的C++重載
通常,如果我們要重載父類中的方法,那么我們必須在父類中將該方法聲明為虛函數(shù),這樣每個擁有虛函數(shù)的類對象將會擁有一個自己的虛函數(shù)表(指針),也就是說,該虛函數(shù)表(數(shù)組)就成了該類對象的“‘靜態(tài)’成員變量(之所以說是‘靜態(tài)’的,是因為該虛函數(shù)數(shù)組及其指向該數(shù)組的指針在該類的所有實例中均為同一份數(shù)據(jù))”了,C++(更確切地說應(yīng)該是編譯器)就是通過虛函數(shù)表來實現(xiàn)函數(shù)的重載機(jī)制的,因為有了虛函數(shù)表后,對虛函數(shù)的調(diào)用就是通過該虛函數(shù)表來完成的,因為編譯器在生成代碼的時候會根據(jù)每個虛函數(shù)在類中的位置而對其進(jìn)行編號,并且通過該序號來對虛函數(shù)進(jìn)行調(diào)用,所以你通常會在反匯編中看到如下代碼:
mov edx, this pointer ; Load EAX with the value of 'this pointer'(or vptr)
mov edx, dword ptr [edx + 4] ; Get the address of the second virtual function in vtable
push ... ; Pass argument 1
push ... ; Pass other arguments here ...
call edx ; Call virtual function
當(dāng)子類從有虛函數(shù)的父類中派生時,他將擁有一份獨立的虛函數(shù)表指針以及虛函數(shù)數(shù)組,并且“開始時”該數(shù)組中的內(nèi)容和基類一模一樣。但是,當(dāng)編譯器在編譯時檢測到子類重載了父類中的虛函數(shù)時,編譯器就會修改子類的虛函數(shù)數(shù)組表,將該表中被重載的虛函數(shù)的地址改為子類中的函數(shù)地址,對于那些沒有被重載的虛函數(shù),該表中的函數(shù)地址和父類的虛函數(shù)表中的地址一樣!當(dāng)一個子類從多個帶有虛函數(shù)的父類中繼承時,該子類就擁有多個虛函數(shù)表指針了(也就是將擁有多個虛函數(shù)指針數(shù)組),當(dāng)我們在這種情況下進(jìn)行指針的轉(zhuǎn)換的時(通常為將子類指針轉(zhuǎn)換成父類指針),所進(jìn)行最終操作的就是虛函數(shù)數(shù)組指針的轉(zhuǎn)換。如:
class A :
public VBase1,
public VBase2,
public VBase3
/* VBase1, VBase2, VBase3 均為存在虛函數(shù)的父類 */
{ ... };
A a;
VBase1* p1;
VBase2* p2;
VBase3* p3;
p1 = &a;
p2 = &a;
p3 = &a;
// 假定這里a的地址為0x0012f564,那么(按字節(jié)為單位)
p1 = &a + 0 = 0x0012f564,
p2 = &a + 4 = 0x0012f568,
p3 = &a + 8 = 0x0012f56C
因為在類對象的內(nèi)存布局中,編譯器總是將虛函數(shù)數(shù)組指針放在偏移為0的地方。
好了,似乎我們已經(jīng)跑題了,關(guān)于這方面的知識在網(wǎng)上也可以找到很多,如果你有興趣,可以參見我的另一篇文章:略談虛函數(shù)的調(diào)用機(jī)制,至此,我相信你已經(jīng)對C++中的重載機(jī)制有一定的認(rèn)識了吧,現(xiàn)在再讓我們來看看怎樣在C++中使用模板來實現(xiàn)函數(shù)的“重載”。
使用C++模板實現(xiàn)函數(shù)的“重載”
通過我們對周圍事物(或數(shù)據(jù))的抽象,我們得到類。如果我們再對類進(jìn)行抽象,得到就是一個模板。模板是一種完全基于代碼級的重用機(jī)制,同時也為我們編寫一些結(jié)構(gòu)良好,靈活的程序提供了手段,所有的這一切都得歸功于編譯器的幫助,因為編譯器最終會將我們所有的這些使用這些高級技術(shù)編寫的代碼轉(zhuǎn)換成匯編代碼(或者應(yīng)該說是機(jī)器碼),好,廢話少說為好!^_^。
通常我們會使用下面的方式來實現(xiàn)函數(shù)的“重載”,這里似乎沒有很復(fù)雜的“理論”要闡述,我就簡單的把大致實現(xiàn)過程描述一下吧:
template <class T> class TBase
{
public:
void foo1 () { printf ("This is foo1 in TBase\n"); }
void foo2 () { printf ("This is foo2 in TBase\n"); }
void callback ()
{ /* 如果子類重載了TBase中的函數(shù),通過pThis將會直接調(diào)用子類中的函數(shù) */
T* pThis = static_cast<T *> (this);
pThis->foo1 ();
pThis->foo2 ();
}
};
class TDerive : public TBase <TDerive>
{
public:
void foo1 () { printf ("This is foo1 in TDerive\n"); } /* “重載”父類中的foo1 */
};
TDerive d;
d.callback ();
輸出結(jié)果為:
This is foo1 in TDerive
This is foo2 in TBase
雖然上面的代碼看起來很奇怪,因為子類將自己作為參數(shù)傳遞給父類。并且子類中定義的“重載”函數(shù)只能通過“回調(diào)”的方式才能被調(diào)用,但這對Windows中的消息處理函數(shù)來說無疑是一大福音,因為Windows中的消息函數(shù)均是回調(diào)函數(shù),似乎這樣的“重載”就是為Windows中的消息處理函數(shù)而定制的。雖然有些奇怪,但是他真的很管用,尤其是在實現(xiàn)我們的消息處理函數(shù)的時候。不是嗎?
C++對象中的屬性和方法(成員函數(shù))
我們通常會將一些相關(guān)的屬性以及操作這些屬性的方法“封裝”到一個類中,從而實現(xiàn)所謂的信息(屬性)隱藏,各類對象(或類的實例)之間的交互都得通過成員函數(shù)來進(jìn)行,這些屬性的值在另一方面也表明了該類對象在特定時刻的狀態(tài)。由于每個類對象都擁有各自的屬性(或狀態(tài)),所以系統(tǒng)需要為每個類對象分配相應(yīng)的屬性(內(nèi)存)存儲空間。但是類中的成員函數(shù)卻不是這樣,他們不占用類對象的任何內(nèi)存空間。這就是為什么使用sizeof (your class)總是返回該類中的成員變量的字節(jié)大小(如果該類中存在虛函數(shù),則還會多出4個字節(jié)的虛函數(shù)數(shù)組指針來)。因為成員函數(shù)所要操作的只是類對象中的屬性,他們所關(guān)心的不是這些屬性的值。
那么,這些類成員函數(shù)又是怎么知道他要去操作哪個類對象中的屬性呢?答案就是通過this指針。this指針說白了就是一個指向該類對象在創(chuàng)建之后位于內(nèi)存中的內(nèi)存地址。當(dāng)我們調(diào)用類中的成員函數(shù)時,編譯器會“悄悄地”將相應(yīng)類對象的內(nèi)存地址(也就是this指針)傳給我們的類成員函數(shù),有了這個指針,我們就可以在類成員函數(shù)中對該類對象中的屬性進(jìn)行訪問了。this指針被“傳入”成員函數(shù)的方式主要取決于你的類成員函數(shù)在聲明時所使用的調(diào)用約定,如果使用__thiscall調(diào)用約定,那么this指針將通過寄存器ECX進(jìn)行傳遞,該方式通常是編譯器(VC)在缺省情況下使用的調(diào)用約定。如果是__stdcall或__cdecl調(diào)用約定,this指針將通過棧進(jìn)行傳遞,并且this指針將是最后一個被壓入棧的參數(shù),雖然我們在聲明成員函數(shù)時,并沒有聲明有這個參數(shù)。
關(guān)于調(diào)用約定與this指針的傳遞
簡單說來,調(diào)用約定(Calling Convention)主要是用來指定相應(yīng)的函數(shù)在被調(diào)用時的參數(shù)傳遞順序,以及在調(diào)用完成后由誰(調(diào)用者還是被調(diào)用者)來修正ESP寄存器的值(因為調(diào)用者向被調(diào)用者通過棧來傳遞參數(shù)時,ESP的值會被修改,系統(tǒng)必須要能夠保證被調(diào)用函數(shù)返回后,ESP的值要能夠恢復(fù)到調(diào)用之前的值,這樣調(diào)用者才能正確的運行下去,對于其他寄存器,編譯器通常會自動為我們生成相應(yīng)的保存與恢復(fù)代碼,通常是在函數(shù)一開始將相關(guān)寄存器的值PUSH到棧中,函數(shù)返回之前再依次pop出來)。
通常對ESP的修正有兩種方式,一種是直接使用ADD ESP, 4*n,一種是RET 4*n(其中n為調(diào)用者向被調(diào)用者所傳遞的參數(shù)個數(shù),乘4是因為在棧中的每個參數(shù)需要占用4個字節(jié),因為編譯器為了提高尋址效率,會將所有參數(shù)轉(zhuǎn)換成32位,即即使你傳遞一個字節(jié),那么同樣會導(dǎo)致ESP的值減少4)。通常我們使用的調(diào)用約定主要有__stdcall,__cdecl,__thiscall,__fastcall。有關(guān)這些調(diào)用約定的詳細(xì)說明,請參見MSDN(節(jié)Argument Passing and Naming Conventions )。這里只簡略地描述他們的用途:幾乎所有的Windows API均使用__stdcall調(diào)用約定,ESP由被調(diào)用者函數(shù)自行修正,通常在被調(diào)用函數(shù)返回之前使用RET 4 * n的形式。__cdecl調(diào)用約定就不一樣,它是由調(diào)用者來對ESP進(jìn)行修正,即在被調(diào)用函數(shù)返回后,調(diào)用者采用ADD ESP, 4 *n的形式進(jìn)行棧清除,通常你會看到這樣的代碼:
push argument1
push argument2
call _cdecl_function
add esp, 8
另外一個__cdecl不得不說的功能是,使用__cdecl調(diào)用約定的函數(shù)可以接受可變數(shù)量的參數(shù),我們見得最多的__cdecl函數(shù)恐怕要算printf了(int __cdecl printf (char *format, ...)),瞧!是不是很cool啊。因為__cdecl是由調(diào)用者來完成棧的清除操作,并且他自己知道自己向被調(diào)用函數(shù)傳遞了多少參數(shù),此次他自己也知道該怎樣去修正ESP。跟__stdcall比起來,唯一的“缺點”恐怕就是他生成的可執(zhí)行代碼要比__stdcall稍長些(即在每個CALL之后,都需要添加一條ADD ESP, X的指令)。但跟他提供給我們的靈活性來說,這點“缺點”又算什么呢?
__thiscall主要用于類成員函數(shù)的調(diào)用約定,在VC中它是成員函數(shù)的缺省調(diào)用約定。他跟__stdcall一樣,由被調(diào)用者清除調(diào)用棧。唯一不同的恐怕就是他們對于this指針的傳遞方式了吧!在__stdcall和__cdecl中,this指針通過棧傳到類成員函數(shù)中,并且被最后一個壓入棧。而在__thiscall中,this指針通過ECX進(jìn)行傳遞,直接通過寄存器進(jìn)行參數(shù)傳遞當(dāng)然會得到更好的運行效率。另外一種,__fastcall,之所以叫fast,是因為使用這種調(diào)用約定的函數(shù),調(diào)用者會“盡可能”的將參數(shù)通過寄存器的方式進(jìn)行傳遞。另外,編譯器將為每種調(diào)用約定的函數(shù)產(chǎn)生不同的命名修飾(即Name-decoration convention),當(dāng)然這些只是編譯器所關(guān)心的東西,我們就不要再深入了吧!。
通過其他途徑調(diào)用類中的成員函數(shù)
對于給定的一個類:
class MemberCallDemo
{
public:
void __stdcall foo (int a) { printf ("In MemberCallDemo::foo, a = %d\n", a); };
};
通常我們會通過下面的方式進(jìn)行調(diào)用該類中的成員方法foo:
MemberCallDemo mcd;
mcd.foo (9);
或者通過函數(shù)指針的方式:
void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo;
(mcd.*foo_ptr) (9);
我總是認(rèn)為這中使用成員函數(shù)指針的調(diào)用方式(或是語法)感到很奇怪,不過它畢竟是標(biāo)準(zhǔn)的并且能夠為C++編譯器認(rèn)可的調(diào)用方式。幾乎在所有編譯器都不允許將成員函數(shù)的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無濟(jì)于事)例如:而只能將其賦給給與該成員函數(shù)類型聲明(包括所使用的調(diào)用約定,返回值,函數(shù)參數(shù))完全相同的變量。因為成員函數(shù)的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時我們?yōu)榱藢崿F(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可),我們可以通過下面兩種方法來實現(xiàn):下面的這幾種試圖將一個成員函數(shù)的地址保存到一個DWORD中都將被編譯器認(rèn)為是語法錯誤: 我總是認(rèn)為這中使用成員函數(shù)指針的調(diào)用方式(或是語法)感到很奇怪,不過它畢竟是標(biāo)準(zhǔn)的并且能夠為C++編譯器認(rèn)可的調(diào)用方式。幾乎在所有編譯器都不允許將成員函數(shù)的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無濟(jì)于事)。下面的這幾種試圖將一個成員函數(shù)的地址保存到一個DWORD中都將被編譯器認(rèn)為是語法錯誤:
DWORD dwFooAddrPtr= 0;
dwFooAddrPtr = (DWORD) &MemberCallDemo::foo; /* Error C2440 */
dwFooAddrPtr = reinterpret_cast<DWORD> (&MemberCallDemo::foo); /* Error C2440 */
因為成員函數(shù)的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時我們?yōu)榱藢崿F(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可)。
我們只能將成員函數(shù)的地址賦給給與該成員函數(shù)類型聲明(包括所使用的調(diào)用約定,返回值,函數(shù)參數(shù))完全相同的變量(如前面的void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo)。因為成員函數(shù)的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時我們?yōu)榱藢崿F(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可),就像我們即將介紹的情況。
通過前面幾節(jié)的分析我們知道,成員函數(shù)的調(diào)用和一個普通的非成員函數(shù)(如全局函數(shù),或靜態(tài)成員函數(shù)等)唯一不同的是,編譯器會在背后“悄悄地”將類對象的內(nèi)存地址(即this指針)傳到類成員函數(shù)中,具體的傳遞方式以該類成員函數(shù)所采用的調(diào)用約定而定。所以,是不是只要我們能夠手動地將這個this指針傳遞給一個成員函數(shù)(這是應(yīng)該是一個函數(shù)地址),是不是就可以使該成員函數(shù)被正確調(diào)用呢?答案是肯定的,但是我們迫在眉睫需要解決的是怎樣才能得到這個成員函數(shù)的地址呢?通常,我們有兩種方法可以達(dá)到此目的:
1。使用內(nèi)嵌匯編(在VC6及以前的版本中將不能編譯通過)
DWORD dwFooAddrPtr = 0;
__asm
{
/* 得到MemberCallDemo::foo偏移地址,事實上就是該成員函數(shù)的內(nèi)存地址(起始地址) */
MOV EAX, OFFSET MemberCallDemo::foo
MOV DWORD PTR [dwFooAddrPtr], EAX
}
這種方法雖然看起來甚是奇怪,但是他卻能夠解決我們所面臨的問題。雖然在目前的應(yīng)用程序開發(fā)中,很少甚至幾乎沒有人使用匯編語言去開發(fā),但是,往往有時一段小小的匯編代碼居然能夠解決我們使用其他方法不能解決的問題,這在上面的例子和下面即將介紹的Thunk中大家就會看到他那強(qiáng)有力的問題解決能力。所以說我們不能將匯編仍在一邊,我們需要了解她,并且能夠在適當(dāng)?shù)臅r候使用她。畢竟她始終是一個最漂亮,最具征服力的編程語言。^_^
2。通過使用union來“欺騙”編譯器
或使用一種更為巧妙的方法,通過使用一個union數(shù)據(jù)結(jié)構(gòu)進(jìn)行轉(zhuǎn)換(Stroustrup在其《The C++ Programming Language》中講到類似方法),由于在union數(shù)據(jù)結(jié)構(gòu)中,所有的數(shù)據(jù)成員均享用同一內(nèi)存區(qū)域,只是我們?yōu)檫@一塊內(nèi)存區(qū)域賦予了不同的類型及名稱,并且我們修改該結(jié)構(gòu)中的任何“變量”都會導(dǎo)致其他所有“變量”的值均被修改。所以我們可以使用這種方法來“欺騙”編譯器,從而讓他認(rèn)為我們所進(jìn)行的“轉(zhuǎn)換”是合法的。
template <class ToType, class FromType>
ToType union_cast (FromType f)
{
union
{ FromType _f;
ToType _t;
} ut;
ut._f = f;
return ut._t;
}
DWORD dwAddrPtr = union_cast<DWORD> (&YourClass::MemberFunction);
怎么樣,這樣的類型轉(zhuǎn)換是不是很酷啊?就像使用reinterpret_cast和static_cast等之類的轉(zhuǎn)換操作符一樣。通過巧妙地使用union的特點輕松“逃”過編譯器的類型安全檢查這一關(guān),從而達(dá)到我們的數(shù)據(jù)轉(zhuǎn)換目的。當(dāng)然,我們通常不會這樣做,因為這樣畢竟是類型不安全的轉(zhuǎn)換,他只適用于特定的非常規(guī)的(函數(shù)調(diào)用)場合。
好,我們現(xiàn)在已經(jīng)得到了該成員函數(shù)的內(nèi)存地址了,下買面我們通過一個“更為奇怪的”方式來調(diào)用成員函數(shù)MemberCallDemo::foo(使用這種方式,該成員函數(shù)foo將不能使用缺省的__thiscall調(diào)用約定,而必須使用__stdcall或__cdecl):
void (__stdcall *fnFooPtr) (void*/* pThis*/, int/* a*/) =
(void (__stdcall *) (void*, int)) dwFooAddrPtr;
fnFooPtr (&mcd, 9);
執(zhí)行上面的調(diào)用后,屏幕上依然會輸出“In MemberCallDemo::foo, a = 9”。這說明我們成功地調(diào)用了成員函數(shù)foo。當(dāng)然,使用這種方式使我們完全沖破了C++的封裝原則,打壞了正常的調(diào)用規(guī)則,即使你將foo聲明為private函數(shù),我們的調(diào)用同樣能夠成功,因為我們是通過函數(shù)地址來進(jìn)行調(diào)用的。
你不禁會這樣問,在實際開發(fā)中誰會這樣做呢?沒錯,我估計也沒有人會這樣來調(diào)用成員函數(shù),除非在一些特定的應(yīng)用下,比如我們接下來所要做的事情。
認(rèn)識Thunk
Thunk!Thunk?這是什么意思?翻開《Oxford Advanced Learner's Dictionary, Sixth-Edition》...查不到!再使用Kingsoft's PowerWord 2006,其曰“錚,鐺,鏘?”顯然是個象聲詞。頓暈,這跟我們所要描述的簡直不挨邊啊!不知道人們?yōu)槭裁匆堰@種技術(shù)稱之為Thunk。不管了,暫時放置一邊吧!
通常,我們所編寫的程序最終將被編譯器的轉(zhuǎn)換成計算機(jī)能夠識別的指令碼(即機(jī)器碼),比如:
C/C++代碼 等價的匯編代碼 編譯器產(chǎn)生的機(jī)器指令
==================== ========================== =====================
int k1, k2, k;
k1 = 1; mov dword ptr [k1], 1 C7 45 E0 01 00 00 00
k2 = 2; mov dword ptr [k2], 2 C7 45 D4 02 00 00 00
k = k1 + k2; mov eax, dword ptr [k1] 8B 45 E0
add eax, dword ptr [k2] 03 45 D4
mov dword ptr [k], eax 89 45 C8
最終,CPU執(zhí)行完指令序列“C7 45 E0 01 00 00 00 C7 45 D4 02 00 00 00 8B 45 E0 03 45 D4 89 45 C8”后,就完成了上面的簡單加法操作。從這里我們似乎能夠得到啟發(fā),既然CPU只能夠認(rèn)識機(jī)器碼,那么我們可以直接將機(jī)器碼送給CPU去執(zhí)行嗎?答案是:當(dāng)然可以,而且還非常高效!那么,怎么做到這一點呢?——定義一個機(jī)器碼數(shù)組,然后跳轉(zhuǎn)到該數(shù)組的起始地址處開始執(zhí)行:
unsigned char machine_code[] = {
0xC7, 0x45, 0xE0, 0x01, 0x00, 0x00, 0x00,
0xC7, 0x45, 0xD4, 0x02, 0x00, 0x00, 0x00,
0x8B, 0x45, 0xE0,
0x03, 0x45, 0xD4,
0x89, 0x45, 0xC8};
void* paddr = machine_code;
使用內(nèi)嵌匯編調(diào)用該機(jī)器碼:
__asm
{ MOV EAX, dword ptr [paddr] ; or mov eax, dword ptr paddr ; or mov eax, paddr
CALL EAX
}
如果使用C調(diào)用該機(jī)器碼,則為:
void (*fn_ptr) (void) = (void (*) (void)) paddr;
fn_ptr ();
怎么樣?當(dāng)上面的CALL EAX執(zhí)行完后,變量k的值同樣等于3。但是,當(dāng)machine_code中的指令執(zhí)行完后,CPU將無法再回到CALL指令的下一條指令了!為什么啊?是的,因為machine_code中沒有返回指令的機(jī)器碼!要讓CPU能夠返回到正確的位置,我們必須將返回指令RET的機(jī)器碼(0xC3)追加到machine_code的末尾,即:unsigned char machine_code[] = {0xC7, 0x45, ..., 0x89, 0x45, 0xC8, 0xC3};。
這就是Thunk!一種能讓CPU直接執(zhí)行我們的機(jī)器碼的技術(shù),也有人稱其為自修改代碼(Self-Modifying Code)。但這有什么用呢?同樣,在通常的開發(fā)中,我們不可能通過這么復(fù)雜的代碼來完成上面的簡單加法操作!誰這樣做了,那他/她一定是個瘋子!^_^。目前所了解的最有用的也是用得最多的就是使用Thunk來更改棧中的參數(shù),甚至可以是棧中的返回地址,或者向棧中壓入額外的參數(shù)(就像我們的KWIN那樣),從而達(dá)到一些特殊目的。當(dāng)然,在你熟知Thunk的原理后,你可能會想出更多的用途來,當(dāng)然,如果你想使用Thunk來隨意破壞當(dāng)前線程的棧數(shù)據(jù),從而直接導(dǎo)致程序或系統(tǒng)崩潰,那也不是不可能的,只要你喜歡,誰又在乎呢?只要你不把這個程序拿給別人運行就行!
通常我們會使用Thunk來“截獲”對指定函數(shù)的調(diào)用,并在真正調(diào)用該函數(shù)之前修改調(diào)用者傳遞給他的參數(shù)或其他任何你想要做的事情,當(dāng)我們做完我們想要做的時候,我們再“跳轉(zhuǎn)到”真正需要被調(diào)用的函數(shù)中去。既然要跳轉(zhuǎn),就勢必要用到JMP指令。由于在Thunk中的代碼必須為機(jī)器指令,所以我們必須按照編譯器的工作方式將我們所需要Thunk完成的代碼轉(zhuǎn)換成機(jī)器指令,因此我們需要知道我們在Thunk所用到的指令的機(jī)器指令的編碼規(guī)則(通常,我們在Thunk中不可能做太多事情,了解所需的指令的編碼規(guī)則也不是件難事)。大家都知道,JMP為無條件轉(zhuǎn)移指令,并且有short、near、far轉(zhuǎn)移,通常編譯器會根據(jù)目標(biāo)地址距當(dāng)前JMP指令的下一條指令之間的距離來決定跳轉(zhuǎn)類型。在生成機(jī)器碼時,并且編譯器會優(yōu)先考慮short轉(zhuǎn)移(如果目標(biāo)地址距當(dāng)前JMP指令的下一條指令的距離在-128和127之間),此時,JMP對應(yīng)的機(jī)器碼為0xEB。如果超出這個范圍,JMP對應(yīng)的機(jī)器碼通常為0xE9。當(dāng)然,JMP還存在其他類型的跳轉(zhuǎn),如絕對地址跳轉(zhuǎn)等,相應(yīng)地也有其他形式的機(jī)器碼,如0xFF,0xEA。我們常用到的只有0xEB和0xE9兩種形式。另外,需要注意的是,在機(jī)器碼中,JMP指令后緊跟的是一個目標(biāo)地址到該條JMP指令的下一條指令之間的距離(當(dāng)然,以字節(jié)為單位),所以,如果我們在Thunk中需要用到JMP指令,我們就必須手動計算該距離(這也是編譯器所需要做的一件事)。如果你已經(jīng)很了解JMP指令的細(xì)節(jié),那么你應(yīng)該知道了下面Thunk的將是什么樣的結(jié)果了吧:
unsigned char machine_code [] = {0xEB, 0xFE};
啊,沒錯,這將是一個死循環(huán)。這一定很有趣吧!事實上他跟JMP $是等價的。由于這里的機(jī)器碼是0xEB,它告訴CPU這是一個short跳轉(zhuǎn),并且,0xFE的最高位為1(即負(fù)數(shù)),所以CPU直到它是一個向后跳轉(zhuǎn)的JMP指令。由于向后跳轉(zhuǎn)的,所以此時該JMP所能跳轉(zhuǎn)到的范圍為-128至-1(即0x80至0xFF),但是由于這時的JMP指令為2個字節(jié),所以向后跳轉(zhuǎn)(從該條指令的下一條指令開始)2個字節(jié)后,就又回到了該條JMP指令的開始位置。
當(dāng)發(fā)生Short JMP指令時,其所能跳轉(zhuǎn)的范圍如下:
偏移量 機(jī)器碼
=========== ======
(-128) 0x80 ??
(-127) 0x81 ??
:
:
(-3) 0xFD ??
(-2) 0xFE EB <- Short JMP指令
(-1) 0xFF XX <- XX為跳轉(zhuǎn)偏移量,其取值范圍可為[0x80 - 0x7F]
(0) 0x00 ?? <- JMP下一條指令的開始位置
(+1) 0x01 ??
(+2) 0x02 ??
:
:
(+125) 0x7D ??
(+126) 0x7E ??
(+127) 0x7F ??
好,讓我們在來看一個例子,來說明Thunk到底是怎樣修改棧中的參數(shù)的:
void foo(int a)
{ printf ("In foo, a = %d\n", a); }
unsigned char code[9];
* ((DWORD *) &code[0]) = 0x042444FF; /* inc dword ptr [esp+4] */
code[4] = 0xe9; /* JMP */
* ((DWORD *) &code[5]) = (DWORD) &foo - (DWORD) &code[0] - 9; /* 跳轉(zhuǎn)偏移量 */
void (*pf)(int/* a*/) = (void (*)(int)) &code[0];
pf (6);
當(dāng)執(zhí)行完pf (6)調(diào)用后,就會得到下面的輸出:
“In foo, a = 7”(明明傳入的是6,為什么到了foo中就變成了7了呢?)。怎么樣?我們在Thunk中通過強(qiáng)制CPU執(zhí)行機(jī)器碼0xFF,0x44,0x24,0x04來將棧中的傳入?yún)?shù)a(位于ESP + 4處)增加1,從而修改了調(diào)用者傳遞過來的參數(shù)。在執(zhí)行完INC DWORD PTR [ESP+4]后,再通過一個跳轉(zhuǎn)指令跳轉(zhuǎn)到真正的函數(shù)入口處。當(dāng)然,我們同樣不可能在實際的開發(fā)中使用這種方法進(jìn)行函數(shù)調(diào)用,之所以這樣做是為了能夠更加容易的弄清楚Thunk到底是怎么工作的!
讓W(xué)indows直接調(diào)用你在類中定義的(非靜態(tài))消息處理函數(shù)
好了,寫了這么久,似乎我們到這里才真正進(jìn)入我們的正題。上面幾節(jié)所描述的都是這一節(jié)所需的基本知識,有了以上知識,我們就能夠很容易的實現(xiàn)我們的最終目的——讓W(xué)indows來直接調(diào)用我們的類消息處理成員函數(shù),在這里無須使用任何靜態(tài)成員函數(shù)或全局函數(shù),所有的事情都將由我們定義的類成員函數(shù)來完成。由于Windows中所有的消息處理均為回調(diào)函數(shù),即它們是由操作系統(tǒng)在特定的消息發(fā)生時被系統(tǒng)調(diào)用的函數(shù),我們需要做的僅僅是定義該消息函數(shù),并將該消息函數(shù)的函數(shù)地址“告訴”Windows。既然我們能夠使用在通過其他途徑調(diào)用類中的成員函數(shù)中所描述的方法得到類成員函數(shù)(消息處理函數(shù))的地址,那么,我們能夠直接將該成員函數(shù)地址作為一個回調(diào)函數(shù)的地址傳給操作系統(tǒng)嗎?很顯然,這是不可能的。但是為什么呢?我想聰明的你已經(jīng)猜到,因為我們的成員函數(shù)需要類對象的this指針去訪問類對象中的屬性,但是Windows是無法將相應(yīng)的類對象的this指針傳給我們的成員函數(shù)的!這就是我們所面臨的問題的關(guān)鍵所在!如果我們能夠解決這個類對象的this指針傳遞問題,即將類對象的this指針手動傳遞到我們的類成員函數(shù)中,那么我們的問題豈不是就解決了嗎?沒錯!
Thunk可以為們解決這個難題!這里Thunk需要解決的是將消息處理函數(shù)所在的類的實例的this指針“傳遞”到消息處理函數(shù)中,從前面的描述我們已經(jīng)知道,this指針的傳遞有兩種方式,一種是通過ECX寄存器進(jìn)行傳遞,一種是使用棧進(jìn)行傳遞。
1。使用ECX傳遞this指針(__thiscall)
這是一種最簡單的方式,它只需我們簡單地在Thunk中執(zhí)行下面的指令即可:
LEA ECX, this pointer
JMP member function-based message handler
使用這種方式傳遞this指針時,類中的消息處理函數(shù)必須使用__thiscall調(diào)用約定!在 關(guān)于調(diào)用約定與this指針的傳遞中我們對調(diào)用約定有較為詳細(xì)的討論。
2。使用棧傳遞this指針(__stdcall或__cdecl)
這是一種稍復(fù)雜的方式,使用棧傳遞this指針時必須確保類中的消息處理函數(shù)使用__stdcall調(diào)用約定,這跟通常的消息處理函數(shù)(靜態(tài)成員函數(shù)或全局函數(shù))使用的是同一種條用約定,唯一不同的是現(xiàn)在我們使用的是類成員函數(shù)(非靜態(tài))。之所以說他稍復(fù)雜,是因為我們要在Thunk中要做稍多的工作。前面我們已經(jīng)說過,我們已經(jīng)將我們定義的Thunk的地址作為“消息處理回調(diào)函數(shù)”地址傳給了Windows,那么,當(dāng)有消息需要處理時,Windows就會調(diào)用我們的消息處理函數(shù),不過這時它調(diào)用的是Thunk中的代碼,并不是真正的我們在類中定義的消息處理函數(shù)。這時,要將this指針?biāo)腿氘?dāng)前棧中可不是件“容易”的事情。讓我們來看看Windows在調(diào)用我們的Thunk代碼時的棧的參數(shù)內(nèi)容:
this指針被壓入棧之前 this指針被壓入棧之后
: ... : : ... :
|---------------| |----------------|
| LPARAM | | LPARAM |
|---------------| |----------------|
| WPARAM | | WPARAM |
|---------------| |----------------|
| UINT (msg) | | UINT (msg) |
|---------------| |----------------|
| HWND | | HWND |
|---------------| |----------------|
| (Return Addr) | <- ESP | <this pointer> | <- New item inserted by this thunk code
|---------------| |----------------|
: ... : | (Return Addr) | <- ESP
|----------------|
: ... :
圖1 圖2
從圖1可以看出,為了將this指針?biāo)腿霔V校覀兛刹荒芎唵蔚厥褂肞USH this pointer的方式將this指針“壓入”棧中!但是為什么呢?想想看,如果直接將this指針壓入棧中,那么原來的返回地址將不能再起效。也就是說我們將不能在我們的消息處理函數(shù)執(zhí)行結(jié)束后“返回”到正確的地方,這勢必會導(dǎo)致系統(tǒng)的崩潰。另一方面,我們的成員函數(shù)要求this指針必須是最后一個被送入棧的參數(shù),所以,我們必須將this指針“插入”到HWND參數(shù)和返回地址(Return Addr)之間。如圖2所示。所以,在這種情況下,我們須在Thunk中完成以下工作:
PUSH DWORD PTR [ESP] ; 保存(復(fù)制)返回地址到當(dāng)前棧中
MOV DWORD PTR [ESP + 4], pThis ; 將this指針?biāo)腿霔V校丛瓉淼姆祷氐刂诽?/span>
JMP member function-based message handler ; 跳轉(zhuǎn)至目標(biāo)消息處理函數(shù)(類成員函數(shù))
實現(xiàn)我們的KWIN包
好了,有了以上知識后,現(xiàn)在就只剩下我們的KWIN包的開發(fā)了,當(dāng)然,如果你掌握以上知識后,你可以運用這些知識,甚至找出新的方法來實現(xiàn)你自己的消息處理包。我想,那一定時間非常令人激動的事情!如果你想到更好的方法,千萬別忘了告訴我一聲哦。
首先來看看我們在KWIN中需要使用到的一個比較重要的宏:
#define __DO_DEFAULT (LRESULT) -2
#define _USE_THISCALL_CALLINGCONVENTION
#ifdef _USE_THISCALL_CALLINGCONVENTION
#define THUNK_CODE_LENGTH 10 /* For __thiscall calling convention ONLY */
#define KCALLBACK __thiscall
#else
#define THUNK_CODE_LENGTH 16 /* For __stdcall or __cdecl calling convention ONLY */
#define KCALLBACK __stdcall
#endif
在KWIN中同時實現(xiàn)了__thiscall和__stdcall兩種調(diào)用約定,如果定義了_USE_THISCALL_CALLINGCONVENTION宏,那么就使用__thiscall調(diào)用約定,否則將使用__stdcall調(diào)用約定。宏KCALLBACK在定義了_USE_THISCALL_CALLINGCONVENTION宏后將被替換成__thiscall,否則為__stdcall。THUNK_CODE_LENGTH定義了在不同的調(diào)用約定下所需的機(jī)器指令碼的長度,如果使用__thiscall,我們只需要10個字節(jié)的機(jī)器指令碼,而在__stdcall下,我們需要使用16字節(jié)的機(jī)器指令碼。
我們將實現(xiàn)對話框和一般窗口程序的消息處理函數(shù)進(jìn)行封裝的包(KWIN),我們力求使用KWIN能為我們的程序帶來更好的靈活性和結(jié)構(gòu)“良好”性,就像我們在本文開始時向大家展示的一小部分代碼那樣。首先我們將定義一個對話框和窗口程序都需要的數(shù)據(jù)結(jié)構(gòu)_K_THUNKED_DATA,該結(jié)構(gòu)封裝了所有所需的Thunk代碼(機(jī)器指令碼)。整個KWIN的結(jié)構(gòu)大致如下:

圖3
在_K_THUNKED_DATA中有一個非常重要的函數(shù)—Init,它的原型如下:
void Init (
DWORD_PTR pThis, /* 消息處理類對象的內(nèi)存地址(this指針) */
DWORD_PTR dwProcPtr /* 消息處理函數(shù)(類成員函數(shù))的地址 */
)
{
DWORD dwDistance = (DWORD) dwProcPtr - (DWORD) &pThunkCode[0] - THUNK_CODE_LENGTH;
#ifdef _USE_THISCALL_CALLINGCONVENTION
/*
Encoded machine instruction Equivalent assembly languate notation
--------------------------- -------------------------------------
B9 ?? ?? ?? ?? mov ecx, pThis ; Load ecx with this pointer
E9 ?? ?? ?? ?? jmp dwProcPtr ; Jump to target message handler
*/
pThunkCode[0] = 0xB9;
pThunkCode[5] = 0xE9;
*((DWORD *) &pThunkCode[1]) = (DWORD) pThis;
*((DWORD *) &pThunkCode[6]) = dwDistance;
#else
/*
Encoded machine instruction Equivalent assembly languate notation
--------------------------- -------------------------------------
FF 34 24 push dword ptr [esp] ; Save (or duplicate) the Return Addr into stack
C7 44 24 04 ?? ?? ?? ?? mov dword ptr [esp+4], pThis ; Overwite the old Return Addr with 'this pointer'
E9 ?? ?? ?? ?? jmp dwProcPtr ; Jump to target message handler
*/
pThunkCode[11] = 0xE9;
*((DWORD *) &pThunkCode[ 0]) = 0x002434FF;
*((DWORD *) &pThunkCode[ 3]) = 0x042444C7;
*((DWORD *) &pThunkCode[ 7]) = (DWORD) pThis;
*((DWORD *) &pThunkCode[12]) = dwDistance;
#endif
}
看見了吧,該函數(shù)的實現(xiàn)異常簡單,但是在KWIN中的作用非凡。我們的KWIN的成敗就依賴于該函數(shù)是否正確的“生成”了我們所需要的機(jī)器指令碼。Init通過將類對象的this指針和消息處理函數(shù)的地址“硬編碼”到數(shù)組_machine_code數(shù)組中,這樣該數(shù)組中就擁有了可以讓W(xué)indows正確調(diào)用我們在類中定義的成員函數(shù)所需的指令了。接下來所需要做的事情就全在我們創(chuàng)建的類(KWindowImpl和KDialogImpl或你自己創(chuàng)建的從這兩個類派生出來的類)中了。
_K_WINDOW_ROOT_IMPL中提供了一些對話框和窗口應(yīng)用程序都需要處理的公共消息的缺省處理(如WM_CREATE,WM_DESTROY,WM_COMMAND,WM_NOTIFY等等),和一些與窗口句柄(HWND)相關(guān)的函數(shù)(如GetWindowRect等等)。并且在_K_WINDOW_ROOT_IMPL的缺省構(gòu)造函數(shù)中完成了Thunk的初始化。這里主要說一下該類中的ProcessBaseMessage方法:
template <class T>
class __declspec(novtable) _K_WINDOW_ROOT_IMPL
{
_K_THUNKED_DATA _thunk;
public:
_K_WINDOW_ROOT_IMPL () : m_hWnd (NULL)
{
T* pThis = static_cast<T *>(this);
_thunk.Init ((DWORD_PTR) pThis, pThis->GetMessageProcPtr());
/* The above GetMessageProcPtr was defined in derived class KDialogImpl and KWindowImpl */
}
...
LRESULT ProcessBaseMessage (UINT msg, WPARAM wParam, LPARAM lParam)
{
T* pThis = static_cast<T *>(this); /* 'Override' support */
LRESULT r = __DO_DEFAULT;
switch (msg)
{
case WM_COMMAND:
r = pThis->OnCommand (HIWORD(wParam), LOWORD(wParam), (HWND) lParam); break;
case WM_NOTIFY:
r = pThis->OnNotify ((int) wParam, (LPNMHDR) lParam); break;
/* Other window message can be handled here*/
}
return r == __DO_DEFAULT ? pThis->DoDefault (msg, wParam, lParam) : r;
}
LRESULT OnCommand (WORD wNotifyCode, WORD wId, HWND hWndCtrl) { return __DO_DEFAULT; }
LRESULT OnNotify (int idCtrl, LPNMHDR pnmh) { return __DO_DEFAULT; }
LRESULT OnXXXXXX (...) { return __DO_DEFAULT; } /* 其他消息處理函數(shù)的定義 */
...
};
需要說明的是, 在該類中定義的大量(缺省)消息處理函數(shù),是為了能夠在其子類中“重載”這些消息處理函數(shù)。這里為了實現(xiàn)的缺省消息處理的簡單性使用了一種比較勉強(qiáng)的方法,即假定沒有任何消息處理函數(shù)會返回-2。比如,當(dāng)你在你的對話框類中“重載”OnCommand方法時(當(dāng)然,你的對話框類必須要繼承KDialogImpl),該方法就能夠在WM_COMMAND消息發(fā)生時被系統(tǒng)自動調(diào)用,關(guān)于基于模板的重載,請參見前面的章節(jié):使用C++模板實現(xiàn)函數(shù)的“重載”。
好,再讓我們來看看KDialogImpl的實現(xiàn):
template <class T>
class KDialogImpl
: public _K_WINDOW_ROOT_IMPL <T>
{
public:
KDialogImpl () : _K_WINDOW_ROOT_IMPL<T> () {}
inline DWORD GetMessageProcPtr ()
{
DWORD dwProcAddr = 0;
__asm
{ /* Use the prefix 'T::' to enable the overload of KDialogProc in derived class */
mov eax, offset T::KDialogProc
mov dword ptr [dwProcAddr], eax
}
return dwProcAddr;
}
INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam) { return __DO_DEFAULT; }
/* Other dialog-based message hanlder can be declared here */
LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam) { return 0; }
INT_PTR DoModal (
HWND hWndParent = ::GetActiveWindow( ),
LPARAM lpInitParam = NULL)
{
return DialogBoxParam (
m_hInstance, MAKEINTRESOURCE(T::IDD),
hWndParent,
(DLGPROC) GetThunkedProcPtr(), lpInitParam);
}
INT_PTR KCALLBACK KDialogProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
/* Cache the window handle when KDialogProc was called at first time */
if (m_hWnd == NULL)
m_hWnd = hWnd;
__ASSERT(m_hWnd == hWnd);
T* pThis = static_cast<T *> (this); /* 'Override' support */
INT_PTR fReturn = 0;
switch (msg)
{
case WM_INITDIALOG:
fReturn = pThis->OnInitDialog ((HWND) wParam, lParam);
break;
case WM_GETDLGCODE:
fReturn = pThis->OnGetDlgCode ((LPMSG) lParam);
break;
/* Other dialog-based message can be handled here */
default:
fReturn = __super::ProcessBaseMessage (msg, wParam, lParam);
}
if (fReturn == __DO_DEFAULT) return DoDefault (msg, wParam, lParam);
if (fReturn)
{
switch (msg)
{
case WM_COMPAREITEM :
case WM_VKEYTOITEM :
case WM_CHARTOITEM :
case WM_INITDIALOG :
case WM_QUERYDRAGICON :
case WM_CTLCOLORMSGBOX :
case WM_CTLCOLOREDIT :
case WM_CTLCOLORLISTBOX :
case WM_CTLCOLORBTN :
case WM_CTLCOLORDLG :
case WM_CTLCOLORSCROLLBAR :
case WM_CTLCOLORSTATIC :
break;
default:
::SetWindowLongPtr(m_hWnd, DWLP_MSGRESULT, (LONG) fReturn);
break;
}
}
return fReturn;
}
...
inline BOOL SetDlgItemInt (int nIDDlgItem, UINT uValue, BOOL bSigned = TRUE)
{ return ::SetDlgItemInt (m_hWnd, nIDDlgItem, uValue, bSigned); }
inline UINT IsDlgButtonChecked (int nIDButton)
{ return ::IsDlgButtonChecked (m_hWnd, nIDButton); }
/* 其他對話框常用函數(shù) */
};
最后我們來看看KWindowImpl的實現(xiàn),KWindowImpl的實現(xiàn)較KDialogImpl要復(fù)雜些,在KWindowImpl中,我們需要注冊特定的窗口類,另外,對于同一類型的窗口,在該類窗口的多實例下,我們需要保證每個實例的this指針能夠被正確傳遞到消息處理函數(shù)中,所以我們需在每次創(chuàng)建該類型的窗口時將該類窗口的窗口過程(Window Procedure)地址改為當(dāng)前實例的窗口過程。所以我們需要使用SetClassLongPtr來修改窗口類的消息處理函數(shù)地址,因此我們需要一個窗口句柄,這也就是為什么我要在KWindowImpl中定義類型為HWND的靜態(tài)變量_hWndLastCreated,我們需要通過它來調(diào)用函數(shù)SetClassLongPtr(該方法覺得很勉強(qiáng)。也許有更好的方法,但是目前沒有發(fā)現(xiàn))。
template <class T>
class KWindowImpl
: public _K_WINDOW_ROOT_IMPL <T>
{
LPTSTR m_lpszClassName;
static HWND _hWndLastCreated; /* work as sentinel */
static CRITICAL_SECTION _cs; /* Enable multi-thread safe support */
static BOOL _fInitilized;
public:
inline DWORD GetMessageProcPtr ()
{
DWORD dwProcAddr = 0;
__asm
{ /* Use the prefix 'T::' to enable the overload of KWndProc in derived class */
mov eax, offset T::KWndProc
mov dword ptr [dwProcAddr], eax
}
return dwProcAddr;
}
/* Disable the default constructor, without specifying the name of window class */
KWindowImpl () { __ASSERT (FALSE); }
KWindowImpl (LPCTSTR lpszClassName) : _K_WINDOW_ROOT_IMPL ()
{
m_lpszClassName = new TCHAR [_tcslen(lpszClassName) + _tcslen(_T("KWindowImpl")) + 8];
_stprintf (m_lpszClassName, _T("%s:%s"), _T("JKTL::KWindowImpl"), lpszClassName);
if (!_fInitilized)
{ ::InitializeCriticalSection (&_cs);
_fInitilized = TRUE;
}
}
~KWindowImpl ()
{ if (m_lpszClassName) delete[] m_lpszClassName; }
LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam)
{ return DefWindowProc (m_hWnd, msg, wParam, lParam); }
LRESULT KCALLBACK KWndProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (m_hWnd == NULL)
{ m_hWnd = hWnd;
_hWndLastCreated = m_hWnd;
/* Leaving the critical section, when window was created successfully */
::LeaveCriticalSection (&_cs);
}
__ASSERT (m_hWnd == hWnd);
/* Do some default message process */
return __super::ProcessBaseMessage (msg, wParam, lParam);
}
BOOL KRegisterClass ()
{
WNDCLASSEX wcx = {sizeof WNDCLASSEX};
/* Enable multi-thread safe, for SetClassLongPtr use ONLY, call here for convenience */
::EnterCriticalSection (&_cs);
if (GetClassInfoEx (m_hInstance, m_lpszClassName, &wcx))
{ /* Ensure that window subsquently created use it's thunked window procedure,
SetClassLongPtr will not effect those windows had been created before */
SetClassLongPtr (_hWndLastCreated, GCL_WNDPROC, (LONG) GetThunkedProcPtr ());
return TRUE;
}
wcx.cbClsExtra = 0;
wcx.cbWndExtra = 0;
wcx.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);;
wcx.hCursor = LoadCursor (NULL, IDC_ARROW);
wcx.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wcx.hInstance = m_hInstance;
wcx.lpfnWndProc = (WNDPROC) GetThunkedProcPtr ();
wcx.lpszClassName = m_lpszClassName;
wcx.lpszMenuName = NULL;
wcx.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
return RegisterClassEx (&wcx);
}
HWND Create (HWND hWndParent, DWORD dwExStyle, DWORD dwStyle, LPCTSTR lpszWndName,
RECT& rc, int nCtrlId = 0, LPVOID lpParam = NULL)
{
__ASSERT (m_hWnd == NULL);
if (!KRegisterClass ()) return NULL;
m_hWnd = ::CreateWindowEx (
dwExStyle, m_lpszClassName,
lpszWndName, dwStyle,
rc.left, rc.top, _RECT_W(rc), _RECT_H(rc),
hWndParent, (HMENU) nCtrlId, m_hInstance, lpParam);
return m_hWnd;
}
};
對于_K_WINDOW_ROOT_IMPL中的m_hWnd成員的賦值,我們是在我們的窗口過程第一次被調(diào)用是設(shè)定的(見代碼)。雖然我并不是很喜歡這種寫法,因為我不希望在窗口處理過程出現(xiàn)于消息處理無關(guān)的代碼。事實上,我們?nèi)匀豢梢栽赥hunk代碼中完成對m_hWnd的賦值,因為當(dāng)系統(tǒng)調(diào)用我們的“窗口處理過程”(即Thunk代碼)時,系統(tǒng)已經(jīng)為我們的窗體分配好了窗口句柄,并位于棧中的ESP+4位置(見圖1),我們完全可以在Thunk中將位于ESP+4處的值保存到m_hWnd中,但是這將增加我們在Thunk中的機(jī)器指令的長度,使用下面的代碼就要增加16字節(jié)的機(jī)器指令(__stdcall調(diào)用約定下,在__thiscall調(diào)用約定下,只需增加10個字節(jié)左右,因為可以直接使用ECX來進(jìn)行數(shù)據(jù)交換),在執(zhí)行Thunk代碼之前,我們需要計算出類對象中m_hWnd成員在該類對象中的偏移位置,這通常可以在類的構(gòu)造函數(shù)中完成(下面的代碼假定m_hWnd在類對象中的偏移位置為8):
PUSH DWORD PTR [ESP]
MOV DWORD PTR[ESP + 4], pThis
PUSH EAX ; Save EAX
PUSH EBX ; Save EBX
MOV EAX, pThis
MOV EBX, DWORD PTR [ESP + 0Ch] ; Load HWND from ESP + 0Ch
MOV DWORD PTR [EAX + 08h], EBX ; Set m_hWnd with value of EBX
POP EBX ; Restore EAX
POP EAX ; Restore EBX
JMP member function-based message handler
在__thiscall調(diào)用約定下為:
MOV ECX, pThis
PUSH EAX
MOV EAX, DWORD PTR [ESP + 8] ; Load HWND from ESP + 8
MOV DWORD PTR [EAX + 08h], EAX ; Save HWND to m_hWnd
POP EAX
JMP member function-based message handler
使用類成員函數(shù)子類化窗口過程
在_K_WINDOW_ROOT_IMPL中實現(xiàn)了SubclassWindow和SubclassDialog方法,你可以使用這兩個方法來輕松的實現(xiàn)窗體的子類化操作。下面的代碼演示了怎樣使用SubclassWindow來子類化輸入框(Edit Control):
class SubclassDemo
: public KDialogImpl <SubclassDemo>
{
_K_THUNKED_SUBCLASS_DATA* m_lpTsdEdit;
public:
INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam)
{
HWND hEdit = GetDlgItem (IDC_EDIT0);
m_lpTsdEdit = SubclassWindow (hEdit, EditWndProcHook);
return TRUE;
}
LRESULT KCALLBACK EditWndProcHook (HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
{
/* Do anything here you like */
return CallWindowProc (m_lpTsdEdit->fnOldWndProc, hEdit, msg, wParam, lParam);
}
}
結(jié)束語
非常高興你能夠讀到這篇文章的末尾,同時也希望這篇文章能對你理解Windows中的消息處理機(jī)制提供一些幫助。歡迎你將你的看法和建議反饋給我(JERKII@HOTMAIL.COM),以彌補(bǔ)由于我當(dāng)前的知識限制而導(dǎo)致的一些錯誤。
KWIN的源碼可以從這里下載。
謝謝!^_^