VCL 窗口函数注册机制研究手记,兼与 MFC 比较(转)

来源:互联网 发布:龙之信条女性捏脸数据 编辑:程序博客网 时间:2024/05/27 00:31
VCL 窗口函数注册机制研究手记 ,兼与 MFC 比较

By   王捷   cheka@yeah.net     ( 转载请保留此信息 )

      这个名字起的有些耸人听闻 ,无他意 ,只为吸引眼球而已 ,如果您对下列关键词有兴

趣 ,希望不要错过本文 :

1.        VCL 可视组件在内存中的分页式管理 ;

2.         让系统回调类的成员方法

3.        Delphi  中汇编指令的使用

4.        Hardcore

5.         第 4 条是骗你的

     我们知道 Windows 平台上的 GUI 程序都必须遵循 Windows 的消息响应机制 ,可以简单概括

如下 ,所有的窗口控件都向系统注册自身的窗口函数 ,运行期间消息可被指派至特定窗口

控件的窗口函数处理。对消息相应机制做这样的概括有失严密 ,请各位见谅 ,我想赶紧转

向本文重点 ,即在利用 Object Pascali 或是 C++ 这样的面向对象语言编程中 ,如何把一个类

的成员方法向系统注册以供回调。

     在注册窗口类即调用 RegisterClass 函数时 ,我们向系统传递的是一个 WindowProc  类

型的函数指针

WindowProc  的定义如下

LRESULT CALLBACK WindowProc(

  HWND hwnd,      // handle to window

  UINT uMsg,      // message identifier

  WPARAM wParam,  // first message parameter

  LPARAM lParam   // second message parameter

);

     如果我们有一个控件类 ,它拥有看似具有相同定义的成员方法 TMyControl.WindowProc,

可是却不能够将它的首地址作为 lpfnWndProc 参数传给 RegisterClass,道理很简单 ,因为

Delphi 中所有类成员方法都有一个隐含的参数 ,也就是 Self,因此无法符合标准

 WindowProc  的定义。

     那么 ,在 VCL 中 ,控件向系统注册时究竟传递了一个什么样的窗口指针 ,同时通过这个

指针又是如何调到各个类的事件响应方法呢?我先卖个关子 ,先看看 MFC 是怎么做的。

     在调查 MFC 代码之前 ,我作过两种猜想 :

一 ,作注册用的函数指针指向的是一个类的静态方法 ,

静态方法同样不需要隐含参数  this ( 对应  Delphi 中的  Self ,不过 Object Pascal 不支持

静态方法 )

二 ,作注册用的函数指针指向的是一个全局函数 ,这当然最传统 ,没什么好说的。

     经过简单的跟踪 ,我发现 MFC 中 ,全局函数 AfxWndProc 是整个 MFC 程序处理消息的 " 根

节点 ",也就是说 ,所有的消息都由它指派给不同控件的消息响应函数 ,也就是说 ,所有

的窗口控件向系统注册的窗口函数很可能就是  AfxWndProc ( 抱歉没做深入研究 ,如果不

对请指正 ) 。而 AfxWndProc  是如何调用各个窗口类的 WndProc 呢?

     哈哈 ,MFC 用了一种很朴素的机制 ,相比它那么多稀奇古怪的宏来说 ,这种机制相当好理解 : 使用一个全局的 Map 数据结构来维护所有的窗口对象和 Handle( 其中 Handle 为键值 ),然后 AfxWndProc 根据 Handle 来找出唯一对应的窗口对象 ( 使用静态函数 CWnd::FromHandlePermanent(HWND hWnd) ),然后调用其 WndProc,注意 WndProc 可是虚拟方法 ,因此消息能够正确到达所指定窗口类的消息响应函数并被处理。

     于是我们有理由猜想 VCL 也可能采用相同的机制 ,毕竟这种方式实现起来很简单。我确

实是这么猜的 ,不过结论是我错了 ......

     开场秀结束 ,好戏正式上演。

     在 Form1 上放一个 Button( 缺省名为 Button1),在其 OnClick 事件中写些代码 ,加上断点 ,

F9 运行 ,当停留在断点上时 ,打开 Call Stack 窗口 (View->Debug Window->Call Stack,

或者按 Ctrl-Alt-S ) 可看到调用顺序如下 ( 从底往上看 ,stack 嘛 )

(  如果你看到的  Stack  和这个不一致 ,请打开 DCU  调试开关

 Project->Options->Compiler->Use Debug DCUs,  这个开关如果不打开 ,是没法调试 VCL

源码的  )

