原文:http://blog.csdn.net/hw_henry2008/article/details/6453676
前几天刚看金山开源代码时写了一篇博客分析了一下其消息机制的实现方式。后来发现写的很多都是ATL里面的,最**的是犯了一个严重的错误,把ATL的窗口消息机制里面一个重要技术:实现HWND和对应窗口类this指针之间的映射的Thunk技术给忽略掉了。后来陈坤GG即时的提醒了我,先谢谢他了!
好了,步入正题,今天主要对比一下ATL和MFC是如何将窗口句柄HWND和对应的类的this指针映射的。
1. 先说一下为什么要映射:
我们自己写WIN32程序时从来没有映射呀,一般只是注册窗口的时候提供一个窗口过程,然后就在窗口过程里面做所有事情就可以了,为什么要映射呢?
我们知道WINDOW是用C写的,所有API不支持面向对象。可关键是ATL/MFC是一个框架,为了尽最大努力屏蔽编程上的繁琐步骤(注册窗口类,提供窗口过程,创建窗口,显示窗口···)和能够使开发人员能够用面向对象的方法来编程,享受极大的方便,就不得不在面向过程的操作系统API和面向对象编程框架直接搭个桥梁,这就是问题的开始···
之所以我们自己写的程序一般是一个主窗口对应一个窗口过程!所有不用关心这些了(而且我们一般WIN32编程也没有完全面向过程去写)。可是ATL/MFC不同,窗口过程不用我们提供,这样咱们编程就方便了,所有框架给我们提供了窗口过程,问题是,框架能为我们每个不同的窗口提供不同的窗口过程,向操纵系统注册吗?不能也无法实现。所以它只能向操纵系统提供一个统一的窗口过程,
1.对ATL来说是 CWindowImplBaseT< >::StartWindowProc这个静态函数,别忘了静态函数其实跟全局函数差不多,是基于类的,它没有this指针,编译器不会为它添加this指针。(其实这个过程有点曲折,待会说)
2.对MFC来说是注册时AfxDlgProc等,不过实际情况比这复杂,待会说。
不管注册时提供的窗口过程是谁,反正有一点是明确的:一定是一个全局函数或类的静态函数。
上面其实是很简单的。既然提供的都是一个相同的函数,那么不管哪个窗口有消息了,操作系统都会调用这一个函数!不同的是提供不同的参数,就是是HWND参数!该参数毫无疑问的标志了一个窗口。问题是,我们如何知道该窗口句柄所对应的窗口类是谁??一个简单的方法:if-else查表。对,窗口一多就很慢!
于是来到了我们讨论的重点:ATL/MFC是如何映射的?
一、先说ATL吧:
还是从源头来,先看怎么注册的:
具体是怎么注册的在我之前的文章里说过http://blog.csdn.net/hw_henry2008/archive/2011/05/22/6438153.aspx
这里简单回顾一下。
这里全部以对话框为基础,多文档也类似的。在DoModal函数里面,创建对话框时是这样的
- HWND CWindowImpl : public CWindowImplBaseT< TBase, TWinTraits >
- ::Create(HWND hWndParent, _U_RECT rect = NULL, LPCTSTR szWindowName = NULL,
- DWORD dwStyle = 0, DWORD dwExStyle = 0,
- _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)
- {
- //···
- ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
- //···Register函数完成注册,封装了全局RegisterClassW函数
- return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect, szWindowName,
- dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam);
- }
上面的代码调用GetWndClassInfo得到窗口结构的基本信息,其中即设置了窗口处理函数有必要贴一下 GetWndClassInfo的代码,它返回一个窗口类的基本信息,其中就包括了窗口处理函数,这是我们用来向操作系统注册的回调函数。
- static ATL::CWndClassInfo& CBkDialogImpl::GetWndClassInfo()
- {
- static ATL::CWndClassInfo wc = {
- { sizeof(WNDCLASSEX),
- CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS | (IsWinXPAndLater() ? CS_DROPSHADOW : 0),
- StartWindowProc, 0, 0, NULL, NULL, NULL,
- (HBRUSH)(COLOR_WINDOW + 1), NULL, NULL, NULL },
- NULL, NULL, IDC_ARROW, TRUE, 0, _T("")
- };
- return wc;
- }
从上面的代码看出:在向操纵系统注册的时候提供的窗口过程是StartWindowProc,在VC/atlmfc/include/atlwin.h里面。这样当第一个消息来的时候,操纵系统毫无疑问会调用我们的StartWindowProc上一次我看到这就没有怎么细看了以至于略过了重要的thunk技术。下面继续看该窗口过程
- LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- //下面的代码其实是从之前保存起来的一个窗口过程的this指针取出来。因为这个函数是第一次调用的,也即该HWND对应这this
- //不过我倒怀疑在多线程的时候这会出问题,你怎么保证保存的this指针不会和hWnd对应呢,如果两个线程同时运行到这,
- //那么不会竞争吗?改天看看。也请大家指点一下,呵呵···(o,我知道了,说完这点待会补充一下^.^)
- CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
- //···
- pThis->m_hWnd = hWnd;//保存一下句柄,以后会用的,为什么保存呢,因为thunk代码会覆盖这个地方的数据。
- //···//GetWindowProc返回的是WindowProc,也是一个静态函数
- pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);//这里待会说
- WNDPROC pProc = pThis->m_thunk.GetWNDPROC();//实际上返回的是一个_stdcallthunk结构体的首地址
- WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
- //将此结构体的首地址设置为窗口过程!!!???看下面
- //···
- return pProc(hWnd, uMsg, wParam, lParam);
- }
疑问来了,m_thunk是什么?每个窗口实例都有这么一个数据成员,定义如下
- class CWndProcThunk
- {
- public:
- _AtlCreateWndData cd;
- CStdCallThunk thunk;//这才是其重点。thunk其实就是一段汇编代码。
- BOOL Init(WNDPROC proc, void* pThis) {
- return thunk.Init((DWORD_PTR)proc, pThis);
- }
- WNDPROC GetWNDPROC() {
- return (WNDPROC)thunk.GetCodeAddress();
- }
- };
- #pragma pack(push,1)
- struct _stdcallthunk
- {
- DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
- DWORD m_this; //
- BYTE m_jmp; // jmp WndProc
- DWORD m_relproc; // relative jmp
- BOOL Init(DWORD_PTR proc, void* pThis)
- {//初始化这段汇编代码,待会会把它强制转换成为一个窗口过程函数的地址。只是不会进行压栈等操作
- m_mov = 0x042444C7; //C7 44 24 0C //后面的注释应该为//C7 44 24 04
- m_this = PtrToUlong(pThis);
- //把this指针移到堆栈的4个字节开始位置。从StartWindowProc的调用规则CALLBACK看出这个位置正好是窗口句柄
- m_jmp = 0xe9;
- m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
- //上面相对跳转
- FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));//刷新CPU预读的指令
- return TRUE;
- }
- void* GetCodeAddress() {
- return this;//返回本结构体的首地址,注意幸好没有虚函数,你懂的。
- }
- //···
- };
上面的代码再多说一下,Init其实就是初始化了thunk结构,把它初始化成这样:
先把堆栈上的4字节处的内容改成传入的对应HWND窗口类的this指针,然后跳转到函数指针proc处执行,其实为WindowProc。
为什么这能够实现呢?
我们知道,StartWindowProc用WindowProc和对应窗口过程的this指针初始化了thunk,然后把这个thunk的“内容”(其实是一段精心安排的汇编代码)强制转换成为窗口过程函数,然后向操作系统注册! 想想这回产生什么效果呢??对,从此以后,每当“该窗口”有消息到来的时候,操作系统会以参数:
( hWnd, uMsg, wParam, lParam) 理所当然的调用该地址处的“函数”!!说的细一点,操作系统内会进行如下动作:
(这里与函数的调用约定有关,不清楚的请参考http://blog.csdn.net/hw_henry2008/archive/2011/05/29/6453257.aspx)
--------------------------------------------------------------
- 01111:
- push lParam
- push wParam
- push uMsg
- push hWnd //CALLBACK约定的压栈规则:从右到左
- push cs:eip //保存返回地址,即指令指针
- call 0x112345678 //假设这是thunk结构体的基址。
- 0x112345678:
- move this [esp+0x04] //看看压栈时的栈状态就知道,该窗口类的this正好覆盖了参数中的hWnd !!
- jmp WindowProc //然后若无其事的跳转到WindowProc去执行。
-----------------------------------------------------------------
此时的堆栈状态为:
lParam
wParam
uMsg
hWnd <----esp+4
cs:eip <----esp寄存器
------------------------------------------------------------------
看到这我们大概知道了,thunk技术在此的用途其实就是:将堆栈中的hWnd改成对应的窗口类this指针,注意thunk结构是每个窗口实例一个的,所以其实这个thunk中不同的地方就只有move的源地址不同,即窗口实例this指针不同于是,以后的每一个消息,操作系统不再调用StartWindowProc函数了,取而代之调用thunk处的代码。
我们继续看WindowProc是如何取得这个this的,毕竟它也是个静态函数。
- LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- //直接把参数hWnd强制转换成窗口类的指针!!为什么能成功呢?
- //因为刚才thunk代码中move this [esp+0x4]正好将此参数覆盖了!!而且thunk代码时每个窗口实例一个。
- CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
- //····
- //巧妙的取得了对应窗口过程的this指针,于是大摇大摆的调用其成员函数啦,
- //当然,m_hWnd句柄第一次也是唯一一次进入StartWindowProc就保存了的。
- BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
- //···
- }
对于ATL的消息分配过程就是上面所说的了,关于thunk的一些细节可以参考我转载的博客。
此外需要稍微了解点汇编语言,调用约定。这些在我转载的博客中有参考.
基本图示如下:
另外补充一下,刚才在StartWindowProc中的代码:
_AtlWinModule.ExtractCreateWndData()我开始觉得会有线程竞争问题出现,刚刚看了下里面的实现,是没问题的,里面不断加了锁,而且还用了“每线程变量”似的处理。
- ATLINLINE ATLAPI_(void*) AtlWinModuleExtractCreateWndData(_ATL_WIN_MODULE* pWinModule)
- {
- //···//下面枷锁,退出此函数解锁
- CComCritSecLock<CComCriticalSection> lock(pWinModule->m_csWindowCreate, false);
- if (FAILED(lock.Lock()))
- {//····
- }
- _AtlCreateWndData* pEntry = pWinModule->m_pCreateWndList;
- if(pEntry != NULL) {
- DWORD dwThreadID = ::GetCurrentThreadId();
- _AtlCreateWndData* pPrev = NULL;
- while(pEntry != NULL) {
- if(pEntry->m_dwThreadID == dwThreadID)
- {//关键是这,只处理当前线程,对于多线程来说,他们访问的是不同的元素
- if(pPrev == NULL)
- pWinModule->m_pCreateWndList = pEntry->m_pNext;
- else
- pPrev->m_pNext = pEntry->m_pNext;
- pv = pEntry->m_pThis;
- break;
- }
- pPrev = pEntry;
- pEntry = pEntry->m_pNext;
- }
- }
- return pv;