由于 C++ 成員函數的調用機制問題,對C語言回調函數的 C++ 封裝是件比較棘手的事。為了保持C++對象的獨立性,理想情況是將回調函數設置到成員函數,而一般的回調函數格式通常是普通的C函數,尤其是 Windows API 中的。好在有些回調函數中留出了一個額外參數,這樣便可以由這個通道將 this 指針傳入。比如線程函數的定義為:
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
LPVOID lpThreadParameter
);
typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;
這樣,當我們實現線程類的時候,就可以:
class Thread
{
private:
HANDLE m_hThread;
public:
BOOL Create()
{
m_hThread = CreateThread(NULL, 0, StaticThreadProc, (LPVOID)this, 0, NULL);
return m_hThread != NULL;
}
private:
DWORD WINAPI ThreadProc()
{
// TODO
return 0;
}
private:
static DWORD WINAPI StaticThreadProc(LPVOID lpThreadParameter)
{
((Thread *)lpThreadParameter)->ThreadProc();
}
};
不過,這樣,成員函數 ThreadProc() 便喪失了一個參數,這通常無傷大雅,任何原本需要從參數傳入的信息都可以作為成員變量讓 ThreadProc 來讀寫。如果一定有些什么是非從參數傳入不可的,那也可以,一種做法,創建線程的時候傳入一個包含 this 指針信息的結構。第二種做法,對該 class 作單例限制——如果現實情況允許的話。
所以,有額外參數的回調函數都好處理。不幸的是,Windows 的窗口回調函數沒有這樣一個額外參數:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
這使得對窗口的 C++ 封裝變得困難。為了解決這個問題,一個很自然的想法是,維護一份全局的窗口句柄到窗口類的對應關系,如:
#include <map>
class Window
{
public:
Window();
~Window();
public:
BOOL Create();
protected:
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
HWND m_hWnd;
protected:
static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static std::map<HWND, Window *> m_sWindows;
};
在 Create 的時候,指定 StaticWndProc 為窗口回調函數,并將 hWnd 與 this 存入 m_sWindows:
BOOL Window::Create()
{
LPCTSTR lpszClassName = _T("ClassName");
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
wcex.lpfnWndProc = StaticWndProc;
wcex.hInstance = hInstance;
wcex.lpszClassName = lpszClassName;
RegisterClassEx(&wcex);
m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (m_hWnd == NULL)
{
return FALSE;
}
m_sWindows.insert(std::make_pair(m_hWnd, this));
ShowWindow(m_hWnd, SW_SHOW);
UpdateWindow(m_hWnd);
return TRUE;
}
在 StaticWindowProc 中,由 hWnd 找到 this,然后轉發給成員函數:
LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
std::map<HWND, Window *>::iterator it = m_sWindows.find(hWnd);
assert(it != m_sWindows.end() && it->second != NULL);
return it->second->WndProc(message, wParam, lParam);
}
(m_sWindows 的多線程保護略過,下同)
據說 MFC 采用的就是類似的做法。缺點是,每次 StaticWndProc 都要從 m_sWindows 中去找 this。由于窗口類一般會保存窗口句柄,回調函數里的 hWnd 就沒多大作用了,如果這個 hWnd 能夠被用來存 this 指針就好了,那么就能寫成這樣:
LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
return ((Window *)hWnd)->WndProc(message, wParam, lParam);
}
這樣看上去就爽多了。傳說中 WTL 所采取的 thunk 技術就是這么干的。之前,只是聽過這遙遠的傳說,今天,終于有機會走進這個傳說去看一看。參考資料是一篇不知原始出處的文章《深入剖析WTL—WTL框架窗口分析》,以及部分 WTL 8.0 代碼,還有其他亂七八糟的文章。
WTL 的思路是,每次在系統調用 WndProc 的時候,讓它鬼使神差地先走到我們的另一處代碼,讓我們有機會修改堆棧中的 hWnd。這處代碼可能是類似這樣的:
__asm
{
mov dword ptr [esp+4], pThis ;調用 WndProc 時,堆棧結構為:RetAddr, hWnd, message, wParam, lParam, ... 故 [esp+4]
jmp WndProc
}
由于 pThis 和 WndProc 需要被事先修改(但又無法在編譯前定好),所以我們需要運行的時候去修改這部分代碼。先弄一個小程序探測下這兩行語句的機器碼:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
return 0;
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
MessageBox(NULL, NULL, NULL, MB_OK);
__asm
{
mov dword ptr [esp+4], 1
jmp WndProc
}
return 0;
}
最前面的 MessageBox 是為了等下調試的時候容易找到進入點。
然后使用 OllyDbg,在 MessageBoxW 上設置斷點,執行到該函數返回:

