matthew curland简介:
visual studio开发小组成员,参与开发了vb的intellisense和object browser。他是vb资深专家,对vb有非常深入的研究,堪称vb大师。所著《advanced visual basice》是阐述vb高级编程技巧的一本好书。
本文英文原著可见2000年2月份《visual basic programmer's journal》(vb程序员月刊)里的《call function pointers》,这是他发表的妙文之一,他的书里的第11章和本文同名,本文应该是这一章节的精华。
之所以推荐此文,是因为它综合运用了vb里的不少技术。我们可从中看到matt大师对vb的深刻理解,而各位技术的综合运用正体现了他深厚的功力。
本文原文:http://www.devx.com/premier/mgznarch/vbpj/2000/02feb00/mc0200/mc0200.asp
(要先注册成premier用户)
本文配套代码:
http://www.devx.com/free/mgznarch/vbpj/code/2000/02feb00/vb0002mc_p.zip
关键字:函数指针,com、对象、接口,vtalbe,vb汇编,动态dll调用。
级别:高级
要求:了解vb对象编程,了解汇编。
调用函数指针
通过使用函数指针,我们能够动态地在代码中插入不同行为的函数,从而使代码拥有动态改变自身行为的能力。
作者:matther curland
要求:使用本文的示例代码,你需要vb5或vb6的专业版或企业版。
从visual basic 5.0开始basic语言引入了一个重要的特性:addressof运算符。这个运算符能够让vb程序员直接体会到将自己的函数指针送出去的快感。比如我们在vb里就能够得到系统字体的列表,我们能够通过标准的api调用来进行子类化。一句话,我们终于可以象文档里所说的那样来使用win32 api了。
不过,这个新玩具只能给我们带来短暂的快感,因为这个礼物并不完整。我们可以送出函数指针,但却没人能将函数指针送给我们。事实上,我们甚至不能给我们自己送函数指针,这使我们不能够体验送礼的真正乐趣(译者:呵呵,光送礼却不能收礼的确没趣)。addressof让我们看到了广袤天地的一角,但是vb却不让我们全面地探索它,因为vb根本就不让我们调用函数指针,我们只能提供函数指针(译者:可以先将函数指针送给api,然后让api回调自已的函数指针来完成函数指针调用的功能,但这还是要先把礼物送给别人)。其实,我们能够自己来实现调用函数指针的功能,我们可以手工将一个对com接口的vtable绑定调用变成一个函数指针调用。最妙的是:我们能够在纯vb里写出调用函数指针的代码,不需要任何辅助的dll。
告诉编译器函数指针是什么样子,是使vb能够调用任何函数的关键。将参数类型和返回值类型交给vb编译器,让编译器将我们的函数调用编译到我们的程序里,这样程序才能在运行时知道怎样去定位函数。在程序被编译后,一个函数就是内存里一串汇编字节流,通过cpu解释执行而形成我们的程序。调用一个函数指针,首先需要程序获得指向这个函数字节流的指针,再通过x86汇编指令call将当前指令指针(译注:即x86汇编里的ip寄存器)转到函数所在的字节流上。在函数完成后,再用ret指令返回给调用此函数的程序来继续操作。
我下面将要提到的方法,利用了vb自己的函数调用方式,所以我先来解释一下vb是怎样来实现函数调用的。vb内部使用三种函数指针,但是,在本质上,不论vb是如何来定位这几类函数指针,调用它们的方法却是一样的。vb编译器必须知道准确的函数原型才能生成调用函数的代码。
第一类,最常见的函数指针类型,就是vb用来调用函数的普通指针,这样的函数定义在标准模块内(或类模块里的友元函数和私有函数)。调用友元函数和私有函数时,调用指令定位在当前指令指针的一个偏移地址处,或者先跳到一个记录着函数位置的查找表里,再跳到函数内(译者:即先"call 绝对地址"跳到一个跳转表内,表里的每个入口都是一个"jmp"到函数)。这些函数都在同一个工程内,联结器总是将所有的模块联结在一起,所以总是知道在内存何处能够找到vb内部函数,因此转移控制到内部函数时,其运行时开销是很少的。
vb对某些函数指针的调用却困难得多
对于另两类函数指针,vb必须在运行时进行额外的工作才能够找出它们。
第二类,vb调用一个com对象接口里的方法。我们可能认为建立com对象的工作是相当复杂的,如果完全用vb来为我们建造com的所有组成部分的话,但事实上并不是这样。按照com的二进制标准,一个com对象是一个指针,这个指针指向一个结构,这个特定结构的第一个元素是一个指向函数指针数组的指针。这个函数指针数组(又叫虚拟函数表,简称vtable)里的前三个指针,一定是标准queryinterface,addref,release函数。vtable里接下来的函数符合给定的com对象接口定义里的函数定义(见图一)
图一:
函数指针代理是怎么工作的?click here
当vb通过一个对象类型的变量来调用一个com对象的方法或属性时,这个变量里存放着对这个com对象接口的引用。vb要定位函数时,首先要通过com引用的第一个元素来获得指向vtalbe的指针,然后才能在vtable里定位函数指针。对一个vtable调用来说,编译器提供了com引用和函数指针在vtable里的偏移量。这样函数指针才能在运行时被动态地选出来。这种双向间接的方式——两种指针都必须被计算(译注:指向vtalbe的指针和vtable里的函数指针都必须在运行时才确定)——使得vtable调用比同一个工程内的直接调用慢得多,因为直接调用不需要任何在运行时才能进行的指针间接指定。
vb对待同一个工程里的类的公有方法和对待外部com对象里方法完全一样,都需要查找vtable,这就是为什么在同一个对象内调用一个友元函数会比调用一个公有函数快得多的原因。但是,查找vtable是com的基础,它使得vb能够使用从外部库里载入的com对象,也是象implements这样的编程概念的实现基础。动态载入不可能通过静态联结来实现,查找vtable的花费是使用动态载入必须付出的代价。
通过object型变量来进行的后期绑定调用不同于vtable绑定调用。当然,这种差别不在于vb用没用vtable,这种差别是因为对后期绑定调用vb使用了不同的vtable。当进行后期绑定调用时,编译器会调用idispatch接口的getidsofnemes和invoke。这需要两次vtable调用和相当多的参数传递,所以这样的处理非常慢,而且必须不断地定位invoke,才能通过类型信息调用到真正的函数指针(译者:真正慢的原因还是invoke所进行的参数调整。当拥有相应对象的接口类型库信息时,vb会进行另一种后期绑定——dispid绑定,它只需要在第一次访问对象时调用getidsofnemes,来获得所有属性和方法的dispid,以后的调用只需要对invoke进行一次vtalbe调用,但由于invoke才是慢的原因,所以dispid绑定比一般后期绑定快不了多少)。毋庸置疑,当在同一个线程里调用com对象时,后期绑定将比vtalbe绑定慢几个数量级(译者:同线程内要慢数百倍。由于跨边界的调配开销,随跨线程、跨进程、跨机器,两种绑定方式在速度上的差别将越来越小)
第三类,通过declare语句来使用函数指针。declare使得vb能够动通过loadlibraray api来动态载入特定的dll,并通过getprocaddress api和函数名(或函数别名)来得到dll里特定的函数指针。声明在类型库里的函数指针是在程序装入时通过import table(输入表)来载入的,而通过declare语句声明的函数指针是在此函数第一次被调用时装入(译者:这两种方式各有优缺点。使用declare在调用时载入,一来vb运行时直接支持,使用简单,二来当需要载入的dll不存在时可以在运行时通过错误捕获来处理。而使用类型库一次性载入,一是会增加载入时间,二是当相应的dll找不到时程序根本就无法起动,但是通过类型库调用api可以绕过vb运行时动态的dll载入过程,这在某些时候很有必要)。
动态指定函数指针