一种实现Win32窗口过程函数(Window Procedure)的新方法

一种实现Win32窗口过程函数(Window Procedure)的新方法
转自: http://blog.csdn.net/JerKii/archive/2006/04/07/654188.aspx#pass_thispointer_stack

一种实现Win32窗口过程函数(Window Procedure)的新方法

基于Thunk实现的类成员消息处理函数

JERKII.SHANG ([email protected])

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

内容

  • 前言
  • 传统的C++重载
  • 使用C++模板实现函数的“重载”
  • C++对象中的属性和方法(成员函数)
  • 关于调用约定与this指针的传递
  • 通过其他途径调用类中的成员函数
  • 认识Thunk
  • 让Windows直接调用你在类中定义的(非静态)消息处理函数
    • 使用ECX传递this指针(__thiscall)
    • 使用栈传递this指针(__stdcall或__cdecl)
  • 实现我们的KWIN包
  • 使用类成员函数子类化窗口过程
  • 结束语

前言

也许你是一位使用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中的消息处理机制提供一些帮助。欢迎你将你的看法和建议反馈给我([email protected]),以弥补由于我当前的知识限制而导致的一些错误。

KWIN的源码可以从这里下载。

谢谢!^_^


你可能感兴趣的:(一种实现Win32窗口过程函数(Window Procedure)的新方法)