MFC功能已經非常強大,自己做界面庫也許沒什麼意思,但是這個過程中卻能學到很多東西。比如說:
窗口類的封裝,從全局窗口消息處理到窗口對象消息處理的映射方法:
對界面進行封裝,一般都是一個窗口一個類,比如實現一個最基本的窗口類CMyWnd,你一定會把窗口過程作為這個類的成員函數,但是使用WINAPI創建窗口時必須注冊類WNDCLASS,裡面有個成員數據lpfnWndProc需要WNDPROC的函數指針,一般想法就是把窗口類的消息處理函數指針傳過去,但是類成員函數除非是靜態的,否則無法轉換到WNDPROC,而全局的消息處理函數又無法得到窗口類對象的指針。這裡有幾種解決辦法:
一種解決方法是用窗口列表,開一個結構數組,窗口類對象創建窗口的時候把窗口HWND和this指針放入數組,全局消息處理函數遍歷數組,利用HWND找出this指針,然後定位到對象內部的消息處理函數。這種方法查找對象的時間會隨著窗口個數的增多而增長。
另一種方法比較聰明一點,WNDCLASS裡面有個成員數據cbWndExtra一般是不用的,利用這點,注冊類時給該成員數據賦值,這樣窗口創建時系統會根據該值開辟一塊內存與窗口綁定,這時把創建的窗口類的指針放到該塊內存,那麼在靜態的窗口消息循環函數就能利用GetWindowLong(hWnd,GWL_USERDATA)取出該指針,return (CMyWnd*)->WindowProc(...),這樣就不用遍歷窗口了。但是這樣一來就有個致命弱點,對窗口不能調用SetWindowLong(hWnd,GWL_USERDATA,數據),否則就會導致程序崩潰。幸好這個函數(特定這幾個參數)是調用幾率極低的,對於窗口,由於創建窗口都是調用窗口類的Create函數,不用手工注冊WNDCLASS類,也就不會調用SetWindowLong函數。但是畢竟缺乏安全性,而且當一秒鐘內處理的窗口消息很多時,這種查找速度也可能不夠快。
還有一種就是比較完美的解決辦法,稱之為thunk技術。thunk是一組動態生成的ASM指令,它記錄了窗口類對象的this指針,並且這組指令可以當作函數,既也可以是窗口過程來使用。thunk先把窗口對象this指針記錄下來,然後轉向到靜態stdProc回調函數,轉向之前先記錄HWND,然後把堆棧裡HWND的內容替換為this指針,這樣在stdProc裡就可以從HWND取回對象指針,定位到WindowProc了。
我們先來看看窗口過程函數定義:
LRESULT WINAPI WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
其實當我們的窗口類CMyWnd創建窗口的時候,窗口句柄是可以得到並且作為成員數據保存,如此一來,第一個參數hWnd是可以不要的,因為可以通過this->m_hWnd得到,我們可以在這裡做手腳,hWnd其實質是一個指針,如果把這個參數替換為窗口類對象的this指針,那麼我們不就可以通過(CMyWnd*)hWnd->WindowProc轉到窗口類內部的窗口過程了嗎?但是窗口過程是系統調用的,怎麼能把hWnd替換掉呢?我們先來看看系統調用這個函數時的堆棧情況:
系統調用m_thunk時的堆棧:
ret HWND MSG WPARAM LPARAM
-------------------------------------------
棧頂 棧底
系統把參數從右到左依次壓棧,最後把返回地址壓棧,我們只要在系統調用窗口過程時修改堆棧,把其中的hWnd參數替換掉就行了。這時thunk技術就有用武之地了,我們先定義一個結構:
#pragma pack(push,1) //該結構必須以字節對齊
struct Thunk {
BYTE Call;
int Offset;
WNDPROC Proc;
BYTE Code[5];
CMyWnd* Window;
BYTE Jmp;
BYTE ECX;
};
#pragma pack(pop)
類定義:
class CMyWnd
{
public:
BOOL Create(...);
LRESULT WINAPI WindowProc(UINT,WPARAM,LPARAM);
static LRESULT WINAPI InitProc(HWND,UINT,WPARAM,LPARAM);
static LRESULT WINAPI stdProc(HWND,UINT,WPARAM,LPARAM);
WNDPROC CreateThunk();
WNDPROC GetThunk(){return m_thunk}
...
private:
WNDPROC m_thunk
}
在創建窗口的時候把窗口過程設定為this->m_thunk,m_thunk的類型是WNDPROC,因此是完全合法的,當然這個m_thunk還沒有初始化,在創建窗口前必須初始化:
WNDPROC CMyWnd::CreateThunk()
{
Thunk* thunk = new Thunk;
///////////////////////////////////////////////
//
//系統調用m_thunk時的堆棧:
//ret HWND MSG WPARAM LPARAM
//-------------------------------------------
//棧頂 棧底
///////////////////////////////////////////////
//call Offset
//調用code[0],call執行時會把下一條指令壓棧,即把Proc壓棧
thunk->Call = 0xE8; // call [rel]32
thunk->Offset = (size_t)&(((Thunk*)0)->Code)-(size_t)&(((Thunk*)0)->Proc); // 偏移量,跳過Proc到Code[0]
thunk->Proc = CMyWnd::stdProc; //靜態窗口過程
//pop ecx,Proc已壓棧,彈出Proc到ecx
thunk->Code[0] = 0x59; //pop ecx
//mov dWord ptr [esp+0x4],this
//Proc已彈出,棧頂是返回地址,緊接著就是HWND了。
//[esp+0x4]就是HWND
thunk->Code[1] = 0xC7; // mov
thunk->Code[2] = 0x44; // dWord ptr
thunk->Code[3] = 0x24; // disp8[esp]
thunk->Code[4] = 0x04; // +4
thunk->Window = this;
//偷梁換柱成功!跳轉到Proc
//jmp [ecx]
thunk->Jmp = 0xFF; // jmp [r/m]32
thunk->ECX = 0x21; // [ecx]
m_thunk = (WNDPROC)thunk;
return m_thunk;
}
這樣m_thunk雖然是一個結構,但其數據是一段可執行的代碼,而其類型又是WNDPROC,系統就會忠實地按窗口過程規則調用這段代碼,m_thunk就把Window字段裡記錄的this指針替換掉堆棧中的hWnd參數,然後跳轉到靜態的stdProc:
//本回調函數的HWND調用之前已由m_thunk替換為對象指針
LRESULT WINAPI CMyWnd::stdProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam)
{
CMyWnd* w = (CMyWnd*)hWnd;
return w->WindowProc(uMsg,wParam,lParam);
}
這樣就把窗口過程轉向到了類成員函數WindowProc,當然這樣還有一個問題,就是窗口句柄hWnd還沒來得及記錄,因此一開始的窗口過程應該先定位到靜態的InitProc,CreateWindow的時候給最後一個參數,即初始化參數賦值為this指針:
CreateWindowEx(
dwExStyle,
szClass,
szTitle,
dwStyle,
x,
y,
width,
height,
hParentWnd,
hMenu,
hInst,
this //初始化參數
);,
在InitProc裡面取出該指針:
LRESULT WINAPI CMyWnd::InitProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam)
{
if(uMsg == WM_NCCREATE)
{
CMyWnd *w = NULL;
w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams;
if(w)
{
//記錄hWnd
w->m_hWnd = hWnd;
//改變窗口過程為m_thunk
SetWindowLong(hWnd,GWL_WNDPROC,(LONG)w-CreateThunk());
return (*(WNDPROC)(w->GetThunk()))(hWnd,uMsg,wParam,lParam);
}
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
這樣就大功告成。
窗口過程轉發流程:
假設已建立CMyWnd類的窗口對象 CMyWnd *window,初始化完畢後調用window->Create,這時Create的窗口其窗口過程函數是靜態CMyWnd::InitWndProc
題外話:thunk的匯編代碼全部寫在注釋裡了,把這段匯編轉成數據可費了不少勁,當時手頭沒有合適的工具,只有一本《8086/8088匯編語言程序設計》,根據附錄中的指令碼匯總表轉成機器碼數據,那裡面根本沒有EAX,ECX,ESP等的概念,只能連蒙帶猜加調試,非法操作了n(n>10)回才得到那些數據,當時真是長出了一口氣:TNND,終於搞定了!:-)