ATL & WTL实现分析 (二)

现在分析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,从而实现了运行时的程序跳转。整个过程如下图所示。这里有个疑问,它跳走后还会回来吗?后面还有一些语句要执行?

WTL消息传递

默认的窗口过程定义如下:

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程序并无二致;显然,我们还有偷懒的手段,见后续分析。

posted @ 2013-04-02 22:56  MacroLee  阅读(736)  评论(0编辑  收藏  举报