部落格已遷移至: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 ><br />::Create(HWND hWndParent, _U_RECT rect = NULL, LPCTSTR szWindowName = NULL,<br />DWORD dwStyle = 0, DWORD dwExStyle = 0,<br />_U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)<br />{<br />//···<br />ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);<br />//···Register函數完成註冊,封裝了全域RegisterClassW函數<br />return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect, szWindowName,<br />dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam);<br />}
上面的代碼調用GetWndClassInfo得到視窗結構的基本資料,其中即設定了視窗處理函數有必要貼一下 GetWndClassInfo的代碼,它返回一個視窗類別的基本資料,其中就包括了視窗處理函數,這是我們用來向作業系統註冊的回呼函數。
static ATL::CWndClassInfo& CBkDialogImpl::GetWndClassInfo()<br />{<br />static ATL::CWndClassInfo wc = {<br />{ sizeof(WNDCLASSEX),<br />CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS | (IsWinXPAndLater() ? CS_DROPSHADOW : 0),<br />StartWindowProc, 0, 0, NULL, NULL, NULL,<br />(HBRUSH)(COLOR_WINDOW + 1), NULL, NULL, NULL },<br />NULL, NULL, IDC_ARROW, TRUE, 0, _T("")<br />};<br />return wc;<br />}
從上面的代碼看出:在向操縱系統註冊的時候提供的視窗過程是StartWindowProc,在VC/atlmfc/include/atlwin.h裡面。這樣當第一個訊息來的時候,操縱系統毫無疑問會調用我們的StartWindowProc上一次我看到這就沒有怎麼細看了以至於略過了重要的thunk技術。下面繼續看該視窗過程
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)<br />{<br />//下面的代碼其實是從之前儲存起來的一個視窗過程的this指標取出來。因為這個函數是第一次調用的,也即該HWND對應這this<br />//不過我倒懷疑在多線程的時候這會出問題,你怎麼保證儲存的this指標不會和hWnd對應呢,如果兩個線程同時運行到這,<br />//那麼不會競爭嗎?改天看看。也請大家指點一下,呵呵···(o,我知道了,說完這點待會補充一下^.^)<br />CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();<br />//···<br />pThis->m_hWnd = hWnd;//儲存一下控制代碼,以後會用的,為什麼儲存呢,因為thunk代碼會覆蓋這個地方的資料。<br />//···//GetWindowProc返回的是WindowProc,也是一個靜態函數<br />pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);//這裡待會說<br />WNDPROC pProc = pThis->m_thunk.GetWNDPROC();//實際上返回的是一個_stdcallthunk結構體的首地址<br />WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);<br />//將此結構體的首地址設定為視窗過程!!!???看下面<br />//···<br />return pProc(hWnd, uMsg, wParam, lParam);<br />}
疑問來了,m_thunk是什嗎?每個視窗執行個體都有這麼一個資料成員,定義如下
class CWndProcThunk<br />{<br />public:<br />_AtlCreateWndData cd;<br />CStdCallThunk thunk;//這才是其重點。thunk其實就是一段彙編代碼。</p><p>BOOL Init(WNDPROC proc, void* pThis) {<br />return thunk.Init((DWORD_PTR)proc, pThis);<br />}<br />WNDPROC GetWNDPROC() {<br />return (WNDPROC)thunk.GetCodeAddress();<br />}<br />};</p><p>#pragma pack(push,1)<br />struct _stdcallthunk<br />{<br />DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)<br />DWORD m_this; //<br />BYTE m_jmp; // jmp WndProc<br />DWORD m_relproc; // relative jmp<br />BOOL Init(DWORD_PTR proc, void* pThis)<br />{//初始化這段彙編代碼,待會會把它強制轉換成為一個視窗過程函數的地址。只是不會進行壓棧等操作<br />m_mov = 0x042444C7; //C7 44 24 0C //後面的注釋應該為//C7 44 24 04<br />m_this = PtrToUlong(pThis);<br />//把this指標移到堆棧的4個位元組開始位置。從StartWindowProc的調用規則CALLBACK看出這個位置正好是視窗控制代碼<br />m_jmp = 0xe9;<br />m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));<br />//上面相對跳轉<br />FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));//重新整理CPU預讀的指令<br />return TRUE;<br />}<br />void* GetCodeAddress() {<br />return this;//返回本結構體的首地址,注意幸好沒有虛函數,你懂的。<br />}<br />//···<br />};
上面的代碼再多說一下,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:<br /> push lParam<br /> push wParam<br /> push uMsg<br /> push hWnd //CALLBACK約定的壓棧規則:從右至左<br /> push cs:eip //儲存返回地址,即指令指標<br /> call 0x112345678 //假設這是thunk結構體的基址。</p><p> 0x112345678:<br /> move this [esp+0x04] //看看壓棧時的棧狀態就知道,該視窗類別的this正好覆蓋了參數中的hWnd !!<br /> jmp WindowProc //然後若無其事的跳轉到WindowProc去執行。</p><p>
-----------------------------------------------------------------
此時的堆棧狀態為:
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)<br />{<br />//直接把參數hWnd強制轉換成視窗類別的指標!!為什麼能成功呢?<br />//因為剛才thunk代碼中move this [esp+0x4]正好將此參數覆蓋了!!而且thunk代碼時每個視窗執行個體一個。<br />CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;<br />//····<br />//巧妙的取得了對應視窗過程的this指標,於是大搖大擺的調用其成員函數啦,<br />//當然,m_hWnd控制代碼第一次也是唯一一次進入StartWindowProc就儲存了的。<br />BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);<br />//···<br />}
對於ATL的訊息分配過程就是上面所說的了,關於thunk的一些細節可以參考我轉載的部落格。
此外需要稍微瞭解點組合語言,呼叫慣例。這些在我轉載的部落格中有參考.
基本圖示如下:
另外補充一下,剛才在StartWindowProc中的代碼:
_AtlWinModule.ExtractCreateWndData()我開始覺得會有線程競爭問題出現,剛剛看了下裡面的實現,是沒問題的,裡面不斷加了鎖,而且還用了“每線程變數”似的處理。
ATLINLINE ATLAPI_(void*) AtlWinModuleExtractCreateWndData(_ATL_WIN_MODULE* pWinModule)<br />{<br />//···//下面枷鎖,退出此函數解鎖<br />CComCritSecLock<CComCriticalSection> lock(pWinModule->m_csWindowCreate, false);<br />if (FAILED(lock.Lock()))<br />{//····<br />}<br />_AtlCreateWndData* pEntry = pWinModule->m_pCreateWndList;<br />if(pEntry != NULL) {<br />DWORD dwThreadID = ::GetCurrentThreadId();<br />_AtlCreateWndData* pPrev = NULL;<br />while(pEntry != NULL) {<br />if(pEntry->m_dwThreadID == dwThreadID)<br />{//關鍵是這,只處理當前線程,對於多線程來說,他們訪問的是不同的元素<br />if(pPrev == NULL)<br />pWinModule->m_pCreateWndList = pEntry->m_pNext;<br />else<br />pPrev->m_pNext = pEntry->m_pNext;<br />pv = pEntry->m_pThis;<br />break;<br />}<br />pPrev = pEntry;<br />pEntry = pEntry->m_pNext;<br />}<br />}<br />return pv;<br />}
二、MFC的實現方式:
限於篇幅,請看下頁:http://blog.csdn.net/hw_henry2008/archive/2011/05/29/6453730.aspx