TForm1.Button1Click(???)

TControl.Click

TButton.Click

TButton.CNCommand ((48401, 3880, 0, 3880, 0))

TControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TWinControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TButtonControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TControl.Perform (48401,3880,3880)

DoControlMsg (3880,(no value))

TWinControl.WMComman d((273, 3880, 0, 3880, 0))

TCustomForm.WMCommand ((273, 3880, 0, 3880, 0))

TControl.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TWinControl.WndProc((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TCustomForm.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

TWinControl.MainWndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))

StdWndProc (3792,273,3880,3880)

      可见  StdWndProc  看上去象是扮演了 MFC 中  AfxWndProc  的角色 ,不过我们先不谈它 ,

如果你抑制不住好奇心 ,可以提前去看它的源码 ,在 Forms.pas 中 ,看到了么 ?  是不是

特 ~~~~ 别有趣阿。

      实际上 ,VCL 在 RegisterClass 时传递的窗口函数指针并非指向 StdWndProc 。那是什么

呢?

      我跟 ,我跟 ,我跟跟跟 ,终于在 Controls.pas 的 TWindowControl 的实现代码中

(procedure TWinControl.CreateWnd;)  看到了 RegisterClass 的调用 ,hoho,终于找到组

织了 ......别忙 ,发现了没 ,这时候注册的窗口函数是 InitWndProc,看看它的定义 ,嗯 ,

符合标准 ,再去瞧瞧代码都干了些什么。

     发现这句 :

SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));

     我 Faint,搞了半天 InitWndProc 初次被调用 ( 对每一个 Wincontrol 来说 ) 就把自个儿

给换了 ,新上岗的是 FObjectInstance 。下面还有一小段汇编 ,是紧接着调用

FObjectInstance 的 ,调用的理由不奇怪 ,因为以后调用 FObjectInstace 都由系统 CallBack

了 ,但现在还得劳 InitWndProc 的大驾去 call 。调用的方式有些讲究 ,不过留给您看完这篇

文章后自个儿琢磨去吧。

     接下来只能继续看 FObjectInstance 是什么东东 ,它定义在  TWinControl  的  Private

段 ,是个 Pointer 也就是个普通指针 ,当什么使都行 ,你跟 Windows 说它就是  WndProc  型指

针  Windows  拿你也没辙。

     FObjectInstance 究竟指向何处呢 ,镜头移向  TWincontrol  的构造函数 ,这是

FObjectInstance 初次被赋值的地方。   多余的代码不用看 ,焦点放在这句上

     FObjectInstance := MakeObjectInstance(MainWndProc);

      可以先告诉您 ,MakeObjectInstance 是本主题最精彩之处 ,但是您现在只需知道

FObjectInstance" 指向了 "MainWndProc,也就是说通过某种途径 VCL 把每个 MainWndProc

作为窗口函数注册了 ,先证明容易的 ,即  MainWndProc  具备窗口函数的功能 ,来看代码 :

(  省去异常处理  )

procedure TWinControl.MainWndProc(var Message: TMessage);

begin

      WindowProc(Message);

      FreeDeviceContexts;

      FreeMemoryContexts;

end;

FreeDeviceContexts;  和   FreeMemoryContexts  是保证 VCL 线程安全的 ,不在本文讨论之列

,只看 WindowProc(Message);  原来  MainWndProc  把消息委托给了方法  WindowProc 处理 ,

注意到  MainWndProc  不是虚拟方法 ,而  WindowProc  则是虚拟的 ,了解  Design Pattern

的朋友应该点头了 ,嗯 ,是个  Template Method ,  很自然也很经典的用法 ,这样一来所有

的消息都能准确到达目的地 ,也就是说从功能上看  MainWndProc  确实可以充作窗口函数。

您现在可以回顾一下 MFC 的  AfxWindowProc  的做法 ,同样是利用对象的多态性 ,但是两种方

式有所区别。

     是不是有点乱了呢 ,让我们总结一下 ,VCL  注册窗口函数分三步 :

1.  [ TWinControl.Create ]

    FObjectInstance  指向了  MainWndProc

2.  [ TWinControl.CreateWnd ]

    WindowClass.lpfnWndProc  值为  @InitWndProc;

     调用 Windows.RegisterClass(WindowClass) 向系统注册

3.  [ InitWndProc  初次被 Callback 时  ]

    SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance))

     窗口函数被偷梁换柱 ,从此  InitWndProc  退隐江湖

    ( 注意是对每个 TWinControl 控件来说 ,InitWndProc  只被调用一次 )

     前面说过 ,非静态的类方法是不能注册成为窗口函数的 ,特别是 Delphi 中

