博客已迁移至:http://kulv.sinaapp.com/,这里不再使用

ATL与MFC消息分发机制的对比---由金山开源代码引出的思考 (一)


前几天刚看金山开源代码时写了一篇博客分析了一下其消息机制的实现方式。后来发现写的很多都是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 WndProcDWORD m_relproc; // relative jmpBOOL Init(DWORD_PTR proc, void* pThis){//初始化这段汇编代码,待会会把它强制转换成为一个窗口过程函数的地址。只是不会进行压栈等操作m_mov = 0x042444C7; //C7 44 24 0C //后面的注释应该为//C7 44 24 04m_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 lParampush wParampush uMsgpush 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;elsepPrev->m_pNext = pEntry->m_pNext;pv = pEntry->m_pThis;break;}pPrev = pEntry;pEntry = pEntry->m_pNext;}}return pv;}

二、MFC的实现方式:

限于篇幅,请看下页:http://blog.csdn.net/hw_henry2008/archive/2011/05/29/6453730.aspx

ATL与MFC消息分发机制的对比---由金山开源代码引出的思考(一)相关推荐

  1. MFC消息响应机制及映射机制理解

    一.MFC消息响应机制分析 ---- MFC是Windows下程序设计的最流行的一个类库,但是该类库比较庞杂,尤其是它的消息映射机制,更是涉及到很多低层的东西,我们在这里,对它的整个消息映射机制进行了 ...

  2. delphi VCL研究之消息分发机制(转)

    原文来源,http://blog.csdn.net/sushengmiyan/article/details/8635550 1.VCL 概貌 先看一下VCL类图的主要分支,如图4.1所示. 在图中可 ...

  3. 【MFC系列-第9天】MFC消息映射机制的原理

    关注公号[逆向通信猿]更精彩!!! 第9天 MFC消息映射机制的原理 9.1 对话框常用的回调函数 a)窗口创建时的消息和虚函数包括:WM_CREATE,WM_INITDIALOG,和PreSubcl ...

  4. 用MFC消息映射机制自定义消息

    Windows系统中的程序大部分都是通过消息和事件驱动的.在windows下的应用程序主要工作是进行消息的循环处理,通过循环等待消息的到来和事件的发生,然后对不同的消息和事件运行相关的代码,完成相应的 ...

  5. 文化袁探索专栏——消息分发机制

    文化袁探索专栏--Activity.Window和View三者间关系 文化袁探索专栏--View三大流程#Measure 文化袁探索专栏--View三大流程#Layout 文化袁探索专栏--消息分发机 ...

  6. Cocos2d-x 3.0 屏幕触摸及消息分发机制

    ***************************************转载请注明出处:http://blog.csdn.net/lttree************************** ...

  7. kafka 消息分发机制、分区和副本机制

    一.消息分发机制 1.1 kafka 消息分发策略 消息是 kafka 中最基本的数据单元,在 kafka 中,一条消息由key.value两部分构成,在发送一条消息 时,我们可以指定这个key,那么 ...

  8. VC++/MFC消息映射机制(1):MFC消息映射原理

    VC++/MFC消息映射机制(1):模仿MFC的消息映射原理 本文为原创文章,转载请注明出处,或注明转载自"黄邦勇帅(原名:黄勇) <C++语法详解>网盘地址:https://p ...

  9. MFc消息映射机制理解

    何谓消息.消息处理函数.消息映射? 消息简单的说就是指通过输入设备向程序发出指令要执行某个操作.具体的某个操作是你的一系列代码.称为消息处理函数.在SDK中消息其实非常容易理解,当窗口建立后便会有一个 ...

最新文章

  1. ajax查找错误信息
  2. 浅谈一下这个所谓的特殊算法——动态规划?
  3. spring——使用xml声明式事务整合jdbc——GRUD
  4. qtp启动java程序_转: QTP六脉神剑之调用Java程序
  5. 【转】敏捷开发,你真的做对了吗?
  6. softlayer virtual machine vhd磁盘镜像导入shell脚本
  7. 【恋上数据结构】动态规划(找零钱、最大连续子序列和、最长上升子序列、最长公共子序列、最长公共子串、0-1背包)
  8. MPC模型预测控制器——控制器建模+MATLAB编程
  9. 电子书:《网页木马攻防实战》
  10. 小白新建C语言程序(VS2019创建C语言编程环境方法详解)
  11. 微信公众号新的消息模板还能换行吗?
  12. LONG RAW转换BLOB
  13. Springboot启动提示:com.alibaba.nacos.shaded.io.grpc.StatusRuntimeException: UNAVAILABLE: io exception
  14. 王者nba服务器维护,王者NBA新手常用问题FAQ大全
  15. 系统集成资质取消后 ,偷偷崛起的ICSCE资质(信息化能力和信用评价资质)
  16. 常见的markdown语法总结(不断更新中......)
  17. 【uniapp 动态设置 起始页 默认展示页面 】
  18. 12月26日学习记录
  19. 生产环境kubeadm部署k8s(1.23)高可用集群
  20. DataPipeline宣布完成数千万元B轮融资,加速构建中国的世界级数据中间件产品

热门文章

  1. 【蓝桥杯单片机组第十一届省赛】— 程序设计部分(第二场)
  2. Vue中的slot和slot-scope使用
  3. Vijos1404 遭遇战 (SPFA)
  4. pydicom读取头文件_python读取dicom图像(SimpleITK和dicom包实现)_愿十四亿神州尽舜尧-CSDN博客_python读取dicom...
  5. python字典均值_Python常用基础语法(函数)汇总
  6. oracle审计日志时长,oracle 审计日志清理
  7. 郑州大河宸院三期信息汇总
  8. “操作无法完成因为其中的文件夹或文件已在另一个程序中打开”解决办法
  9. 交换机常用的命令以及使用方法
  10. Deformable Convolution