最近看了一篇讲ATL Thunk技术的好文章(下载),收获较多,在此做一个总结。
我们知道,经典的Windows程序设计采用纯Windows API来实现,创建一个窗口必须严格遵循“定义窗口类,注册窗口类,创建窗口,显示窗口,更新窗口,启动消息循环”的步骤。虽然经典的Windows程序设计在一定程度上已经体现出了OOP的思想 (对象 = 数据 + 代码),但是与当代的OOP (封装、继承、多态) 还有很大的差距。这种差距类似于C struct和C++ class之间的差距。
为了使Windows编程更加简洁高效,需要对纯Windows API进行封装。市场上的应用程序框架,比如MFC,WTL等,主要就做了这件事情。
按照什么思路来对纯Windows API进行封装呢?那就是对各个函数进行分组,比如许多函数的第一个参数都是HWND hwnd。何不把HWND hwnd作为类的成员变量,调用与hwnd相关的函数就不必再写hwnd参数了。这样就完成了第一步。
接下来问题来了:WndProc函数怎么处理呢?它不能是普通的类方法,因为编译器会给普通类方法悄悄添加一个this指针,从而导致参数格式与WNDPROC不相同。有办法,那就是在方法前面添加static,这样就能将类的WndProc传递给窗口类的lpfnWndProc字段了。这样就完成了第二步。
可是,真正问题才刚刚出现。WndProc现在是类的static方法,但是如果想要WndProc内部调用非static方法呢?要是能够把this指针传递进去就好了。首先想到的方法就是用全局变量。(不然就要增加函数参数,那句跟WNDPROC不同了) 使用全局变量有很大的弊端,那就是这个全局变量到底应该设多大呢?因为你很可能用相同的窗口类创建多个窗口,这时候WndProc都一样,只能以类对象实例的指针来区分。也有解决办法,大不了用动态内存。还有查找的问题呢?用Red-Black tree够快么?显然,这样思考下去就太复杂。就算花了大力气实现了这种思路,动态内存太慢了,查找也得花时间,这跟ATL的要求不吻合:速度快,尺寸小。
实际上ATL采用了另外一种技术来解决这个问题,当然就是“Thunk技术”。Thunk技术看似高深,那是因为关注它的人不多,资料不够充足而已。
Thunk就是“转换”的意思,从ATL的实现来看,它是一种采用小段汇编代码对WndProc参数hwnd进行偷梁换柱的方法。不要看到汇编就却步,因为它只涉及到两条指令:mov和jmp。博主对汇编也只停留在本科时所学的那点知识,而且忘得差不多,但是理解Thunk技术已经足够。
话不多说,下面是一个Thunk技术的模拟程序。窗口类的定义在zwindow.h头文件中,WinMain入口在simpleframework.cpp文件中。
// zwindow.h // 2012-11-17 by btwsmile // global class ZWindow; ZWindow* g_pWnd = NULL; #pragma pack(push, 1) typedef struct _WndProcThunk { DWORD m_mov; DWORD m_this; BYTE m_jmp; DWORD m_relproc; }THUNK; #pragma pack(pop) class ZWindow { public: THUNK m_thunk; HWND m_hwnd; void Init(WNDPROC proc, void* pThis) { m_thunk.m_mov = 0x042444c7; m_thunk.m_this = (DWORD)pThis; m_thunk.m_jmp = 0xe9; m_thunk.m_relproc = (int)proc - ((int)&m_thunk + sizeof(m_thunk)); FlushInstructionCache(GetCurrentProcess(), &m_thunk, sizeof(m_thunk)); } public: ZWindow() : m_hwnd(NULL) { } BOOL Create( LPCTSTR szClassName, LPCTSTR szWindowName, HINSTANCE hInstance, HWND hWndParent = NULL, DWORD dwStyle = WS_OVERLAPPEDWINDOW, HMENU hMenu = 0, int x = 200, int y = 200, int iWidth = 610, int iHeight = 420 ) { HWND hwnd = CreateWindow(szClassName, szWindowName, dwStyle, x, y, iWidth, iHeight, hWndParent, hMenu, hInstance, NULL); if(hwnd != m_hwnd) MessageBox(NULL, _T("hwnd != m_hwnd"), _T("ERROR"), MB_OK); return hwnd != NULL; } void Attach(HWND hwnd) { m_hwnd = hwnd; } BOOL ShowWindow(int iCmdShow) { return ::ShowWindow(m_hwnd, iCmdShow); } BOOL UpdateWindow() { return ::UpdateWindow(m_hwnd); } HDC BeginPaint(LPPAINTSTRUCT lpps) { return ::BeginPaint(m_hwnd, lpps); } BOOL EndPaint(LPPAINTSTRUCT lpps) { return ::EndPaint(m_hwnd, lpps); } BOOL GetClientRect(LPRECT lpRect) { return ::GetClientRect(m_hwnd, lpRect); } virtual BOOL OnPaint(WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; RECT rect; HDC hdc; hdc = BeginPaint(&ps); GetClientRect(&rect); rect.top += 20; DrawText(hdc, _T("Parent"), -1, &rect, DT_SINGLELINE | DT_CENTER); EndPaint(&ps); return TRUE; } virtual void OnLButtonUp(WPARAM wParam, LPARAM lParam) { MessageBox(m_hwnd, _T("Press Parent"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL); } // StartWndProc static LRESULT CALLBACK StartWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { ZWindow* pThis = g_pWnd; pThis->m_hwnd = hwnd; pThis->Init(WndProc, pThis); WNDPROC WindowProcess = (WNDPROC)&(pThis->m_thunk); SetWindowLong(hwnd, GWL_WNDPROC, (LONG)WindowProcess); return WindowProcess(hwnd, message, wParam, lParam); } // WndProc static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { ZWindow* pThis = (ZWindow*)hwnd; switch(message) { case WM_PAINT: pThis->OnPaint(wParam, lParam); break; case WM_LBUTTONUP: pThis->OnLButtonUp(wParam, lParam); break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProc(pThis->m_hwnd, message, wParam, lParam); } }; class ZDerivedWindow1 : public ZWindow { public: BOOL OnPaint(WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; RECT rect; HDC hdc; hdc = BeginPaint(&ps); GetClientRect(&rect); ::SetBkMode(hdc, TRANSPARENT); ::Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom); rect.top += 20; DrawText(hdc, _T("Derived 1"), -1, &rect, DT_SINGLELINE | DT_CENTER); EndPaint(&ps); return TRUE; } void OnLButtonUp(WPARAM wParam, LPARAM lParam) { MessageBox(m_hwnd, _T("Press Derived 1"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL); } }; class ZDerivedWindow2 : public ZWindow { public: BOOL OnPaint(WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; RECT rect; HDC hdc; hdc = BeginPaint(&ps); GetClientRect(&rect); ::SetBkMode(hdc, TRANSPARENT); ::Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom); rect.top += 20; DrawText(hdc, _T("Derived 2"), -1, &rect, DT_SINGLELINE | DT_CENTER); EndPaint(&ps); return TRUE; } void OnLButtonUp(WPARAM wParam, LPARAM lParam) { MessageBox(m_hwnd, _T("Press Derived 2"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL); } }; // simpleframework.cpp // 2012-11-17 by btwsmile #include <Windows.h> #include <tchar.h> #include "zwindow.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { TCHAR szAppName[] = _T("Thunk Model"); WNDCLASS wndclass; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.hCursor = LoadCursor(hInstance, IDC_ARROW); wndclass.hIcon = LoadIcon(hInstance, IDI_APPLICATION); wndclass.hInstance = hInstance; wndclass.lpfnWndProc = ZWindow::StartWndProc; wndclass.lpszClassName = szAppName; wndclass.lpszMenuName = NULL; wndclass.style = CS_VREDRAW | CS_HREDRAW; if( !RegisterClass(&wndclass) ) { MessageBox(NULL, _T("Fail to register wndclass"), _T("ERROR"), MB_OK); return 0; } ZWindow zw; g_pWnd = &zw; zw.Create(szAppName, szAppName, hInstance); zw.ShowWindow(iCmdShow); zw.UpdateWindow(); ZDerivedWindow1 zdw1; g_pWnd = &zdw1; zdw1.Create(szAppName, NULL, hInstance, zw.m_hwnd, WS_VISIBLE | WS_CHILD, NULL, 50, 50, 225, 300); ZDerivedWindow2 zdw2; g_pWnd = &zdw2; zdw2.Create(szAppName, NULL, hInstance, zw.m_hwnd, WS_VISIBLE | WS_CHILD , NULL, 325, 50, 225, 300); // message loop MSG msg; while( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }
在VC 6.0中编译后运行正常,如图所示:
点击不同的区域,会弹出相应的message box,说明thunk成功了。
请注意看WinMain函数中,wndclass.lpfnWndProc被赋值为了ZWindow::StartWndProc,所以zw、zdw1和zdw2窗口在收到第一条消息时,首先调用的是StartWndProc。然而StartWndProc做了什么呢?它对m_hwnd和m_thunk成员变量进行了赋值。然后将m_thunk强制转换成了WNDPROC类型的函数指针,并调用SetWindowLong函数将它设置成为窗口新的过程函数。对每个窗口来说,StartWndProc函数只被调用了一次,之后的消息都与它没有关系了。之后这个窗口的消息全部都会直接流向m_thunk代码,而m_thunk代码最终调用WndProc来完成消息的处理,不过它在调用之前将hwnd参数“纂改”成了窗口类实例的句柄。
可以说,StartWndProc只是昙花一现的过程函数,它“组装”m_thunk码并将用m_thunk来替换自己。我们甚至可以将StartWndProc看成是真实窗口过程函数的“工厂”,它在运行时创造出了“代码”,这也是为何Init函数的最后调用FlushInstructionCache的原因。
如果你将上述代码拷贝到VS2010,然后编译运行会遇到问题:FlushInstructionCache函数抛出异常。这是因为VS2010的数据执行保护起了作用。可以对Thunk做下面的改进:
(1) 将ZWindow的成员变量THUNK m_thunk该为THUNK* m_pThunk,然后将Init函数修改为:
THUNK* m_pThunk; void Init(WNDPROC proc, void* pThis) { m_pThunk = (THUNK*)VirtualAlloc(NULL, sizeof(THUNK), MEM_COMMIT, PAGE_EXECUTE_READWRITE); m_pThunk->m_mov = 0x042444c7; m_pThunk->m_this = PtrToLong(pThis); m_pThunk->m_jmp = 0xe9; m_pThunk->m_relproc = (int)proc - ((int)m_pThunk + sizeof(THUNK)); FlushInstructionCache(GetCurrentProcess, m_pThunk, sizeof(THUNK)); }
(2) StartWndProc函数需做相应的更改:
将WNDPROC WindowProcess = (WNDPROC)&(pThis->m_thunk);替换为WNDPROC WindowProcess = (WNDPROC)(pThis->m_pThunk);
如果你忍受不了例子中的全局变量g_pWnd,也有解决的方法。
(1) 删除ZWindow* g_wnd相关的代码。
(2) 修改ZWindow::Create方法,将CreateWindow函数的最后一个参数从NULL改为this。然后对StartWndProc函数做相应的更改:
// StartWndProc static LRESULT CALLBACK StartWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { ZWindow* pThis = (ZWindow*)((LPCREATESTRUCT)lParam)->lpCreateParams; pThis->m_hwnd = hwnd; pThis->Init(WndProc, pThis); WNDPROC WindowProcess = (WNDPROC)(pThis->m_pThunk); SetWindowLong(hwnd, GWL_WNDPROC, (LONG)WindowProcess); return WindowProcess(hwnd, message, wParam, lParam); }
文章写得比较粗糙,有些点可能未覆盖到,欢迎回帖交流!