`
isiqi
  • 浏览: 16036092 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

从普通函数到对象方法 ------Windows窗口过程的面向对象封装

阅读更多

从普通函数到对象方法

------Windows窗口过程的面向对象封装

开始,由VirtualAlloc想起

我在查看VirtualAlloc这个API的时候,思绪竟然跳到另一个地方去了。那是以前阅读VCL源码时遗留下来的问题,Classes单元的MakeObjectInstance函数调用了VirtualAlloc,我甚是不解,为什么Delphi提供了那么多内存分配函数,而MakeObjectInstance偏偏要用系统提供的API,更令我不解的是,之后再也不见有VirtualFree的调用,也就是说,VCL其实存在内存泄漏?这个问题我在网上也看到相关的讨论,有人认为这的确是VCLBug,有人甚至修改了Classes单元,在单元的结束节处调用VirtualFree以释放以前分配的内存。

不过我对这个问题始终持保留态度,MakeObjectInstance是一个非常重要的函数,担负着窗口过程到对象方法的转换,Borland没有理由留着这个“Bug”不理。

于是我重新阅读了MakeObjectInstance这个倍受李维赞誉的函数,我想我这次是读懂了,为什么不调用VirtualFree,因为没有必要,进程在结束的时候会毫无保留的回收所有的内存,而经由VirtualAlloc分配到的内存就保留在由TInstanceBlock记录所组成的链表中,这个链表组成的内存并不是使用一次即弃掉的,它是可重用的,调用一次FreeObjectInstance,那张链表便空余出TObjectInstance大小的内存,以供下一次使用。所以,这其实是一个内存池,提供了更上一层的内存分配机制。而在结束的时候调用VirtualFree就显得没有任何必要了。

回到上面提出的第一个问题,为什么要调用VirtualAlloc,而不用Delphi提供的内存分配函数,如果没有看到System单元的这两个变量,我想永远也不可能找到答案:

var

AllocMemCount: Integer; { Number of allocated memory blocks }

AllocMemSize: Integer; { Total size of allocated memory blocks }

这两个变量的确是记录内存使用的总量,前提是你调用Delphi提供的内存管理函数,如果调用Windows原生的API,则VCL是没有办法感应到的。写到这里,再看看上面的描述,也许一切都了然了。

然而,这只是我写这篇文章的导火线,真正原因是我读懂了MakeObjectInstance,以前的许多疑惑已经拨云见日,窗口过程到对象方法的脉络在我的脑中从未有过这么清晰,因此欲罢不能,作此文记之。

使用,将窗口过程转成对象方法的步骤

SDK的角度来讲,设置窗口过程有两种方法(我所能想到的),一是调用RegisterClass,另一个是调用SetWindowLong,第一种用在创建窗口的时候,另一种用在改变窗口过程的时候。在Delphi中,假设你写了一个自定义窗口类,那么你可以重载WndProc,这个方法就相当于窗口过程。可以确定,VCL在开始时肯定也是用上面所说的方法,设置窗口过程,只是后来经过一些转换,最终使窗口过程调用到对象实例的WndProc,所以WndProc可以当成窗口过程来使用。

这个转换的步骤从表面上看很简单,现在我们不必去深究其原理,只要知道通过下面的做法,就可以将一个窗口过程转成对象的方法。

首先,到Controls单元的TWinControl类,这是所有窗口的父类,转换过程就在这里面完成。TWinControl的构造函数中写了这一句:

constructor TWinControl.Create(AOwner: TComponent);

begin

... ...

FObjectInstance := Classes.MakeObjectInstance(MainWndProc);

... ...

end;

其中的MainWndProc就是代替窗口过程的对象方法。

接着,在InitWndProc有如下代码:

function InitWndProc(HWindow: HWnd; Message, WParam,

LParam: Longint): Longint;

Begin

... ...

SetWindowLong(HWindow, GWL_WNDPROC,

Longint(CreationControl.FObjectInstance));

... ...

end;

InitWndProc就是刚开始的窗口过程,而调用了SetWindowLong之后,窗口过程就转成了FobjectInstance了。而实际上最终得到调用是却是MainWndProc

最后,在TWinControl的析构函数中还写了如下语句:

destructor TWinControl.Destroy;

begin

... ...

if FObjectInstance <> nil then

Classes.FreeObjectInstance(FObjectInstance);

... ...

end;

这是为了回收由MakeObjectInstance使用的内存,让这块内存可在下一次重用。

上面就是TWinControl的窗口过程到对象方法的转换步骤,这的确是很神奇的事情,它们在某些情况下是很有用的,比如TComboBox,在这个控件里面有一个用于编辑的Edit和一个用于下拉选择的ListBox,这两个控件是在ComboBox创建的时候一起创建的,VCL没有办法对它们进行封装,但有时候需要处理他们的消息,这时,上面的方法就派上用场了,事实上TComboBox就是运用上面的方法,将EditListBox的窗口过程转换成TcomboBox内部的方法的,有兴趣者请查阅一下VCL

对上面进行一次总结:

1、 假设你通过原生的API创建了一个窗口,如果你想让这个窗口的窗口过程被指定为一个类的方法,那么可以在类的内部调用MakeInstanceObject,传进类的一个方法(如上面的MainWndProc,当然这个方法必须是TwndMethod类型的),并保留函数返回的指针。

2、 调用SetWindowLong,用类保留的指针替换原来的窗口过程。到这里,窗口过程就被传进MakeInstanceObject的对象方法所代替了。

3、 在消毁这个类的实例时,别忘了调用FreeObjectInstance,并传回保留的指针。如果这时窗口还未消毁,还得用SetWindowLong恢复原来的窗口过程。

知道如何使用并不是我们的最终目的,我们要更进一步,为什么会是这样,请看下一节。

实现,窗口过程到对象方法的转换技术

窗口过程实际上是一个回调函数,向API传递函数的地址,Windows保留着这个函数地址,在适当的时候调用这个函数。那么对象方法与普通函数有什么不同呢,对于同一种调用规则来说,不同之处就是对象方法在第一个参数之前有一个隐藏的参数,这个参数就是对象的实例(如果是C++应该叫实例指针,而Delphi的对象实例就是一个指针,只已经为大多数人所共知的事实)。

另一方面,WindowsAPI使用的是Stdcall的调用规则,从机器指令的角度看,就是在Call某个函数之前,先将函数的参数从右向左地压栈。而Delphi为了提高效率,默认使用了Register调用规则,粗略的讲就是从左向右传递参数,且前三个参数分别放在EAXEDXECX寄存器中,其后则依次入栈。若要知道详细的规则,请查看Delphi的帮助主题:Calling conventions

现在,如果我们想让窗口过程流入某个对象的方法,要解决两个问题:

1、 在进入对象方法的入口时,先将对象实例作为第一个参数传入,其次再将窗口过程的参数依次传入。对于Register调用规则来说,就是将对象实例赋值给EAX,再将其他参数按照规则赋给相应的寄存器或者压栈。

2、 Stdcall规则到Register规则的转换,这个不是必须的,因为Delphi也支持StdCall规则,但对Register规则来说效率更高,另一方面DelphiRegister规则作了更多的支持,比如Published的属性就只能指定Register规则的方法。

现在让我们围线着这两个问题开始探索VCL是如何做的。

VCL在开始的时候同样要遵守Win32的做法,首先填充一个窗口类结构然后注册窗口类,注意TWinControl.CreateWnd中的这一句:

WindowClass.lpfnWndProc := @InitWndProc;

它将窗口过程指定为InitWndProc函数。

接下来就创建窗口类,在TWinControl.CreateWindowHandle中:

FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,

X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);

现在来看,一切都似乎正常,但其实在调用CreateWindowEx的时候,事情正在稍稍发生变化。CreateWindowEx的时候系统将发送(请注意是发送而不是投递)一个WM_CREATE消息给窗口,处理这个消息的是谁呢,正是上面看到的InitWndProc

有必要看一下这个函数的代码,我顺便作了详细的注释:

01functionInitWndProc(HWindow:HWnd;Message,WParam,
02LParam:Longint):Longint;
03Begin
04//CreationControl就是窗口类,TWinControlCreateWnd的时候将Self赋给它
05//由此可以看到VCL的窗口类是非线程安全的。
06CreationControl.FHandle:=HWindow;
07//重设窗口过程,从此之后,这个函数再也不会得到调用了
08SetWindowLong(HWindow,GWL_WNDPROC,
09Longint(CreationControl.FObjectInstance));
10if(GetWindowLong(HWindow,GWL_STYLE)andWS_CHILD<>0)and
11(GetWindowLong(HWindow,GWL_ID)=0)then
12 SetWindowLong(HWindow,GWL_ID,HWindow);
13//设置该窗的一些属性,与我们讨论的无关,可不去理会它们
14SetProp(HWindow,MakeIntAtom(ControlAtom),THandle(CreationControl));
15SetProp(HWindow,MakeIntAtom(WindowAtom),THandle(CreationControl));
16//主动调用一次FobjectInstance
17asm
18PUSHLParam
19PUSHWParam
20PUSHMessage
21PUSHHWindow
22MOVEAX,CreationControl
23MOVCreationControl,0
24CALL[EAX].TWinControl.FObjectInstance
25MOVResult,EAX
26end;
27end;

6行对窗口类的Fhandle进行赋值,这么做是必要的,因为正常情况下Fhandle只有到CreateWindowsEx返回之后才能得到赋值,在这个函数调用的过程中,系统发送WM_CREATE消息给窗口,在外部,我们可以得到WM_CREATE的处理器进行处理,如果没有第6行的赋值,则那时我们将没有办法得到窗口句柄。我想这也是InitWndProc存在的原因之一。

8行重新设置窗口过程,设置为窗口类的FobjectInstance,从此以后,窗口消息只会流到FobjectStance指向的地方,这个函数也就作废了。

而接下来是一段汇编代码,主要的意思是调用FobjectInstance1821行传递参数(还记得STDCALL规则吗),然后24行调用FobjectInstance。这段汇编就相当于这样的语句:

WinControl := CreationControl;

CreationControl := nil;

Result := TThunkProc(WinControl.FObjectInstance)(HWindow, Message, WParam, LParam);

其实这正是Linux版下面的做法。

在这里我想说一下CALL指令,理解它的行为,对下文是很有帮助的,CALL指令可以分解为两个动作:先将下一条指令的地址(EIP)压栈,然后跳转到操作数指定的地址去。与CALL对应的是RET指令,这个指令其实就是从栈顶弹出一个值,然后跳转到这个值指明的地址去。这就是函数的原理,在函数内部,维持堆栈的平衡是非常重要的,你必须保证在RET的时候弹出来的值正是CALL的时候压入的值,这样才能正确返回到CALL指令的下一条指令的地址,要不然执行点就不知跳到哪里去了?当然使用高级语言不用去关心这些东西,但理解堆栈的知识仍然是非常有用的。

InitWndProc完成它的历史命令之后,我们可以把目光关注到FobjectInstance这个指针去,现在它就是新的窗口过程,但是它到底指向了什么东西呢,答案就在前面看到的MakeObjectInstance中,我们要去详细的分解这个函数的代码,不过之前我要从总体上说一下这个过程:

FobjectInstance指向一块由MakeObjectInstance分配好的内存,这块内存存放的是一段机器指令,这段机器指令其实也是在MakeObjectInstance写入的,当FobjectInstance得到调用时,就执行了那段指令,这段指令的任务是将对象方法(这个方法就是传入MakeObjectInstance的那个参数,即MainWndProc)存放在ECX,然后跳转到StdWndProc去,StdWndProcECX取出MainWndProc,并从这个方法中得到对象实例(对象方法其实是一个地址和一个对象实例的组合,详情请看TMethod帮助),然后构造出一个Tmessage的结构,最后调用MainWndProc,流程完毕。

为了让读者有一个总体的认知,我画了下面的流程图:

从上面的分析看,至少有这么几个元素对转换过程起着至关重要的作用:

MakeObjectInstance函数

FObjectInstance以及其指向的内存

StdWndProc函数

现在我们就来详细解析它们。

TWinControl的构造函数中调用了MakeObjectInstance,并传入TWinControl的一个方法:MainWndProcMakeObjectInstance的代码是这样的:

01functionMakeObjectInstance(Method:TWndMethod):Pointer;
02const
03//机器指令
04BlockCode:array[1..2]ofByte=(
05$59,{POPECX}
06$E9);{JMPStdWndProc}
07PageSize=4096;
08var
09Block:PInstanceBlock;
10Instance:PObjectInstance;
11Begin
12//InstFreeList指向一个TObjectInstance记录,这个记录是当前可用的
13ifInstFreeList=nilthen
14begin
15//如果InstFreeList为空,就再创建4K的内存,这个内存格式化为一个
16//TinstanceBlock结构。
17Block:=VirtualAlloc(nil,PageSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
18Block^.Next:=InstBlockList;
19//对新创建的4K内存进行初始化
20Move(BlockCode,Block^.Code,SizeOf(BlockCode));
21Block^.WndProcPtr:=Pointer(CalcJmpOffset(@Block^.Code[2],@StdWndProc));
22//TinstanceBlock里面含有313TobjectInstance记录,对这些记录进行初始化
23Instance:=@Block^.Instances;
24repeat
25Instance^.Code:=$E8;{CALLNEARPTROffset}
26Instance^.Offset:=CalcJmpOffset(Instance,@Block^.Code);
27Instance^.Next:=InstFreeList;
28InstFreeList:=Instance;
29Inc(Longint(Instance),SizeOf(TObjectInstance));
30untilLongint(Instance)-Longint(Block)>=SizeOf(TInstanceBlock);
31InstBlockList:=Block;
32end;
33//将可用的TobjectInstance块返回,并让InstFreeList指向下一个可用的块
34Result:=InstFreeList;
35Instance:=InstFreeList;
36InstFreeList:=Instance^.Next;
37//MainWndProc保存在这里
38Instance^.Method:=Method;
39end;

这个函数一个非常重要的任务就是管理一个链表,这个链表的每一项有4096字节大小,每一项可以认为是一个TinstanceBlock结构(实际上TinStanceBlock只有4092字节,即最后4个字节是没有用的)。这个链表会随着MakeObjectInstance 的调用而增加链表项,但是不会被释放,到进程结束时由操作系统回收。InstBlockList变量指向这个链表头,可以用下图来表示:

每一个TinstanceBlock的结构是这样的:

PInstanceBlock = ^TInstanceBlock;

TInstanceBlock = packed record

Next: PInstanceBlock; //下一个块

Code: array[1..2] of Byte; //机器码

WndProcPtr: Pointer; //指针,相当于操作数

Instances: array[0..InstanceCount] of TObjectInstance;//314个记录数组

end;

CodeWndProcPtr一起组成了一段机器指令,请回头看看第2021行,最后CodeWndProcPtr成员一起组成了类似下面这样的指令:

POP ECX

JMP Offset

上面的Offset是另有用意的,它等于WndProcPtr,而Jmp的结果是跳到StdWndProc的入口点去,为什么能够这样呢,请看第21行:

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

CalcJmpOffset函数如下
function CalcJmpOffset(Src, Dest: Pointer): Longint;

begin

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

end;

StdWndProc的地址减去Code[2]的地址与5的和,为什么Code[2]还要加上5,才能被StdWndProc的地址减呢,原因是Code[2]等到$E9,后面跟一个地址(可以是绝对地址也可以是相对地址,这里是使用相对地址)就形成了一条JMP指令,$E9占一个字节,地址是一个指针占了4个字节,所以这条指令占用了5个字节,所以Code[2]要和5加后,被StdWndProc的地址减去后才能得到一个正确的相对地址(其实也就是StdWndProc的地址到JMP指令的距离)。

接下来的Instances是一个数组,共有314个,数组的每一项是一个TobjectInstance记录:

PObjectInstance = ^TObjectInstance;

TObjectInstance = packed record

Code: Byte; //机器码

Offset: Integer; //偏移,操作数

case Integer of

0: (Next: PObjectInstance); //可能是指向下一个记录

1: (Method: TWndMethod); //也可能存放一个方法类型

end;

CodeOffset也组成了一条机器指令,请看第2526行,这条指令相当于:

CALLNEARPTROffset

Offset也是通过CalcJmpOffset计算得到的,它指定当前地址到BlockCode处的偏移,也就是调用ObjectInstance所在的InstanceBlockCode处的代码。另外,请注意这里是使用CALL而不是JMP,这是有特殊的含意的,你不妨可以思考一下,稍后我会作解释。

接下来是一个变体,有可能是Next指向下一个记录,也有可能是一个TwndMethod的变量。看MakeObjectInstance的代码,在初始化Block块的时候,是将数组中所有项都设成Next的。这样看来,当一个InstanceBlock新生成时,这个Instances数组也可以当成一个链表了,从第28行可以看出,有一个变量InstFreeList,就指向了这个表头。

但在34行下面的几句代码,返回了InstFreeList,并将这个记录指针的Next变成了Method,将传进来的参数Method赋给它,最后InistFreeList指向下一个ObjectInstance。这样看来,一个InstanceBlock是否已经用完取决于它里面的Instances数组,如果所有ObjectInstance的最后一个成员是Method,那么表示这个块已经用完了,相反如果是Next则表示还有ObjectInstance可用。

<p class
分享到:
评论

相关推荐

    易语言程序免安装版下载

     使用说明如下:函数声明和调用方法与DLL命令一致;“库文件名”以.lib或.obj为后缀的将被视为静态库,可使用绝对路径或相对路径(相对当前源代码所在目录),如依赖多个静态库请分别列出并以逗号分隔;“在库中的...

    新版Android开发教程.rar

    � 暂不具备 Push Mail 和 Office(DataViz 、 QuickOffice 计划近期推出 ) 功能,目前主要面向的是普通消费 者 用户,对商业用户支持尚弱。 Android Android Android Android 带来的影响 ANDROID 的推出后可能影响的...

    MFC的程序框架剖析

    类的集合,是一套面向对象的函数库,以类的方式提供给用户使用 2、MFC AppWizard是一个辅助我们生成源代码的向导工具,它可以帮助我们自动生成基于MFC框架的源代码 二、基于MFC的程序框架剖析 1、MFC程序的ClassView...

    PT80-NEAT开发指南v1.1

    NEAT 程序一般执行过程 ..................................................................................................................... 20 第四章 窗口 ................................................

    Powerbuilder9.0实用教程源代码

    2.13 在PowerBuilder 9.0中实现面向对象编程 58 2.13.1 创建对象 58 2.13.2 设置对象属性 59 2.13.3 编写事件代码 59 2.13.4 实现继承对象 60 2.14 用户对象和用户事件 61 2.14.1 用户对象分类 61 2.14.2 ...

    VC之美化界面篇本文专题讨论VC中的界面美化,适用于具有中等VC水平的读者。读者最好具有以下VC基础:

    ②在普通窗口中,直接创建一个CXPButton类对象,然后在OnCreate()中调用CXPButton的Create方法; 以下的章节将综合地使用以上的方法,请读者朋友留心观察。 3.2 使用MFC类的既有函数 在界面美化的专题中,MFC也...

    Python Cookbook

    第6章 面向对象编程 217 引言 217 6.1 温标的转换 223 6.2 定义常量 225 6.3 限制属性的设置 227 6.4 链式字典查询 229 6.5 继承的替代方案-自动托管 231 6.6 在代理中托管特殊方法 234 6.7 有命名子项的...

    XML轻松学习手册--XML肯定是未来的发展趋势,不论是网页设计师还是网络程序员,都应该及时学习和了解

    面向对象的思想方法已经非常流行了,在编程语言(例如java,js)中,都运用面向对象的编程思想。在XML中,就是要将网页也作为一个对象来操作和控制,我们可以建立自己的对象和模板。与对象进行交流,如何命令对象,...

    javaSE代码实例

    6.1.2 面向过程与面向对象思想的对比 78 6.1.3 面向对象技术的背景和特点 79 6.2 类的定义与对象的创建 80 6.3 成员变量 81 6.3.1 成员变量的开发与使用 81 6.3.2 成员变量的初始值 82 6.3.3 对象引用...

    C#微软培训资料

    第九章 面向对象的程序设计 .101 9.1 面向对象的基本概念.101 9.2 对象的模型技术 .103 9.3 面向对象的分析 .105 9.4 面向对象的设计 .107 9.5 小 结 .110 第十章 类 .112 10.1 类 的 声 明 .112 ...

    思库教育PHP零基础培训+进阶课程+PHP项目开发实战 21G PHP零基础学习视频教程.txt

    │ │ └[北京思库教育]第57集 面向对象编程(OOP).avi │ ├ │ │ ├[北京思库教育]第58集 构造方法.avi │ │ ├[北京思库教育]第59集 clone及静态方法.avi │ │ ├[北京思库教育]第60集 常量继承.avi │ │ └...

    RED HAT LINUX 6大全

    11.5.3 DNS将名字映射到IP地址及反 序操作 207 11.5.4 前区和反区必须保持同步 207 11.5.5 HUP信号和重启 207 11.5.6 IN-ADDR.ARPA域 207 11.5.7 主机命名方案 208 11.5.8 配置DNS客户:/etc/resolv.conf 208 ...

Global site tag (gtag.js) - Google Analytics