• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            牽著老婆滿街逛

            嚴以律己,寬以待人. 三思而后行.
            GMail/GTalk: yanglinbo#google.com;
            MSN/Email: tx7do#yahoo.com.cn;
            QQ: 3 0 3 3 9 6 9 2 0 .

            一種實現Win32消息處理處理函數的新方法 - 基于Thunk實現的類成員消息處理函數

            轉載自:http://blog.csdn.net/JerKii/archive/2006/04/07/654188.aspx

            一種實現Win32窗口過程函數(Window Procedure)的新方法

            基于Thunk實現的類成員消息處理函數

            JERKII.SHANG (JERKII@HOTMAIL.COM)

            MAR.10th - 31st, 2006

            Windows是一個消息驅動的操作系統,在系統中發生的所有消息均需要通過消息處理過程(或叫窗口過程)進行處理。由于C++給我們在程序設計中帶來更多的靈活性(如繼承、重載、多態等),所以我們都希望能夠使用C++的類來封裝Windows中的窗口過程函數,但是Windows規定了窗口過程函數必須定義為一個全局函數,也就是說需要使用面向過程的方法來實現,為了使用面向對象的技術來實現消息處理,我們必須另辟它徑。目前我們在網絡上見得比較多的方式是使用Thunk將即將傳遞給窗口過程的第一個參數(HWND hWnd)的值使用類對象的內存地址(即this指針)進行替換(ATL使用的也是這種方法)。這樣,在相應的窗口過程中通過將hWnd強制轉換成類對象的指針,這樣就可以通過該指針調用給類中的成員函數了。但是該方法仍然需要將該消息處理函數定義成一個靜態成員函數或者全局函數。本文將介紹一種完全使用(非靜態)類成員函數實現Win32的窗口過程函數和窗口過程子類化的新方法。雖然也是基于Thunk,但是實現方法完全不同于之前所說的那種,我所采用的是方法是——通過對Thunk的調用,將類對象的this指針直接傳遞給在類中定義的窗口處理函數(通過ECX或棧進行傳遞),這樣就能夠使Windows直接成功地調用我們窗口過程函數了。另外,本文介紹一種使用C++模板進行消息處理函數的“重載”,這種方法直接避免了虛函數的使用,因此所有基類及其派生類中均無虛函數表指針以及相應的虛函數表(在虛函數較多的情況下,該數組的大小可是相當可觀的)。從而為每個類的實例“節省”了不少內存空間(相對于使用傳統的函數重載機制)。

            關鍵字: C++ 模板,調用約定,Thunk,機器指令(編碼),內嵌匯編
            環境:VC7,VC8,32位Windows

            內容

            前言

            也許你是一位使用MFC或ATL進行編程的高手,并且能在很短的時間內寫出功能齊全的程序。但是,你是否曾經花時間去想過“MFC或ATL是通過什么樣的途徑來調用我們的消息處理函數的呢?他們是怎樣將Windows產生的消息事件傳遞給我們的呢?”在MFC中定義一個從CWnd繼承而來的類,相應的消息事件就會發送到我們定義的類中來,你不覺得這背后所隱藏的一切很奇怪嗎?如果你的感覺是這樣,那么本文將使用一種簡單并且高效的方法來揭開這個神秘的面紗以看個究竟,同時我將非常詳細地介紹需要使用到的各種知識,以便能讓更多初學者更容易掌握這些知識。

            在Windows中,所有的消息均通過窗口過程函數進行處理,窗口過程函數是我們和Windows操作系統建立聯系的唯一途徑,窗口過程函數的聲明均為:

            LRESULT __stdcall WndProc
            (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

            MSDN有對該函數中的每個參數的詳細描述,如果你現在仍然對該函數存在疑問,那么請先參照MSDN中的相關內容后,再繼續閱讀本文。通常,Windows均要求我們將消息處理函數定義為一個全局函數,或者是一個類中的靜態成員函數。并且該函數必須采用__stdcall的調用約定(Calling Convention),因為__stdcall的函數在返回前會自己修正ESP的值(由于參數傳遞所導致的ESP的改變),從而使ESP的值恢復到函數調用之前的狀態。

            全局函數或靜態成員函數的使用使得我們很難發揮C++的優勢(除非你一直想使用C進行Windows程序開發,如果是這樣,那么你也就沒有必要再繼續閱讀本文了),因為在這種結構下(即面向過程),我們不能很方便地在消息處理函數中使用我們的C++對象,因為在這樣的消息處理函數中,我們很難得到我們對象的指針,從而導致我們不能很方便的操作C++對象中的屬性。為了解決對Win32的封裝,Microsoft先后推出了MFC和ATL,可以說兩者都是非常優秀的解決方案,并且一直為多數用戶所使用,為他們在Windows下的程序開發提供了很大的便利。

            但是,MFC在我們大家的眼里都是一種比較笨重的方法,使用MFC開發出來的程序都必須要在MFC相關動態庫的支持下才能運行,并且這些動態庫的大小可不一般(VS2005中的mfc80.dll就有1.04M),更為甚者,CWnd中包含大量的虛函數,所以每個從他繼承下來的子類都有一個數量相當可觀的虛函數表(雖然通過在存在虛函數的類上使用sizeof得到的結果是該類對象的大小只增長了4個字節,即虛函數表指針,但是該指針所指向的虛函數數組同樣需要內存空間來存儲。一個虛函數在虛函數表中需占用4個字節,如果在我們的程序中用到較多的CWnd的話,定會消耗不少內存(sizeof(CWnd) = 84,sizeof(CDialog) = 116)。ATL與MFC完全不同,ATL采用模板來對Win32中的所有內容進行封裝,使用ATL開發出來的程序不需要任何其他動態庫的支持(當然,除基本的Windows庫外),ATL使用Thunk將C++對象的指針通過消息處理函數的第一個參數(即hWnd)傳入,這樣,在消息處理函數中,我們就可以很方便地通過該指針來訪問C++對象中的屬性和成員函數了,但這種方法也必須要借助幾個靜態成員函數來實現。

            本文將采用這幾種技術實現一種完全由C++類成員函數(非靜態)實現的Windows消息處理機制,并最終開發一個封裝Windows消息處理的工具包(KWIN)。首先讓我們來看看怎樣使用KWIN來開發的一個簡單程序:

            創建一個簡單的窗口程序: 

            #include "kwin.h"
            class MyKWinApp : public KWindowImpl<MyKWinApp>
            {
            public:
            MyKWinApp () : KWindowImpl<MyKWinApp> ("MyKWinAppClassName")
            {}
            /* Overriede the window procdure */
            LRESULT KCALLBACK KWndProc (
            HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
            {   /* Do anthing you want here */
            return __super::KWndProc (hWnd, msg, wParam, lParam);
            }
            BOOL OnDestroy () { PostQuitMessage (0); return TRUE; }
            /* Override other message handler */
            };
            INT __stdcall WinMain (
            HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
            {
            MyKWinApp kapp;
            kapp.CreateOverlappedWindow ("This is my first KWinApp");
            MSG msg;
            while (GetMessage (&msg, 0, 0, 0))
            {   TranslateMessage (&msg);
            DispatchMessage (&msg);
            }
            return msg.wParam;
            }
            

            創建一個簡單的對話框程序:

            #include "kwin.h"
            class MyKDlgApp : public KDialogImpl<MyKDlgApp>
            {
            public:
            enum {IDD = IDD_DLG_MYFIRSTDIALOG };
            BOOL OnCommand(WORD wNotifyCode, WORD wId, HWND hWndCtrl)
            {   if (wId == IDOK) EndDialog (m_hWnd, wId); return TRUE; }
            };
            INT __stdcall WinMain (
            HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
            {
            MyKDlgApp kapp;
            kapp.DoModal ();
            return 0;
            }

            怎么樣?使用KWIN開發包后,你的程序結構是不是變得更加清晰了呢?你可以在你的類中(如MyKWinApp或MyKDlgApp)“重載”更多的消息處理函數,你甚至可以“重載”窗口過程函數(如MyKWinApp)。這里的重載跟通常意義上的重載可不是一個意思,這里的“重載”是使用C++模板機制實現的,而傳統意義上的重載需要通過虛函數(更準確地說應該是虛函數表)來實現。好,你現在是不是很想知道,這個KWIN的內部到底是怎樣實現的了吧?好,讓我來一步一步帶你步入KWIN的內部中去吧,現在你唯一需要的就是耐心,因為這篇文章寫得似乎太長了些^_^ ...

            傳統的C++重載

            通常,如果我們要重載父類中的方法,那么我們必須在父類中將該方法聲明為虛函數,這樣每個擁有虛函數的類對象將會擁有一個自己的虛函數表(指針),也就是說,該虛函數表(數組)就成了該類對象的“‘靜態’成員變量(之所以說是‘靜態’的,是因為該虛函數數組及其指向該數組的指針在該類的所有實例中均為同一份數據)”了,C++(更確切地說應該是編譯器)就是通過虛函數表來實現函數的重載機制的,因為有了虛函數表后,對虛函數的調用就是通過該虛函數表來完成的,因為編譯器在生成代碼的時候會根據每個虛函數在類中的位置而對其進行編號,并且通過該序號來對虛函數進行調用,所以你通常會在反匯編中看到如下代碼:

            mov edx, this pointer        ; Load EAX with the value of 'this pointer'(or vptr)
            mov edx, dword ptr [edx + 4] ; Get the address of the second virtual function in vtable
            push ...                     ; Pass argument 1 
            push ...                     ; Pass other arguments here ...
            call edx                     ; Call virtual function

            當子類從有虛函數的父類中派生時,他將擁有一份獨立的虛函數表指針以及虛函數數組,并且“開始時”該數組中的內容和基類一模一樣。但是,當編譯器在編譯時檢測到子類重載了父類中的虛函數時,編譯器就會修改子類的虛函數數組表,將該表中被重載的虛函數的地址改為子類中的函數地址,對于那些沒有被重載的虛函數,該表中的函數地址和父類的虛函數表中的地址一樣!當一個子類從多個帶有虛函數的父類中繼承時,該子類就擁有多個虛函數表指針了(也就是將擁有多個虛函數指針數組),當我們在這種情況下進行指針的轉換的時(通常為將子類指針轉換成父類指針),所進行最終操作的就是虛函數數組指針的轉換。如:

            class A :
            public VBase1,
            public VBase2,
            public VBase3
            /* VBase1, VBase2, VBase3 均為存在虛函數的父類 */
            { ... };
            A a;
            VBase1* p1;
            VBase2* p2;
            VBase3* p3;
            p1 = &a;
            p2 = &a;
            p3 = &a;
             // 假定這里a的地址為0x0012f564,那么(按字節為單位)
            p1 = &a + 0 = 0x0012f564,
            p2 = &a + 4 = 0x0012f568,
            p3 = &a + 8 = 0x0012f56C
            

            因為在類對象的內存布局中,編譯器總是將虛函數數組指針放在偏移為0的地方。

            好了,似乎我們已經跑題了,關于這方面的知識在網上也可以找到很多,如果你有興趣,可以參見我的另一篇文章:略談虛函數的調用機制,至此,我相信你已經對C++中的重載機制有一定的認識了吧,現在再讓我們來看看怎樣在C++中使用模板來實現函數的“重載”。

            使用C++模板實現函數的“重載”

            通過我們對周圍事物(或數據)的抽象,我們得到類。如果我們再對類進行抽象,得到就是一個模板。模板是一種完全基于代碼級的重用機制,同時也為我們編寫一些結構良好,靈活的程序提供了手段,所有的這一切都得歸功于編譯器的幫助,因為編譯器最終會將我們所有的這些使用這些高級技術編寫的代碼轉換成匯編代碼(或者應該說是機器碼),好,廢話少說為好!^_^。

            通常我們會使用下面的方式來實現函數的“重載”,這里似乎沒有很復雜的“理論”要闡述,我就簡單的把大致實現過程描述一下吧:

            template <class T> class TBase
            {
            public:
            void foo1 () { printf ("This is foo1 in TBase\n"); }
            void foo2 () { printf ("This is foo2 in TBase\n"); }
            void callback ()
            {	/* 如果子類重載了TBase中的函數,通過pThis將會直接調用子類中的函數 */
            T* pThis = static_cast<T *> (this);
            pThis->foo1 ();
            pThis->foo2 ();
            }
            };
            class TDerive : public TBase <TDerive>
            {
            public:
            void foo1 () { printf ("This is foo1 in TDerive\n"); } /* “重載”父類中的foo1 */
            };
            TDerive d;
            d.callback ();
            輸出結果為:
            This is foo1 in TDerive
            This is foo2 in TBase

            雖然上面的代碼看起來很奇怪,因為子類將自己作為參數傳遞給父類。并且子類中定義的“重載”函數只能通過“回調”的方式才能被調用,但這對Windows中的消息處理函數來說無疑是一大福音,因為Windows中的消息函數均是回調函數,似乎這樣的“重載”就是為Windows中的消息處理函數而定制的。雖然有些奇怪,但是他真的很管用,尤其是在實現我們的消息處理函數的時候。不是嗎?

            C++對象中的屬性和方法(成員函數)

            我們通常會將一些相關的屬性以及操作這些屬性的方法“封裝”到一個類中,從而實現所謂的信息(屬性)隱藏,各類對象(或類的實例)之間的交互都得通過成員函數來進行,這些屬性的值在另一方面也表明了該類對象在特定時刻的狀態。由于每個類對象都擁有各自的屬性(或狀態),所以系統需要為每個類對象分配相應的屬性(內存)存儲空間。但是類中的成員函數卻不是這樣,他們不占用類對象的任何內存空間。這就是為什么使用sizeof (your class)總是返回該類中的成員變量的字節大小(如果該類中存在虛函數,則還會多出4個字節的虛函數數組指針來)。因為成員函數所要操作的只是類對象中的屬性,他們所關心的不是這些屬性的值。

            那么,這些類成員函數又是怎么知道他要去操作哪個類對象中的屬性呢?答案就是通過this指針。this指針說白了就是一個指向該類對象在創建之后位于內存中的內存地址。當我們調用類中的成員函數時,編譯器會“悄悄地”將相應類對象的內存地址(也就是this指針)傳給我們的類成員函數,有了這個指針,我們就可以在類成員函數中對該類對象中的屬性進行訪問了。this指針被“傳入”成員函數的方式主要取決于你的類成員函數在聲明時所使用的調用約定,如果使用__thiscall調用約定,那么this指針將通過寄存器ECX進行傳遞,該方式通常是編譯器(VC)在缺省情況下使用的調用約定。如果是__stdcall或__cdecl調用約定,this指針將通過棧進行傳遞,并且this指針將是最后一個被壓入棧的參數,雖然我們在聲明成員函數時,并沒有聲明有這個參數。

            關于調用約定與this指針的傳遞

            簡單說來,調用約定(Calling Convention)主要是用來指定相應的函數在被調用時的參數傳遞順序,以及在調用完成后由誰(調用者還是被調用者)來修正ESP寄存器的值(因為調用者向被調用者通過棧來傳遞參數時,ESP的值會被修改,系統必須要能夠保證被調用函數返回后,ESP的值要能夠恢復到調用之前的值,這樣調用者才能正確的運行下去,對于其他寄存器,編譯器通常會自動為我們生成相應的保存與恢復代碼,通常是在函數一開始將相關寄存器的值PUSH到棧中,函數返回之前再依次pop出來)。

            通常對ESP的修正有兩種方式,一種是直接使用ADD ESP, 4*n,一種是RET 4*n(其中n為調用者向被調用者所傳遞的參數個數,乘4是因為在棧中的每個參數需要占用4個字節,因為編譯器為了提高尋址效率,會將所有參數轉換成32位,即即使你傳遞一個字節,那么同樣會導致ESP的值減少4)。通常我們使用的調用約定主要有__stdcall,__cdecl,__thiscall,__fastcall。有關這些調用約定的詳細說明,請參見MSDN(節Argument Passing and Naming Conventions )。這里只簡略地描述他們的用途:幾乎所有的Windows API均使用__stdcall調用約定,ESP由被調用者函數自行修正,通常在被調用函數返回之前使用RET 4 * n的形式。__cdecl調用約定就不一樣,它是由調用者來對ESP進行修正,即在被調用函數返回后,調用者采用ADD ESP, 4 *n的形式進行棧清除,通常你會看到這樣的代碼:

            push argument1
            push argument2
            call _cdecl_function
            add esp, 8
            

            另外一個__cdecl不得不說的功能是,使用__cdecl調用約定的函數可以接受可變數量的參數,我們見得最多的__cdecl函數恐怕要算printf了(int __cdecl printf  (char *format, ...)),瞧!是不是很cool啊。因為__cdecl是由調用者來完成棧的清除操作,并且他自己知道自己向被調用函數傳遞了多少參數,此次他自己也知道該怎樣去修正ESP。跟__stdcall比起來,唯一的“缺點”恐怕就是他生成的可執行代碼要比__stdcall稍長些(即在每個CALL之后,都需要添加一條ADD ESP, X的指令)。但跟他提供給我們的靈活性來說,這點“缺點”又算什么呢?

            __thiscall主要用于類成員函數的調用約定,在VC中它是成員函數的缺省調用約定。他跟__stdcall一樣,由被調用者清除調用棧。唯一不同的恐怕就是他們對于this指針的傳遞方式了吧!在__stdcall和__cdecl中,this指針通過棧傳到類成員函數中,并且被最后一個壓入棧。而在__thiscall中,this指針通過ECX進行傳遞,直接通過寄存器進行參數傳遞當然會得到更好的運行效率。另外一種,__fastcall,之所以叫fast,是因為使用這種調用約定的函數,調用者會“盡可能”的將參數通過寄存器的方式進行傳遞。另外,編譯器將為每種調用約定的函數產生不同的命名修飾(即Name-decoration convention),當然這些只是編譯器所關心的東西,我們就不要再深入了吧!。

            通過其他途徑調用類中的成員函數

            對于給定的一個類:

            class MemberCallDemo
            {
            public:
            void __stdcall foo (int a) { printf ("In MemberCallDemo::foo, a = %d\n", a); };
            };
            

            通常我們會通過下面的方式進行調用該類中的成員方法foo:

            MemberCallDemo mcd;
            mcd.foo (9);
            或者通過函數指針的方式:
            void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo;
            (mcd.*foo_ptr) (9);
            

            我總是認為這中使用成員函數指針的調用方式(或是語法)感到很奇怪,不過它畢竟是標準的并且能夠為C++編譯器認可的調用方式。幾乎在所有編譯器都不允許將成員函數的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無濟于事)例如:而只能將其賦給給與該成員函數類型聲明(包括所使用的調用約定,返回值,函數參數)完全相同的變量。因為成員函數的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數中,為了成員函數被安全調用,編譯器禁止此類型的轉換也在情理當中。但有時我們為了實現特殊的目的需要將成員函數的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可),我們可以通過下面兩種方法來實現:下面的這幾種試圖將一個成員函數的地址保存到一個DWORD中都將被編譯器認為是語法錯誤: 我總是認為這中使用成員函數指針的調用方式(或是語法)感到很奇怪,不過它畢竟是標準的并且能夠為C++編譯器認可的調用方式。幾乎在所有編譯器都不允許將成員函數的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無濟于事)。下面的這幾種試圖將一個成員函數的地址保存到一個DWORD中都將被編譯器認為是語法錯誤:

            DWORD dwFooAddrPtr= 0;
            dwFooAddrPtr = (DWORD) &MemberCallDemo::foo; /* Error C2440 */
            dwFooAddrPtr = reinterpret_cast<DWORD> (&MemberCallDemo::foo); /* Error C2440 */

            因為成員函數的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數中,為了成員函數被安全調用,編譯器禁止此類型的轉換也在情理當中。但有時我們為了實現特殊的目的需要將成員函數的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可)。

            我們只能將成員函數的地址賦給給與該成員函數類型聲明(包括所使用的調用約定,返回值,函數參數)完全相同的變量(如前面的void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo)。因為成員函數的聲明中都有一個隱藏的this指針,該指針會通過ECX或棧的方式傳遞到成員函數中,為了成員函數被安全調用,編譯器禁止此類型的轉換也在情理當中。但有時我們為了實現特殊的目的需要將成員函數的地址直接賦給一個DWORD變量(或其他任何能夠保存一個指針值的變量,通常只要是一個32位的變量即可),就像我們即將介紹的情況。

            通過前面幾節的分析我們知道,成員函數的調用和一個普通的非成員函數(如全局函數,或靜態成員函數等)唯一不同的是,編譯器會在背后“悄悄地”將類對象的內存地址(即this指針)傳到類成員函數中,具體的傳遞方式以該類成員函數所采用的調用約定而定。所以,是不是只要我們能夠手動地將這個this指針傳遞給一個成員函數(這是應該是一個函數地址),是不是就可以使該成員函數被正確調用呢?答案是肯定的,但是我們迫在眉睫需要解決的是怎樣才能得到這個成員函數的地址呢?通常,我們有兩種方法可以達到此目的:

            1。使用內嵌匯編(在VC6及以前的版本中將不能編譯通過)

            DWORD dwFooAddrPtr = 0;
            __asm
            {
            /* 得到MemberCallDemo::foo偏移地址,事實上就是該成員函數的內存地址(起始地址) */
            MOV EAX, OFFSET MemberCallDemo::foo
            MOV DWORD PTR [dwFooAddrPtr], EAX
            }
            

            這種方法雖然看起來甚是奇怪,但是他卻能夠解決我們所面臨的問題。雖然在目前的應用程序開發中,很少甚至幾乎沒有人使用匯編語言去開發,但是,往往有時一段小小的匯編代碼居然能夠解決我們使用其他方法不能解決的問題,這在上面的例子和下面即將介紹的Thunk中大家就會看到他那強有力的問題解決能力。所以說我們不能將匯編仍在一邊,我們需要了解她,并且能夠在適當的時候使用她。畢竟她始終是一個最漂亮,最具征服力的編程語言。^_^

            2。通過使用union來“欺騙”編譯器

            或使用一種更為巧妙的方法,通過使用一個union數據結構進行轉換(Stroustrup在其《The C++ Programming Language》中講到類似方法),由于在union數據結構中,所有的數據成員均享用同一內存區域,只是我們為這一塊內存區域賦予了不同的類型及名稱,并且我們修改該結構中的任何“變量”都會導致其他所有“變量”的值均被修改。所以我們可以使用這種方法來“欺騙”編譯器,從而讓他認為我們所進行的“轉換”是合法的。

            template <class ToType, class FromType>
            ToType union_cast (FromType f)
            {
            union
            {    FromType _f;
            ToType   _t;
            } ut;
            ut._f = f;
            return ut._t;
            }
            DWORD dwAddrPtr = union_cast<DWORD> (&YourClass::MemberFunction);
            

            怎么樣,這樣的類型轉換是不是很酷啊?就像使用reinterpret_cast和static_cast等之類的轉換操作符一樣。通過巧妙地使用union的特點輕松“逃”過編譯器的類型安全檢查這一關,從而達到我們的數據轉換目的。當然,我們通常不會這樣做,因為這樣畢竟是類型不安全的轉換,他只適用于特定的非常規的(函數調用)場合。

            好,我們現在已經得到了該成員函數的內存地址了,下買面我們通過一個“更為奇怪的”方式來調用成員函數MemberCallDemo::foo(使用這種方式,該成員函數foo將不能使用缺省的__thiscall調用約定,而必須使用__stdcall或__cdecl):

            void (__stdcall *fnFooPtr) (void*/* pThis*/, int/* a*/) =
            (void (__stdcall *) (void*, int)) dwFooAddrPtr;
            fnFooPtr (&mcd, 9);

            執行上面的調用后,屏幕上依然會輸出“In MemberCallDemo::foo, a = 9”。這說明我們成功地調用了成員函數foo。當然,使用這種方式使我們完全沖破了C++的封裝原則,打壞了正常的調用規則,即使你將foo聲明為private函數,我們的調用同樣能夠成功,因為我們是通過函數地址來進行調用的。

            你不禁會這樣問,在實際開發中誰會這樣做呢?沒錯,我估計也沒有人會這樣來調用成員函數,除非在一些特定的應用下,比如我們接下來所要做的事情。

            認識Thunk

            Thunk!Thunk?這是什么意思?翻開《Oxford Advanced Learner's Dictionary, Sixth-Edition》...查不到!再使用Kingsoft's PowerWord 2006,其曰“錚,鐺,鏘?”顯然是個象聲詞。頓暈,這跟我們所要描述的簡直不挨邊啊!不知道人們為什么要把這種技術稱之為Thunk。不管了,暫時放置一邊吧!

            通常,我們所編寫的程序最終將被編譯器的轉換成計算機能夠識別的指令碼(即機器碼),比如:

            C/C++代碼               等價的匯編代碼                   編譯器產生的機器指令
            ==================== ==========================   =====================
            int k1, k2, k;
            k1 = 1;               mov  dword ptr [k1], 1      C7 45 E0 01 00 00 00
            k2 = 2;               mov  dword ptr [k2], 2      C7 45 D4 02 00 00 00
            k = k1 + k2;          mov  eax, dword ptr [k1]    8B 45 E0
            add  eax, dword ptr [k2]    03 45 D4
            mov  dword ptr [k], eax     89 45 C8
            

            最終,CPU執行完指令序列“C7 45 E0 01 00 00 00 C7 45 D4 02 00 00 00 8B 45 E0 03 45 D4 89 45 C8”后,就完成了上面的簡單加法操作。從這里我們似乎能夠得到啟發,既然CPU只能夠認識機器碼,那么我們可以直接將機器碼送給CPU去執行嗎?答案是:當然可以,而且還非常高效!那么,怎么做到這一點呢?——定義一個機器碼數組,然后跳轉到該數組的起始地址處開始執行:

            unsigned char machine_code[] = {
            0xC7, 0x45, 0xE0, 0x01, 0x00, 0x00, 0x00,
            0xC7, 0x45, 0xD4, 0x02, 0x00, 0x00, 0x00,
            0x8B, 0x45, 0xE0,
            0x03, 0x45, 0xD4,
            0x89, 0x45, 0xC8};
            void* paddr = machine_code;
            使用內嵌匯編調用該機器碼:
            __asm
            {   MOV EAX, dword ptr [paddr] ; or  mov eax, dword ptr paddr ; or  mov eax, paddr
            CALL EAX
            }
            如果使用C調用該機器碼,則為:
            void (*fn_ptr) (void) = (void (*) (void)) paddr;
            fn_ptr ();

            怎么樣?當上面的CALL EAX執行完后,變量k的值同樣等于3。但是,當machine_code中的指令執行完后,CPU將無法再回到CALL指令的下一條指令了!為什么啊?是的,因為machine_code中沒有返回指令的機器碼!要讓CPU能夠返回到正確的位置,我們必須將返回指令RET的機器碼(0xC3)追加到machine_code的末尾,即:unsigned char machine_code[] = {0xC7, 0x45, ..., 0x89, 0x45, 0xC8, 0xC3};。

            這就是Thunk!一種能讓CPU直接執行我們的機器碼的技術,也有人稱其為自修改代碼(Self-Modifying Code)。但這有什么用呢?同樣,在通常的開發中,我們不可能通過這么復雜的代碼來完成上面的簡單加法操作!誰這樣做了,那他/她一定是個瘋子!^_^。目前所了解的最有用的也是用得最多的就是使用Thunk來更改棧中的參數,甚至可以是棧中的返回地址,或者向棧中壓入額外的參數(就像我們的KWIN那樣),從而達到一些特殊目的。當然,在你熟知Thunk的原理后,你可能會想出更多的用途來,當然,如果你想使用Thunk來隨意破壞當前線程的棧數據,從而直接導致程序或系統崩潰,那也不是不可能的,只要你喜歡,誰又在乎呢?只要你不把這個程序拿給別人運行就行!

            通常我們會使用Thunk來“截獲”對指定函數的調用,并在真正調用該函數之前修改調用者傳遞給他的參數或其他任何你想要做的事情,當我們做完我們想要做的時候,我們再“跳轉到”真正需要被調用的函數中去。既然要跳轉,就勢必要用到JMP指令。由于在Thunk中的代碼必須為機器指令,所以我們必須按照編譯器的工作方式將我們所需要Thunk完成的代碼轉換成機器指令,因此我們需要知道我們在Thunk所用到的指令的機器指令的編碼規則(通常,我們在Thunk中不可能做太多事情,了解所需的指令的編碼規則也不是件難事)。大家都知道,JMP為無條件轉移指令,并且有short、near、far轉移,通常編譯器會根據目標地址距當前JMP指令的下一條指令之間的距離來決定跳轉類型。在生成機器碼時,并且編譯器會優先考慮short轉移(如果目標地址距當前JMP指令的下一條指令的距離在-128和127之間),此時,JMP對應的機器碼為0xEB。如果超出這個范圍,JMP對應的機器碼通常為0xE9。當然,JMP還存在其他類型的跳轉,如絕對地址跳轉等,相應地也有其他形式的機器碼,如0xFF,0xEA。我們常用到的只有0xEB和0xE9兩種形式。另外,需要注意的是,在機器碼中,JMP指令后緊跟的是一個目標地址到該條JMP指令的下一條指令之間的距離(當然,以字節為單位),所以,如果我們在Thunk中需要用到JMP指令,我們就必須手動計算該距離(這也是編譯器所需要做的一件事)。如果你已經很了解JMP指令的細節,那么你應該知道了下面Thunk的將是什么樣的結果了吧:

            unsigned char machine_code [] = {0xEB, 0xFE};

            啊,沒錯,這將是一個死循環。這一定很有趣吧!事實上他跟JMP $是等價的。由于這里的機器碼是0xEB,它告訴CPU這是一個short跳轉,并且,0xFE的最高位為1(即負數),所以CPU直到它是一個向后跳轉的JMP指令。由于向后跳轉的,所以此時該JMP所能跳轉到的范圍為-128至-1(即0x80至0xFF),但是由于這時的JMP指令為2個字節,所以向后跳轉(從該條指令的下一條指令開始)2個字節后,就又回到了該條JMP指令的開始位置。

            當發生Short JMP指令時,其所能跳轉的范圍如下:

            偏移量 機器碼 =========== ====== (-128) 0x80 ?? (-127) 0x81 ?? : : (-3) 0xFD ?? (-2) 0xFE EB <- Short JMP指令 (-1) 0xFF XX <- XX為跳轉偏移量,其取值范圍可為[0x80 - 0x7F] (0) 0x00 ?? <- JMP下一條指令的開始位置 (+1) 0x01 ?? (+2) 0x02 ?? : : (+125) 0x7D ?? (+126) 0x7E ?? (+127) 0x7F ??

            好,讓我們在來看一個例子,來說明Thunk到底是怎樣修改棧中的參數的:

            void foo(int a)
            { printf ("In foo, a = %d\n", a); }
            unsigned char code[9];
            * ((DWORD *) &code[0]) = 0x042444FF; /* inc dword ptr [esp+4] */
            code[4]  = 0xe9; /* JMP */
            * ((DWORD *) &code[5]) = (DWORD) &foo - (DWORD) &code[0] - 9; /* 跳轉偏移量 */
            void (*pf)(int/* a*/) = (void (*)(int)) &code[0];
            pf (6);

            當執行完pf (6)調用后,就會得到下面的輸出:
            “In foo, a = 7”(明明傳入的是6,為什么到了foo中就變成了7了呢?)。怎么樣?我們在Thunk中通過強制CPU執行機器碼0xFF,0x44,0x24,0x04來將棧中的傳入參數a(位于ESP + 4處)增加1,從而修改了調用者傳遞過來的參數。在執行完INC DWORD PTR [ESP+4]后,再通過一個跳轉指令跳轉到真正的函數入口處。當然,我們同樣不可能在實際的開發中使用這種方法進行函數調用,之所以這樣做是為了能夠更加容易的弄清楚Thunk到底是怎么工作的!

            讓Windows直接調用你在類中定義的(非靜態)消息處理函數

            好了,寫了這么久,似乎我們到這里才真正進入我們的正題。上面幾節所描述的都是這一節所需的基本知識,有了以上知識,我們就能夠很容易的實現我們的最終目的——讓Windows來直接調用我們的類消息處理成員函數,在這里無須使用任何靜態成員函數或全局函數,所有的事情都將由我們定義的類成員函數來完成。由于Windows中所有的消息處理均為回調函數,即它們是由操作系統在特定的消息發生時被系統調用的函數,我們需要做的僅僅是定義該消息函數,并將該消息函數的函數地址“告訴”Windows。既然我們能夠使用在通過其他途徑調用類中的成員函數中所描述的方法得到類成員函數(消息處理函數)的地址,那么,我們能夠直接將該成員函數地址作為一個回調函數的地址傳給操作系統嗎?很顯然,這是不可能的。但是為什么呢?我想聰明的你已經猜到,因為我們的成員函數需要類對象的this指針去訪問類對象中的屬性,但是Windows是無法將相應的類對象的this指針傳給我們的成員函數的!這就是我們所面臨的問題的關鍵所在!如果我們能夠解決這個類對象的this指針傳遞問題,即將類對象的this指針手動傳遞到我們的類成員函數中,那么我們的問題豈不是就解決了嗎?沒錯!

            Thunk可以為們解決這個難題!這里Thunk需要解決的是將消息處理函數所在的類的實例的this指針“傳遞”到消息處理函數中,從前面的描述我們已經知道,this指針的傳遞有兩種方式,一種是通過ECX寄存器進行傳遞,一種是使用棧進行傳遞。

            1。使用ECX傳遞this指針(__thiscall)

            這是一種最簡單的方式,它只需我們簡單地在Thunk中執行下面的指令即可:

            LEA ECX, this pointer
            JMP member function-based message handler
            

            使用這種方式傳遞this指針時,類中的消息處理函數必須使用__thiscall調用約定!在 關于調用約定與this指針的傳遞中我們對調用約定有較為詳細的討論。

            2。使用棧傳遞this指針(__stdcall或__cdecl)

            這是一種稍復雜的方式,使用棧傳遞this指針時必須確保類中的消息處理函數使用__stdcall調用約定,這跟通常的消息處理函數(靜態成員函數或全局函數)使用的是同一種條用約定,唯一不同的是現在我們使用的是類成員函數(非靜態)。之所以說他稍復雜,是因為我們要在Thunk中要做稍多的工作。前面我們已經說過,我們已經將我們定義的Thunk的地址作為“消息處理回調函數”地址傳給了Windows,那么,當有消息需要處理時,Windows就會調用我們的消息處理函數,不過這時它調用的是Thunk中的代碼,并不是真正的我們在類中定義的消息處理函數。這時,要將this指針送入當前棧中可不是件“容易”的事情。讓我們來看看Windows在調用我們的Thunk代碼時的棧的參數內容:

            this指針被壓入棧之前             this指針被壓入棧之后
            :      ...      :            :       ...      :
            |---------------|            |----------------|
            |     LPARAM    |            |     LPARAM     |
            |---------------|            |----------------|
            |     WPARAM    |            |     WPARAM     |
            |---------------|            |----------------|
            |   UINT (msg)  |            |   UINT (msg)   |
            |---------------|            |----------------|
            |      HWND     |            |      HWND      |
            |---------------|            |----------------|
            | (Return Addr) | <- ESP     | <this pointer> | <- New item inserted by this thunk code
            |---------------|            |----------------|
            :      ...      :            | (Return Addr)  | <- ESP
            |----------------|
            :       ...      :
            圖1                           圖2
            

            從圖1可以看出,為了將this指針送入棧中,我們可不能簡單地使用PUSH this pointer的方式將this指針“壓入”棧中!但是為什么呢?想想看,如果直接將this指針壓入棧中,那么原來的返回地址將不能再起效。也就是說我們將不能在我們的消息處理函數執行結束后“返回”到正確的地方,這勢必會導致系統的崩潰。另一方面,我們的成員函數要求this指針必須是最后一個被送入棧的參數,所以,我們必須將this指針“插入”到HWND參數和返回地址(Return Addr)之間。如圖2所示。所以,在這種情況下,我們須在Thunk中完成以下工作:

            PUSH  DWORD PTR [ESP]                          ; 保存(復制)返回地址到當前棧中
            MOV   DWORD PTR [ESP + 4], pThis               ; 將this指針送入棧中,即原來的返回地址處
            JMP   member function-based message handler    ; 跳轉至目標消息處理函數(類成員函數)
            

            實現我們的KWIN包

            好了,有了以上知識后,現在就只剩下我們的KWIN包的開發了,當然,如果你掌握以上知識后,你可以運用這些知識,甚至找出新的方法來實現你自己的消息處理包。我想,那一定時間非常令人激動的事情!如果你想到更好的方法,千萬別忘了告訴我一聲哦。

            首先來看看我們在KWIN中需要使用到的一個比較重要的宏:

            #define __DO_DEFAULT (LRESULT) -2
            #define _USE_THISCALL_CALLINGCONVENTION
            #ifdef _USE_THISCALL_CALLINGCONVENTION
            #define THUNK_CODE_LENGTH      10 /* For __thiscall calling convention ONLY */
            #define KCALLBACK __thiscall
            #else
            #define THUNK_CODE_LENGTH      16 /* For __stdcall or __cdecl calling convention ONLY */
            #define KCALLBACK __stdcall
            #endif

            在KWIN中同時實現了__thiscall和__stdcall兩種調用約定,如果定義了_USE_THISCALL_CALLINGCONVENTION宏,那么就使用__thiscall調用約定,否則將使用__stdcall調用約定。宏KCALLBACK在定義了_USE_THISCALL_CALLINGCONVENTION宏后將被替換成__thiscall,否則為__stdcall。THUNK_CODE_LENGTH定義了在不同的調用約定下所需的機器指令碼的長度,如果使用__thiscall,我們只需要10個字節的機器指令碼,而在__stdcall下,我們需要使用16字節的機器指令碼。

            我們將實現對話框和一般窗口程序的消息處理函數進行封裝的包(KWIN),我們力求使用KWIN能為我們的程序帶來更好的靈活性和結構“良好”性,就像我們在本文開始時向大家展示的一小部分代碼那樣。首先我們將定義一個對話框和窗口程序都需要的數據結構_K_THUNKED_DATA,該結構封裝了所有所需的Thunk代碼(機器指令碼)。整個KWIN的結構大致如下:


            圖3

            在_K_THUNKED_DATA中有一個非常重要的函數—Init,它的原型如下:

            void Init (
            DWORD_PTR pThis,      /* 消息處理類對象的內存地址(this指針) */
            DWORD_PTR dwProcPtr   /* 消息處理函數(類成員函數)的地址 */
            )
            {
            DWORD dwDistance = (DWORD) dwProcPtr - (DWORD) &pThunkCode[0] - THUNK_CODE_LENGTH;
            #ifdef _USE_THISCALL_CALLINGCONVENTION
            /*
            Encoded machine instruction   Equivalent assembly languate notation
            ---------------------------   -------------------------------------
            B9 ?? ?? ?? ??                mov    ecx, pThis ; Load ecx with this pointer
            E9 ?? ?? ?? ??                jmp    dwProcPtr  ; Jump to target message handler
            */
            pThunkCode[0] = 0xB9;
            pThunkCode[5] = 0xE9;
            *((DWORD *) &pThunkCode[1]) = (DWORD) pThis;
            *((DWORD *) &pThunkCode[6]) = dwDistance;
            #else
            /*
            Encoded machine instruction   Equivalent assembly languate notation
            ---------------------------   -------------------------------------
            FF 34 24                      push  dword ptr [esp]          ; Save (or duplicate) the Return Addr into stack
            C7 44 24 04 ?? ?? ?? ??       mov   dword ptr [esp+4], pThis ; Overwite the old Return Addr with 'this pointer'
            E9 ?? ?? ?? ??                jmp   dwProcPtr                ; Jump to target message handler
            */
            pThunkCode[11] = 0xE9;
            *((DWORD *) &pThunkCode[ 0]) = 0x002434FF;
            *((DWORD *) &pThunkCode[ 3]) = 0x042444C7;
            *((DWORD *) &pThunkCode[ 7]) = (DWORD) pThis;
            *((DWORD *) &pThunkCode[12]) = dwDistance;
            #endif
            }

            看見了吧,該函數的實現異常簡單,但是在KWIN中的作用非凡。我們的KWIN的成敗就依賴于該函數是否正確的“生成”了我們所需要的機器指令碼。Init通過將類對象的this指針和消息處理函數的地址“硬編碼”到數組_machine_code數組中,這樣該數組中就擁有了可以讓Windows正確調用我們在類中定義的成員函數所需的指令了。接下來所需要做的事情就全在我們創建的類(KWindowImpl和KDialogImpl或你自己創建的從這兩個類派生出來的類)中了。

            _K_WINDOW_ROOT_IMPL中提供了一些對話框和窗口應用程序都需要處理的公共消息的缺省處理(如WM_CREATE,WM_DESTROY,WM_COMMAND,WM_NOTIFY等等),和一些與窗口句柄(HWND)相關的函數(如GetWindowRect等等)。并且在_K_WINDOW_ROOT_IMPL的缺省構造函數中完成了Thunk的初始化。這里主要說一下該類中的ProcessBaseMessage方法:

            template <class T>
            class __declspec(novtable) _K_WINDOW_ROOT_IMPL
            {
            _K_THUNKED_DATA _thunk;
            public:
            _K_WINDOW_ROOT_IMPL () : m_hWnd (NULL)
            {
            T* pThis = static_cast<T *>(this);
            _thunk.Init ((DWORD_PTR) pThis, pThis->GetMessageProcPtr());
            /* The above GetMessageProcPtr was defined in derived class KDialogImpl and KWindowImpl */
            }
            ...
            LRESULT ProcessBaseMessage (UINT msg, WPARAM wParam, LPARAM lParam)
            {
            T* pThis = static_cast<T *>(this); /* 'Override' support */
            LRESULT r = __DO_DEFAULT;
            switch (msg)
            {
            case WM_COMMAND:
            r = pThis->OnCommand (HIWORD(wParam), LOWORD(wParam), (HWND) lParam); break;
            case WM_NOTIFY:
            r = pThis->OnNotify ((int) wParam, (LPNMHDR) lParam); break;
            /* Other window message can be handled here*/
            }
            return r == __DO_DEFAULT ? pThis->DoDefault (msg, wParam, lParam) : r;
            }
            LRESULT OnCommand (WORD wNotifyCode, WORD wId, HWND hWndCtrl) { return __DO_DEFAULT; }
            LRESULT OnNotify (int idCtrl, LPNMHDR pnmh) { return __DO_DEFAULT; }
            LRESULT OnXXXXXX (...) { return __DO_DEFAULT; } /* 其他消息處理函數的定義 */
            ...
            };
            

            需要說明的是, 在該類中定義的大量(缺省)消息處理函數,是為了能夠在其子類中“重載”這些消息處理函數。這里為了實現的缺省消息處理的簡單性使用了一種比較勉強的方法,即假定沒有任何消息處理函數會返回-2。比如,當你在你的對話框類中“重載”OnCommand方法時(當然,你的對話框類必須要繼承KDialogImpl),該方法就能夠在WM_COMMAND消息發生時被系統自動調用,關于基于模板的重載,請參見前面的章節:使用C++模板實現函數的“重載”

            好,再讓我們來看看KDialogImpl的實現:

            template <class T>
            class KDialogImpl
            : public _K_WINDOW_ROOT_IMPL <T>
            {
            public:
            KDialogImpl () : _K_WINDOW_ROOT_IMPL<T> () {}
            inline DWORD GetMessageProcPtr ()
            {
            DWORD dwProcAddr = 0;
            __asm
            {   /* Use the prefix 'T::' to enable the overload of KDialogProc in derived class */
            mov eax, offset T::KDialogProc
            mov dword ptr [dwProcAddr], eax
            }
            return dwProcAddr;
            }
            INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam) { return __DO_DEFAULT; }
            /* Other dialog-based message hanlder can be declared here */
            LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam) { return 0; }
            INT_PTR DoModal (
            HWND hWndParent = ::GetActiveWindow( ),
            LPARAM lpInitParam = NULL)
            {
            return DialogBoxParam (
            m_hInstance, MAKEINTRESOURCE(T::IDD),
            hWndParent,
            (DLGPROC) GetThunkedProcPtr(), lpInitParam);
            }
            INT_PTR KCALLBACK KDialogProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
            {
            /* Cache the window handle when KDialogProc was called at first time */
            if (m_hWnd == NULL)
            m_hWnd = hWnd;
            __ASSERT(m_hWnd == hWnd);
            T* pThis = static_cast<T *> (this); /* 'Override' support */
            INT_PTR fReturn = 0;
            switch (msg)
            {
            case WM_INITDIALOG:
            fReturn = pThis->OnInitDialog ((HWND) wParam, lParam);
            break;
            case WM_GETDLGCODE:
            fReturn = pThis->OnGetDlgCode ((LPMSG) lParam);
            break;
            /* Other dialog-based message can be handled here */
            default:
            fReturn = __super::ProcessBaseMessage (msg, wParam, lParam);
            }
            if (fReturn == __DO_DEFAULT) return DoDefault (msg, wParam, lParam);
            if (fReturn)
            {
            switch (msg)
            {
            case WM_COMPAREITEM         :
            case WM_VKEYTOITEM          :
            case WM_CHARTOITEM          :
            case WM_INITDIALOG          :
            case WM_QUERYDRAGICON       :
            case WM_CTLCOLORMSGBOX      :
            case WM_CTLCOLOREDIT        :
            case WM_CTLCOLORLISTBOX     :
            case WM_CTLCOLORBTN         :
            case WM_CTLCOLORDLG         :
            case WM_CTLCOLORSCROLLBAR   :
            case WM_CTLCOLORSTATIC      :
            break;
            default:
            ::SetWindowLongPtr(m_hWnd, DWLP_MSGRESULT, (LONG) fReturn);
            break;
            }
            }
            return fReturn;
            }
            ...
            inline BOOL SetDlgItemInt (int nIDDlgItem, UINT uValue, BOOL bSigned = TRUE)
            { return ::SetDlgItemInt (m_hWnd, nIDDlgItem, uValue, bSigned); }
            inline UINT IsDlgButtonChecked  (int nIDButton)
            { return ::IsDlgButtonChecked (m_hWnd, nIDButton); }
            /* 其他對話框常用函數 */
            };
            

            最后我們來看看KWindowImpl的實現,KWindowImpl的實現較KDialogImpl要復雜些,在KWindowImpl中,我們需要注冊特定的窗口類,另外,對于同一類型的窗口,在該類窗口的多實例下,我們需要保證每個實例的this指針能夠被正確傳遞到消息處理函數中,所以我們需在每次創建該類型的窗口時將該類窗口的窗口過程(Window Procedure)地址改為當前實例的窗口過程。所以我們需要使用SetClassLongPtr來修改窗口類的消息處理函數地址,因此我們需要一個窗口句柄,這也就是為什么我要在KWindowImpl中定義類型為HWND的靜態變量_hWndLastCreated,我們需要通過它來調用函數SetClassLongPtr(該方法覺得很勉強。也許有更好的方法,但是目前沒有發現)。

            template <class T>
            class KWindowImpl
            : public _K_WINDOW_ROOT_IMPL <T>
            {
            LPTSTR m_lpszClassName;
            static HWND _hWndLastCreated; /* work as sentinel */
            static CRITICAL_SECTION _cs; /* Enable multi-thread safe support */
            static BOOL _fInitilized;
            public:
            inline DWORD GetMessageProcPtr ()
            {
            DWORD dwProcAddr = 0;
            __asm
            {   /* Use the prefix 'T::' to enable the overload of KWndProc in derived class */
            mov eax, offset T::KWndProc
            mov dword ptr [dwProcAddr], eax
            }
            return dwProcAddr;
            }
            /* Disable the default constructor, without specifying the name of window class */
            KWindowImpl () { __ASSERT (FALSE); }
            KWindowImpl (LPCTSTR lpszClassName) : _K_WINDOW_ROOT_IMPL ()
            {
            m_lpszClassName = new TCHAR [_tcslen(lpszClassName) + _tcslen(_T("KWindowImpl")) + 8];
            _stprintf (m_lpszClassName, _T("%s:%s"), _T("JKTL::KWindowImpl"), lpszClassName);
            if (!_fInitilized)
            {   ::InitializeCriticalSection (&_cs);
            _fInitilized = TRUE;
            }
            }
            ~KWindowImpl ()
            {   if (m_lpszClassName) delete[] m_lpszClassName; }
            LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam)
            {   return DefWindowProc (m_hWnd, msg, wParam, lParam); }
            LRESULT KCALLBACK KWndProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
            {
            if (m_hWnd == NULL)
            {   m_hWnd = hWnd;
            _hWndLastCreated = m_hWnd;
            /* Leaving the critical section, when window was created successfully */
            ::LeaveCriticalSection (&_cs);
            }
            __ASSERT (m_hWnd == hWnd);
            /* Do some default message process */
            return __super::ProcessBaseMessage (msg, wParam, lParam);
            }
            BOOL KRegisterClass ()
            {
            WNDCLASSEX wcx = {sizeof WNDCLASSEX};
            /* Enable multi-thread safe, for SetClassLongPtr use ONLY, call here for convenience */
            ::EnterCriticalSection (&_cs);
            if (GetClassInfoEx (m_hInstance, m_lpszClassName, &wcx))
            {    /* Ensure that window subsquently created use it's thunked window procedure,
            SetClassLongPtr will not effect those windows had been created before */
            SetClassLongPtr (_hWndLastCreated, GCL_WNDPROC, (LONG) GetThunkedProcPtr ());
                        return TRUE;
            }
            wcx.cbClsExtra       = 0;
            wcx.cbWndExtra       = 0;
            wcx.hbrBackground    = (HBRUSH) (COLOR_BTNFACE + 1);;
            wcx.hCursor          = LoadCursor (NULL, IDC_ARROW);
            wcx.hIcon            = LoadIcon (NULL, IDI_APPLICATION);
            wcx.hInstance        = m_hInstance;
            wcx.lpfnWndProc      = (WNDPROC) GetThunkedProcPtr ();
                    wcx.lpszClassName    = m_lpszClassName;
            wcx.lpszMenuName     = NULL;
            wcx.style            = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
            return RegisterClassEx (&wcx);
            }
            HWND Create (HWND hWndParent, DWORD dwExStyle, DWORD dwStyle, LPCTSTR lpszWndName,
            RECT& rc, int nCtrlId = 0, LPVOID lpParam = NULL)
            {
            __ASSERT (m_hWnd == NULL);
            if (!KRegisterClass ()) return NULL;
            m_hWnd = ::CreateWindowEx (
            dwExStyle, m_lpszClassName,
            lpszWndName, dwStyle,
            rc.left, rc.top, _RECT_W(rc), _RECT_H(rc),
            hWndParent, (HMENU) nCtrlId, m_hInstance, lpParam);
            return m_hWnd;
            }
            };
            

            對于_K_WINDOW_ROOT_IMPL中的m_hWnd成員的賦值,我們是在我們的窗口過程第一次被調用是設定的(見代碼)。雖然我并不是很喜歡這種寫法,因為我不希望在窗口處理過程出現于消息處理無關的代碼。事實上,我們仍然可以在Thunk代碼中完成對m_hWnd的賦值,因為當系統調用我們的“窗口處理過程”(即Thunk代碼)時,系統已經為我們的窗體分配好了窗口句柄,并位于棧中的ESP+4位置(見圖1),我們完全可以在Thunk中將位于ESP+4處的值保存到m_hWnd中,但是這將增加我們在Thunk中的機器指令的長度,使用下面的代碼就要增加16字節的機器指令(__stdcall調用約定下,在__thiscall調用約定下,只需增加10個字節左右,因為可以直接使用ECX來進行數據交換),在執行Thunk代碼之前,我們需要計算出類對象中m_hWnd成員在該類對象中的偏移位置,這通常可以在類的構造函數中完成(下面的代碼假定m_hWnd在類對象中的偏移位置為8):

            PUSH        DWORD PTR [ESP]
            MOV         DWORD PTR[ESP + 4], pThis
            PUSH    EAX                         ; Save EAX
            PUSH    EBX                         ; Save EBX
            MOV     EAX, pThis
            MOV     EBX, DWORD PTR [ESP + 0Ch]  ; Load HWND from ESP + 0Ch
            MOV     DWORD PTR [EAX + 08h], EBX  ; Set m_hWnd with value of EBX
            POP     EBX                         ; Restore EAX
            POP     EAX                         ; Restore EBX
            JMP         member function-based message handler

            在__thiscall調用約定下為:

            MOV         ECX, pThis
            PUSH    EAX
            MOV     EAX, DWORD PTR [ESP + 8]    ; Load HWND from ESP + 8
            MOV     DWORD PTR [EAX + 08h], EAX  ; Save HWND to m_hWnd
            POP     EAX
            JMP         member function-based message handler
            

            使用類成員函數子類化窗口過程

            在_K_WINDOW_ROOT_IMPL中實現了SubclassWindow和SubclassDialog方法,你可以使用這兩個方法來輕松的實現窗體的子類化操作。下面的代碼演示了怎樣使用SubclassWindow來子類化輸入框(Edit Control):

            class SubclassDemo
            : public KDialogImpl <SubclassDemo>
            {
            _K_THUNKED_SUBCLASS_DATA* m_lpTsdEdit;
            public:
            INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam)
            {
            HWND hEdit = GetDlgItem (IDC_EDIT0);
            m_lpTsdEdit = SubclassWindow (hEdit, EditWndProcHook);
            return TRUE;
            }
            LRESULT KCALLBACK EditWndProcHook (HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
            {
            /* Do anything here you like */
            return CallWindowProc (m_lpTsdEdit->fnOldWndProc, hEdit, msg, wParam, lParam);
            }
            }
            

            結束語

            非常高興你能夠讀到這篇文章的末尾,同時也希望這篇文章能對你理解Windows中的消息處理機制提供一些幫助。歡迎你將你的看法和建議反饋給我(JERKII@HOTMAIL.COM),以彌補由于我當前的知識限制而導致的一些錯誤。

            KWIN的源碼可以從這里下載。

            謝謝!^_^


            posted on 2010-06-27 03:26 楊粼波 閱讀(1893) 評論(0)  編輯 收藏 引用

            久久久久久久波多野结衣高潮| AAA级久久久精品无码片| 欧美久久久久久| 国产午夜久久影院| 三级三级久久三级久久| 国产高潮久久免费观看| 99精品久久久久久久婷婷| 久久99精品国产麻豆宅宅| 中文字幕人妻色偷偷久久| 办公室久久精品| 国内精品久久久久影院优| 中文字幕无码久久人妻| 亚洲成人精品久久| 国内精品久久久久伊人av| 久久精品国产乱子伦| 久久久久亚洲精品天堂久久久久久| 精品熟女少妇a∨免费久久| 日本加勒比久久精品| 久久99精品久久久久久水蜜桃| 久久精品人成免费| 人妻精品久久久久中文字幕69 | 国内精品久久国产| 91久久精品国产成人久久| 久久久av波多野一区二区| 狠狠色丁香久久婷婷综合| 欧洲性大片xxxxx久久久| 国产精品伦理久久久久久| 久久免费视频网站| 97久久超碰国产精品2021| 久久亚洲欧美国产精品| 狠狠色狠狠色综合久久| 国产成人综合久久精品红| 久久婷婷五月综合国产尤物app| 久久婷婷五月综合97色直播| 久久久91人妻无码精品蜜桃HD| 精品久久久久久久中文字幕| 精品视频久久久久| 亚洲精品成人久久久| 久久久精品国产| 伊人久久综合无码成人网| 久久丫精品国产亚洲av不卡|