现在分析ATL中窗口过程的实现。这部分功能在CWindowImplBaseT类中实现。
在Win32程序中,窗口过程(WndProc)是一个回调函数,且其指针保存在WNDCLASSEX结构体中,在窗口注册时传递给了操作系统。当窗口得到消息时,OS会调用窗口过程,通过一个大的switch-case语句块实现了消息的分发和处理。而在ATL中,以一种看似优雅的方式来封装这个过程。
首先,注意到在DECLARE_WND_CLASS(”分析一”)中,窗口注册传递的窗口过程是:
1: template <class TBase, class TWinTraits>
2: LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
它的建立起从CWindowImpl继承的窗口对象的HWND句柄与这个窗口对象实例的指针之间的映射。从而实现了将OS对窗口过程函数的调用转换成对窗口对象的某个成员函数的调用。这个稍后将会看到具体实例。不过这里有个问题,这个窗口句柄HWND到窗口对象指针的映射过程是何时建立的呢?答案是窗口对象接到第一个消息的时候,发生在CreateWindow返回HWND之前*。这之后,HWND就缓存在了CWindowImpl的后代窗口对象的数据成员中,然后对象的真正窗口过程就取代了这个StartWindowProc。这个过程有点像ARM的bootloader,处理完繁琐事情建立起干净环境之后,让真正的代码开始执行。StartWindowProc的实现如下:
1: CWindowImplBaseT< TBase, TWinTraits >* pThis =
(CWindowImplBaseT< TBase,TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
2: ATLASSERT(pThis != NULL);
3: if(!pThis)
4: {
5: return 0;
6: }
7: pThis->m_hWnd = hWnd;
8:
9: // Initialize the thunk. This is allocated in CWindowImplBaseT::Create,
10: // so failure is unexpected here.
11:
12: pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
13: WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
14: WNDPROC pOldProc
= (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
15: #ifdef _DEBUG
16: // check if somebody has subclassed us already since we discard it
17: if(pOldProc != StartWindowProc)
18: ATLTRACE(atlTraceWindowing, 0,
_T("Subclassing through a hook discarded.\n"));
19: #else
20: (pOldProc); // avoid unused warning
21: #endif
22: return pProc(hWnd, uMsg, wParam, lParam);
其中m_thunk这个数据成员有一些典故:ATL团队在实现从关联了不同windows消息的HWND句柄到用于处理这些消息的对象this指针映射时有多个选择。比如,建立一个全局表用于保存HWNDs到pointers的映射,但当随着窗口增多时查找时间将大的不可接受;可以将this指针强制保存在窗口数据中(如WNDCLASS结构的cbWndExtra数据成员中),但ATL/WTL的使用者很可能在操作窗口数据时将其覆盖或销毁。最终ATL团队采用了将多条汇编指令保存在一个thunk中的技术来解决这个问题。btw,这个实现比较丑陋,不可移植(当然巨硬也从未考虑将其移出x86)。这个thunk表现的即像一个对象(保存了其他对象的this指针)又像一个函数,这就是用ATL团队用汇编指令来实现它的原因。(不知可否这样理解,在c++中函数不是第一等对象,不可以运行时构造;而汇编语言一方面表现的像数据——二进制数,另一方面又是可执行的,可以强制让pc指向这段“数据”,然后执行语句,从而表现的像函数。巧妙和丑陋的集中体现)“machine instructions built on the fly, one per window.”每个CWindowImpl对象都拥有一个thunk,也即每个对象都有自己的窗口过程。
现在通过一个例子来说明其具体实现过程:
1: class CMywindow : public CWindowImpl<CMyWindow> {...};
2: CMyWindow wnd1; wnd1.Create(...);
3: CMyWindow wnd2; wnd2.Create(...);
thunk的任务是在调用CWindowImpl的静态成员函数WindowProc之前,将栈上的HWND用CWindowImpl对象的this指针替代。这些汇编指令保存在_WndProcThunk结构体中:
1: struct _stdcallthunk
2: {
3: DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
4: DWORD m_this; //
5: BYTE m_jmp; // jmp WndProc
6: DWORD m_relproc; // relative jmp
7: BOOL Init(DWORD_PTR proc, void* pThis)
8: {
9: m_mov = 0x042444C7; //C7 44 24 0C
10: m_this = PtrToUlong(pThis);
11: m_jmp = 0xe9;
12: m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
13: // write block from data cache and
14: // flush from instruction cache
15: FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
16: return TRUE;
17: } . . .
注意到在WTL8.0中,这个结构体的实现已经发生变化。整个thunk部分实现放在了atlstdthunk.h文件中,可以根据处理器的不同来分别编译进去。每个继承自CWindowImpl的类生成的对象都保存了一个这样的结构体,它在StartWindowProc中用对象的this指针初始化,并找到静态成员函数作为窗口的处理过程呢个。(译,额,生硬)。在StartWindowProc中调用的初始化函数即为上面的Init函数,也就是thunk这个对象。执行如上述代码:首先将pThis指针值移动到esp+4,即覆盖了HWND的地址;然后跳转到该窗口对象的窗口过程。最后将这些代码flush进指令cache,从而实现了运行时的程序跳转。整个过程如下图所示。这里有个疑问,它跳走后还会回来吗?后面还有一些语句要执行?
默认的窗口过程定义如下:
1: template <class TBase, class TWinTraits>
2: LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
首先将对象的pThis指针从调用栈中提取出:
1: CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
若重写GetWindProc虚函数,则此时所得到的hWnd已经是被用户替换之后的。提取出pThis指针后,再将当前的消息保存到m_pCurrentMsg中。
1: // set a ptr to this message and save the old value
2: _ATL_MSG msg(pThis->m_hWnd, uMsg, wParam, lParam);
3: const _ATL_MSG* pOldMsg = pThis->m_pCurrentMsg;
4: pThis->m_pCurrentMsg = &msg;
然后再将该消息传递给继承自CWindowImpl的窗口对象的虚成员函数ProcessWindowMessage,定义如下:
1: class ATL_NO_VTABLE CMessageMap
2: {
3: public:
4: virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam,LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID) = 0;
5: };
这个成员函数必须由继承自CWindowImpl的窗口类实现(纯虚),例如:
1: class CMywindow : public CWindowImpl<CMyWindow>
2: {
3: public:
4: virtual BOOL ProcessWindowMessage( HWND, UINT uMsg, WPARAM wParam,
5: LPARAM lParam, LRESULT lResult, DWORD /*dwMsgMapID*/)
6: {
7: BOOL bHandled = TRUE;
8: switch (uMsg) {
9: case WM_PAINT: lResult = OnPaint();break;
10: case WM_DESTROY:lResult = OnDestroy();break;
11: default: bHandled = FALSE;
12: }
13: return bHandled;
14: }
15:
16: private:
17: LRESULT onPaint()
18: {
19: PAINTSTRUCT ps;
20: HDC hdc = BeginPaint(&ps);
21: RECT rect;
22: GetClientRect( &rect);
23: DrawText(hdc, __T("Hello WTL"), -1, &rect,
24: DT_CENTRE);
25: EndPaint(&ps);
26: return 0;
27: }
28:
29: LRESULT onDestroy()
30: {
31: PostQuitMessage(0);
32: return 0;
33: }
34: };
至此消息处理函数从全局窗口过程转换成为一个成员函数,这可以使编程大大的方便。例如:在上例中所调用的BeginPaint,GetClientRect等方法都成为CMainWindow的成员函数(当然都尤其对应的win32API,但是我们都已经将其包装在CWindow中(见分析一),免去了时时要熟悉HWND参数,代码利落。而ProcessWindowMessage无法处理的消息,则会调用DefWindowProc来处理。
为了进一步方便使用,当WindowProc处理完最后一条消息或者HWND被删时,会调用OnFinalMessage,这可用于窗口应用的关闭的后续处理。在上例中,如果将onDestroy改为OnFinalMessage,并从消息分发中去掉WM_DESTROY分支,这个销毁语句仍然能够执行。
分析到这里,ATL关键的消息分发机制就暂告一段落,难点已经解决。可以看到在ProcessWindowMessage中包含了大量枯燥的分发式的代码,与win32程序并无二致;显然,我们还有偷懒的手段,见后续分析。