;重新将ecx寄存器里保存的返回地址压栈 ;以使得函数指针调用后知道返回到哪儿 push ecx
;将控制转移到函数指针,
;它在this指针后偏移4个字节处。 jmp DWORD PTR [eax + 4]
这四条指令的连在一起需要6个字节:59 58 51 FF 60 04。我们在后面补两个Int3指令(CC CC)以凑足8个字节,这正好可以一个VB的Currency变量内。这样一个Currency变量的地址里会放着如下的magic number(幻数)——368956918007638.6215@ ——这个Currency变量是指向代理函数的函数指针。这个代理函数挤掉this指针,并可跳到任何函数,而不用考虑函数的参数。这就是说,我们可以用同样的汇编代码来代理任何函数指针。我们现在需要一个vTable来包含这个指向字节流的指针,它实际上是一个函数。(译者:即用vTable的某个入口包含代理函数指针)。
使用代理函数需要用到一个结构,它偏移4字节处是我们要调用的函数指针。我们还需要它偏移0个字节处是一个指向vTable的指针,这样才能让这个结构和一个COM对象一样,只有这样VB才能调用到vTable里的函数。我们并没必要为了一个简单的函数指针调用而在堆里分配内存;相反,我们仅需在调用代码的某个地方声明一个FunctionDelegator结构的变量。虽然我们提供了AddRef和Release函数,但它们不做任何事,只不过是迁就一下VB(译者:VB她对我们的对象引用进行严格的跟踪。每当我们新增一个对我们对象的引用,她就会调用一次我们对象里的AddRef,以准确计录对象被引用的次数;每当我们的一个引用和对象分手,她又会调用Release来通知我们的对象减少引用计数。VB她这样做是为了当我们所有的引用都和对象分手后,对象能够在内存里被干净地抛弃。为了迁就VB她的这个习惯,哪怕我们手工建立的对象并不动态分配内存,我们的对象也必须提供AddRef和Release)。所以第四个vTable入口是一个指向代理函数汇编代码的指针。函数代理的代码里声明了一个UDT来包含一个vTalbe数组指针。(代码见Listing1)
将结构转换成COM对象
当我们将一个指向合法vTable的指针传给FunctionDelegator结构,并将这个结构拷贝到一个对象变量里,这个结构就成为合法的COM对象了。这个对象的QueryInterface(译者:以下简称QI)函数相信我们所要求的接口vTalbe的第四个入口的函数原型总是和函数指针相符的。如果不支持所要求的接口,QI函数通常返回E_NOINTERFACE错误。这个错误状态在VB里表现出来就是在停在Set语句上的类型不符错误。FunctionDelegator对象的这种信任的设计要求我们必须自己来保证类型安全,我们永远不要向这个对象请求一个不符合函数指针原型的接口。如果我们破坏了这个规则,对我们的惩罚就将是崩溃而不是类型不匹配错误了(译者:要体会这种惩罚,可以试着将Listing1代码里的InitDelegator返回的接口用VB里的任意接口来引用,比如用Shape,由于其第四个接口定义不符,崩溃)。
FunctionDelegator的vTalbe不进行任何引用计数,所以我们不用编写任何tear-down(严重错误处理)或内存释放代码。当栈越出它的scope时(译者:此处的scope是指FunctionDelegator对象变量的变量范围,即声明和使用它的过程级或模块级范围),COM对象所使用的内存会自动从栈里清除,这意味着InitDelegator所返回的COM对象必然在结构
49
自己销毁之前(或同时)被销毁。
在VB能够调用到代理函数之前,还有一个步骤:我们必须为我们想要调用的函数指针定义一个接口。通过使用mktylib工具来生成对象定义语言(ODL)文件,我们能够非常容易地做到这一点。尽管mktylib.exe是midl.exe的一个官方的功能简化版本,但当我们要生成给VB使用的严格的类型库时,mktylib.exe相对更容易使用。而且,不同于midl.exe,mktylib.exe它是和单独的VB产品一起销售的。我们的接口定义必须继承自IUnknown并且有一个附加的函数。当我们仅仅使用ODL待性而不使用oleautomation特性时,我们能够避免OLE自动化在注册表里的HKCR\\Interface主键下写入不必要的注册键值。虽然我们的QI函数忽略uuid,但是它还是需要我们建立类型库。(译者:虽然可以通过ActiveX工程来生成包含类型库的组件,这样可以不用外部工具就能生成类型库,但是VB里所有的组件都是支持OLE自动化的,它们必须在注册表里注册键值。更重要的是,VB所生成的接口都继承自IDispatch,其vTable并不符合本文的要求。如果不想使用对象定义语言,而想用更纯的VB地来做,就必须修改代理函数的实现,因为继承至IDispatch后,我们只能在vTable的第八个入口里放代理函数指针。虽然这种做法可行,但是实现起来很复杂,因为需要手工建立能迁就VB的IDispatch,而这决不象本文手工建立 IUnknown接口这么简单。虽然可能,但这个弯子绕得太大了)
作为例子,这里定义了三种函数。第一种是在排序算法中回调的标准的比较函数原型。第二种函数指针调用能够返回COM HRESULT错误代码,比如DllRegisterServer。第三种是一个即没有参数也没有返回值的函数。我们可以按照自己的需要来加入函数声明。保存经过我们修改的FuncDecl.odl文件,并且执行mktylib FuncDecl.odl,然后再将FuncDecl.tlb的引用加入我们的工程。(见Listing2里的ODL)
我们能够看到,通过调用下面的一对函数,我们的确是可以实时调用函数指针了,而很长时间以来,对VB程序员来说,想使用这对函数是不可能的,这对函数就是DllRegisterServer和DllUnregisterServer。通过访问这两个标准的ActiveX DLL和OCX入口函数,可以让我们的EXE按照自已的需要来定位和注册自己的组件(译者:这个技术还是有相当价值的。虽然能够通过Shell语句调用RegSvr32.exe来注册组件,但是它仅支持标准的入口:DllRegisterServer和DllUnregisterServer。而使用这里的技术,我们就能够调用非标准的入口,在ATL工程里将两个两个输出函数换个名字,我们在VB里依然可以注册,这样简单的操作就能起到一定的保护组件的作用)。对这样的外部函数来说,我们是通过LoadLibrary和GetProcAddress调用来从外部DLL获取函数指针,并将这个函数指针移到FunctionDelegator结构里以使我们能够调用这个函数指针本身。(见Listing3)
使用函数指针来排序 (译者:这里原文用了几段来演示如何通过函数指针回调的方法来进行数组排序。仅就本文要谈的函数指针调用来说,这和Listing3里的处理方式类似,因为此处省略这几段。)
我们能够在很多方面使用这种调用函数指针技术。比如,我们可以通过在运行时插入具有不同行为的函数来动态改变某段代码的行为。我们也可以通过这种技术在VB里实现type casting(强制类型转换)(译者:通过VarPtr得到一个变量的无类型指针,然后将这个指针做为参数,将这个指针传给不同的类型转换函数指针,并调用之,即可实现强制类型转换)。我不可能把所有可能的应用都列出来,但是这里我再来演示一段小程序。
50