根本没有静态类方法 ,那么 MainWndProc  也不能有特权 ( 当然宝兰可以为此在编译器上动点

手脚 ,如果他们不怕成为呕像的话 ) 。

     那么 ,那么 ,您应该意识到了 ,在幕后操纵一切的 ,正是 ......

     背景打出字幕

     超级巨星 : 麦克奥布吉特因斯坦斯

             (MakeObjectInstance)

     天空出现闪电 ,哦耶 ,主角才刚刚亮相。

     废话不说 ,代码伺候 :

(  原始码在  Form.pas  中 ,"{}" 中是原始的注释 ,而 " //"  后的是我所加 ,您可以直

接就注释代码 ,也可以先看我下面的评论 ,再回头啃 code )

//  共占  13 Bytes,变体纪录以最大值为准

type

  PObjectInstance = ^TObjectInstance;

  TObjectInstance = packed record

    Code: Byte;                     // 1 Bytes

    Offset: Integer;                // 4 Bytes

    case Integer of

      0: (Next: PObjectInstance);  // 4 Bytes

      1: (Method: TWndMethod);     // 8 Bytes

                                   // TWndMethod  是一个指向对象方法的指针 ,

                                   //  事实上是一个指针对 ,包含方法指针以

                                   //  及一个对象的指针 ( 即 Self )

  end;

// 313 是满足整个 TInstanceBlock 的大小不超过 4096 的最大值

InstanceCount = 313;

//  共占  4079 Bytes

type

  PInstanceBlock = ^TInstanceBlock;

  TInstanceBlock = packed record

    Next: PInstanceBlock;        // 4 Bytes

    Code: array[1..2] of Byte;   // 2 Bytes

    WndProcPtr: Pointer;         // 4 Bytes

    Instances: array[0..InstanceCount] of TObjectInstance; 313 * 13 = 4069

  end;

function CalcJmpOffset(Src, Dest: Pointer): Longint;

begin

  Result := Longint(Dest) - (Longint(Src) + 5);

end;

function MakeObjectInstance(Method: TWndMethod): Pointer;

const

  BlockCode: array[1..2] of Byte = (

    $59,       { POP ECX }

    $E9);      { JMP StdWndProc }  //  实际上只有一个 JMP

  PageSize = 4096;

var

  Block: PInstanceBlock;

  Instance: PObjectInstance;

begin

  // InstFreeList = nil  表明一个 Instance block 已被占满 ,于是需要为一个新

  // Instance block 分配空间 ,一个个 Instance block 通过 PinstanceBlock 中的

  // Next  指针相连 ,形成一个链表 ,其头指针为 InstBlockList

  if InstFreeList = nil then

  begin

    //  为 Instance block 分配虚拟内存 ,并指定这块内存为可读写并可执行

    // PageSize  为 4096 。

    Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    Block^.Next := InstBlockList;

    Move(BlockCode, Block^.Code, SizeOf(BlockCode));

    Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));

    //  以下代码建立一个 Instance 的链表

    Instance := @Block^.Instances;

    repeat

      Instance^.Code := $E8;  { CALL NEAR PTR Offset }

      // 算出相对  jmp StdWndProc 指令的偏移量 ,放在 $E8 的后面

      Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);

      Instance^.Next := InstFreeList;

      InstFreeList := Instance;

      //  必须有这步 ,让 Instance 指针移至当前 instance 子块的底部

      Inc(Longint(Instance), SizeOf(TObjectInstance));

      //  判断一个 Instance block 是否已被构造完毕

    until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);

    InstBlockList := Block;

  end;

  Result := InstFreeList;

  Instance := InstFreeList;

  InstFreeList := Instance^.Next;

  Instance^.Method := Method;

end;

     不要小看这区区几十行代码的能量 ,就是它们对  VCL  的可视组件进行了分页式管理 ,

( 代码中对两个链表进行操作 ,InstanceBlock  中有  ObjectInstance  的链表 ,而一个个

InstanceBlock   又构成一个链表  ) 一个  InstanceBlock  为一页 ,有 4096  字节 ,虽然

InstanceBlock  实际使用的只有  4079  字节 ,不过为了  Alignment ,就加了些  padding

凑满  4096  。从代码可见每一页中可容纳  313  个所谓的 ObjectInstance,如果望文生义

很容易将这个  ObjectInstance  误解为对象实例 ,其实不然 ,每个 ObjectInstance  其实是