這里我們看到,mov dword ptr [esp+4] 的機器碼為 C7 44 24 04,后面緊接著的一個 DWORD 是 mov 的第二個操作數。jmp 的機器碼是 e9,后面緊接著的一個 DWORD 是跳轉的相對地址。其中 00061000h - 0006102Bh = FFFFFFD5h。
于是定義這樣一個結構:
#pragma pack(push,1)
typedef struct _StdCallThunk
{
DWORD m_mov; // = 0x042444C7
DWORD m_this; // = this
BYTE m_jmp; // = 0xe9
DWORD m_relproc; // = relative distance
} StdCallThunk;
#pragma pack(pop)
這個結構可以作為窗口類的成員變量存在。我們的窗口類現在變成了這樣子:
class Window
{
public:
Window();
~Window();
public:
BOOL Create();
protected:
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
HWND m_hWnd;
StdCallThunk m_thunk;
protected:
static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};
似乎少了點什么……創建窗口的時候,我們是不能直接把回調函數設到 StaticWndPorc 中去的,因為這個函數是希望被寫成這樣子的:
LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
return ((Window *)hWnd)->WndProc(message, wParam, lParam);
}
那么至少需要一個臨時的回調函數,在這個函數里去設置新的回調函數(設到 m_thunk 上),再由 m_thunk 來調用 StaticWndProc,StaticWndProc 再去調用 WndProc,這樣整個過程就通了。
但是,臨時回調函數還是需要知道從 hWnd 到 this 的對應關系。可是現在我們不能照搬用剛才的 m_sWindows 了。因為窗口在創建過程中就會調用到回調函數,需要使用到 m_sWindows 里的 this,而窗口被成功創建之前,我們沒法提前拿到 HWND 存入 m_sWindows。現在,換個方法,存當前線程 ID 與 this 的對應關系。這樣,這個類變成了:
#include <map>
class Window
{
public:
Window();
~Window();
public:
BOOL Create();
protected:
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
HWND m_hWnd;
StdCallThunk m_thunk;
protected:
static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static std::map<DWORD, Window *> m_sWindows;
}; 然后實現 Create 和 TempWndProc:
BOOL Window::Create()
{
LPCTSTR lpszClassName = _T("ClassName");
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
wcex.lpfnWndProc = TempWndProc;
wcex.hInstance = hInstance;
wcex.lpszClassName = lpszClassName;
RegisterClassEx(&wcex);
DWORD dwThreadId = GetCurrentThreadId();
m_sWindows.insert(std::make_pair(dwThreadId, this));
m_thunk.m_mov = 0x042444c7;
m_thunk.m_jmp = 0xe9;
m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (m_hWnd == NULL)
{
return FALSE;
}
ShowWindow(m_hWnd, SW_SHOW);
UpdateWindow(m_hWnd);
return TRUE;
}
LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());
assert(it != m_sWindows.end() && it->second != NULL);
Window *pThis = it->second;
m_sWindows.erase(it);
WNDPROC pWndProc = (WNDPROC)&pThis->m_thunk;
pThis->m_thunk.m_this = (DWORD)pThis;
pThis->m_thunk.m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)&pThis->m_thunk + sizeof(StdCallThunk));
m_hWnd = hWnd;
SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);
return pWndProc(hWnd, message, wParam, lParam);
}
差不多可以了,調試一下。結果,在 thunk 的第一行出錯了。我原以為地址算錯了神馬的,嘗試把 thunk.m_mov 改為 0x90909090,再運行,還是出錯。于是傻掉了……過了好一會兒才意識到,可能是因為 thunk 在數據段,無法被執行。可是,很久很久以前偶滴一個敬愛的老師在 TC 中鼓搗程序運行時改變自身代碼時,貌似無此問題啊。。。然后查呀查,原來是 Windows 在的數據執行保護搞的鬼。于是,需要用 VirtualAlloc 來申請一段有執行權限的內存。WTL 里面也是這么做的,不過它似乎維護了一塊較大的可執行內存區作為 thunk 內存池,我們這里從簡。最后,整個流程終于跑通了。最終代碼清單如下:
#include <Windows.h>
#include <assert.h>
#include <map>
#include <tchar.h>
#pragma pack(push,1)
typedef struct _StdCallThunk
{
DWORD m_mov;
DWORD m_this;
BYTE m_jmp;
DWORD m_relproc;
} StdCallThunk;
#pragma pack(pop)
class Window
{
public:
Window();
~Window();
public:
BOOL Create();
protected:
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
HWND m_hWnd;
StdCallThunk *m_pThunk;
protected:
static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static std::map<DWORD, Window *> m_sWindows;
};
std::map<DWORD, Window *> Window::m_sWindows;
Window::Window()
{
}
Window::~Window()
{
VirtualFree(m_pThunk, sizeof(StdCallThunk), MEM_RELEASE);
}
BOOL Window::Create()
{
LPCTSTR lpszClassName = _T("ClassName");
HINSTANCE hInstance = GetModuleHandle(NULL);
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
wcex.lpfnWndProc = TempWndProc;
wcex.hInstance = hInstance;
wcex.lpszClassName = lpszClassName;
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassEx(&wcex);
DWORD dwThreadId = GetCurrentThreadId();
m_sWindows.insert(std::make_pair(dwThreadId, this));
m_pThunk = (StdCallThunk *)VirtualAlloc(NULL, sizeof(StdCallThunk), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
m_pThunk->m_mov = 0x042444c7;
m_pThunk->m_jmp = 0xe9;
m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (m_hWnd == NULL)
{
return FALSE;
}
ShowWindow(m_hWnd, SW_SHOW);
UpdateWindow(m_hWnd);
return TRUE;
}
LRESULT Window::WndProc(UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_LBUTTONUP:
MessageBox(m_hWnd, _T("LButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
break;
case WM_RBUTTONUP:
MessageBox(m_hWnd, _T("RButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProc(m_hWnd, message, wParam, lParam);
}
LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());
assert(it != m_sWindows.end() && it->second != NULL);
Window *pThis = it->second;
m_sWindows.erase(it);
WNDPROC pWndProc = (WNDPROC)pThis->m_pThunk;
pThis->m_pThunk->m_this = (DWORD)pThis;
pThis->m_pThunk->m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)pThis->m_pThunk + sizeof(StdCallThunk));
pThis->m_hWnd = hWnd;
SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);
return pWndProc(hWnd, message, wParam, lParam);
}
LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
return ((Window *)hWnd)->WndProc(message, wParam, lParam);
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
Window wnd;
wnd.Create();
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
剛才有一處,存 this 指針的時候,我很武斷地把它與當前線程 ID 關聯起來了,其實這正是 WTL 本身的做法。它用 CAtlWinModule::AddCreateWndData 存的 this,最終會把當前線程 ID 和 this 作關聯。我是這么理解的吧,同一線程不可能同時有兩處在調用 CreateWindow,所以這樣取回來的 this 是可靠的。
好了,到此為止,邊試驗邊記錄的,不知道理解是否正確。歡迎指出不當之處,也歡迎提出相關的問題來考我,歡迎介紹有關此問題的新方法、新思路,等等,總之,請各位看官多指教哈。
posted on 2010-10-24 16:44
溪流 閱讀(6604)
評論(40) 編輯 收藏 引用 所屬分類:
C++ 、
Windows