一小段可执行代码 ,而这些可执行代码不是编译期间生成的 ,也不是象虚拟函数那样滞后

联编 ,而根本就是 MakeObjectInstance  在运行期间 " 创作 " 的 ( 天哪 )!  也就是说 ,

MakeObjectInstance  将所有的可视 VCL 组件   改造成了一页页的可执行代码区域 ,是不是

很了不起呢。

     不明白 ObjectInstance 所对应的代码是做什么的么?没关系 ,一起来看

                call  - - - - - - - - - - - >  pop ECX                           //  在 call  之前 ,下一个指令的地址会被压栈

                @MainWndProc                                                     //  紧接着执行 pop ECX,  为何这么做呢?

                @Object( 即 Self)  //  前面注释中提过

     答案在  StdWndProc  的代码中 ,要命哦 ,全是汇编 ,可是无限风光在险峰 ,硬着头皮闯

一回吧。

      果不其然 ,我们发现其中用到了 ECX

function StdWndProc(Window: HWND; Message, WParam: Longint;

  LParam: Longint): Longint; stdcall; assembler;

asm

        XOR     EAX,EAX

        PUSH    EAX

        PUSH    LParam

        PUSH    WParam

        PUSH    Message

        MOV     EDX,ESP

        MOV     EAX,[ECX].Longint[4] //    相当于  MOV EAX, [ECX+4] ( [ECX+4]  是什么?就是 Self )

        CALL    [ECX].Pointer        //    相当于  CALL    [ECX] ,  也就是调用  MainWndProc

        ADD     ESP,12

        POP     EAX

end;

     这段汇编中在调用 MainWndProc 前作了些参数传递的工作 ,由于 MainWndProc  的定义如

下 :

procedure TwinControl..MainWndProc(var Message: TMessage);

     根据 Delphi  的约定 ,这种情况下隐函数 Self  作为第一个参数 ,放入 EAX  中 ,

TMessage  结构的指针作为第二个参数 ,放入 EDX 中 ,而 Message 的指针从哪儿来呢?我们看

到在连续几个  Push  之后 ,程序已经在堆栈中构造了一个 TMessage  结构 ,而这时的 ESP

当然就是这个结构的指针 ,于是将它赋给 EDX  。如果您不熟悉这方面的约定 ,可以参考

Delphi  的帮助 Object Pascal Refrence -> Program Control 。

     现在真相大白 ,Windows  消息百转千折终于传进 MainWndProc ,  不过这一路也可谓相

当精彩 ,MakeObject 这一函数自然是居功至伟 , StdWndProc  也同样是幕后英雄 ,让我们

把  MakeObjectInstance  作出的代码和 StdWndProc  连接起来 ,哦 ,堪称鬼斧神工 .

   (  大富翁没法显示图像 ,可以去

     http://jp.njuct.edu.cn/crystal/article\vcl%20hardcore.htm

      看完整全文 ,感谢房客支持 )

     就此在总结一下 , FobjectInstance  被 VCL  注册为窗口函数 ,而实际上

FObjectInstance  并不实际指向某个函数 ,而是指向一个 ObjectInstance,  而后者我们已

经知道是一系列相接的可执行代码段当中的一块 ,当系统需要将  FObjectInstance  当做

窗口函数作为回调时 ,实际进入了 ObjectInstance  所在的代码段 ,然后几番跳跃腾挪 (

一个 call  加一个  jump ) 来到 StdWndProc ,StdWndProc  的主要功用在于将 Self  指针

压栈 ,并把 Windows 的消息包装成 Delphi 的 TMessage  结构 ,如此才能成功调用到

TWinControl 类的成员方法  MainWndProc,  消息一旦进入 MainWndProc  便可以轻车熟路一路

高唱小曲来到各个对象转属的 WndProc ,  从此功德圆满。

     后记 :

     个人感觉在这一技术上 VCL  要比 MFC  效率高出不少 ,后者每次根据窗口句柄来检索相

对应的窗口对象指针颇为费时 ,同时 MakeObject   的代码也相当具有参考价值 ,有没有想

过让你自己的程序在内存中再开一堆可执行代码?

     所有的代码是基于 Delphi5 的 ,可能与其余版本有所出入 ,但相信不会很大。

     整个星期六和星期天我都花在写作此文上了 ( 连调试带写字 ),  不过水平所限 ,难免

有所错误与表达不周 ,但愿不至以己昏昏令人昏昏 ,欢迎来信探讨指教

cheka@yeah.net , thanx
原创粉丝点击