2)Native Code(本地代码)到底做了什么? 打起精神,我们再深入一步。用OLEVIEW得到的类型库还不能正确的反映各对象方法对应的DLL中的函数入口,你应该已经发现用OLEVIEW得到的IDL文件中各个方法的entry属性值都是0x600000XX这样的假东西。要得到类型库中各方法在DLL中的真正入口,我们需要自己来写段程序。 即使在VB中我们也可以非常容易地获取类型库信息,再加上点COM初始化和调用代码,我们就能用自己的代码实现VB6才引入的CallByName函数(在本系列后面的《Hack COM》中我会更深入谈谈COM,作为一名VB程序员对COM的理解非常重要)。由于本文的关键不是指导如何在VB里使用类型库,所以下面提供的方法尽量从简。 新建一个标准EXE工程,添加对TypeLib Infomation的引用,在Form中放一个名为lblInfo的标签,然后添加如下代码: ''''程序1 Private Sub Form_Load() Dim oTLInfo As TypeLibInfo Dim oMemInfo As MemberInfo Dim sDllName As String Dim sOrdinal As Integer
Set oTLInfo = TLI.TypeLibInfoFromFile("MSVBVM60.DLL")
lblInfo = "MATH模块包含以下方法:" & vbCrLf
For Each oMemInfo In oTLInfo.TypeInfos.NamedItem("Math").Members With oMemInfo .GetDllEntry sDllName, vbNullString, sOrdinal
Next End Sub 运行以后我们就可以知道MATH模块中的Abs方法定义在VBA6.DLL中,其编号为656。在DEPEND中查看VBA6.DLL中编号为656的函数,果然就是rtcAbsVar,用VBE6.DLL试试结果相同。 还记得前面的注意一吧,VB6.EXE没有引入rtc开头的函数这说明在IDE环境中执行的VBA方法实际上是通过COM调用VBA对象库中的方法(跟踪p-code是噩梦,所以我无法验证它用的是什么绑定方式)。而注意二中提到的最终可执行程序中引入了rtcMsgBox,如我们所料最终的程序会直接调用它,这要比COM调用快一点,但在跟踪最终程序时,我发现rtcMsgBox内部却是经过了二万五千里长征后才会去调用MessageBoxA这个API,其间有多次对其它对象的COM调用,慢!可能是因为显示的是模态对话框,在多进程多线程环境有很多需要考虑的因素吧,如果你是疯狂在意效率的程序员,你应该试试用API来重写MsgBox,绝对快不少。再来看看注意三,让我们把以下的程序编译成使用本地代码的"程序2.EXE"(为了后面的实验,可以在工程属性的编译选项卡中将它设成"无优化"和"生成符号化调试信息"程序2.EXE""): ''''程序2 Private Declare Sub DebugBreak Lib "kernel32" () Private Sub Main() Dim i As Long, j As Long Dim k i = &H1234 DebugBreak k = 1234 j = Abs(k) j = Abs(i) MsgBox "ss" j = VarPtr(i) End Sub 用DEPEND观察"程序2.EXE",我们可以发现"程序2.EXE"并没有如我们预期的一样在引入595的rtcMsgBox的同时引入656的rtcAbsVar,相反它引入了__vbaVarAbs和__vbaI4Abs,看看函数名就知道一个针对的是Variant,一个针对的是long。这说明VB在最终生成的代码中对象Abs这样的可以进一步针对不同类型优化的VBA函数进行了相应的处理,观察一下所有以__vba开头的函数绝大部分都是那些最基本最常用的VBA函数,可以说__vba开头的VBA函数是rtc开头的VBA函数的优化版本,它们基本上是VB开发小组重新写的,绝大多数在函数内部实现自身功能,而rtc开头的函数大多数是调用COM服务对象来完成工作。从这么多__vba开头的函数上可以看出VB小组在Native Code(本地代码)的优化上下了不少功夫,这决对不是吹牛。它的确高度优化了不少科学计算相关的函数,以ABS为例Native Code要比p-code快4倍以上。但是并不是所有的计算函数都经过了这样的优化,比如Rnd函数,它就没有对应的__vba开头的优化函数,而是直接对应到rtcRandomNext函数上,虽然rtcRandomNext也已经优化过,但内部依然用了COM调用,还是不如自己重写的快,我不明白为什么VB开发小组没有考虑为它写一个对应的__vbaRnd。 不要以为上面的分析没有意义,因为我们可以从现象看本质,也可以从本质来解释现象。比如我们再做一个实验,给你的代码加入一个类模块,你可以试试声明一个和内部方法同名的公有的方法(这是一个很有用的技术,在本系列后面的《错误处理》中我们会用到这种方法),比如我们可以声明一个Public Function Rnd(x) as single,同样我们可以自己写一个同名的MsgBox。但是你试试能不能声明一个Public Function abs(x) ,这时VB肯定会弹出一个莫名其妙的编译错误提示框告诉你"缺少标识符",这种错误发生在你的函数名和VB关键字冲突的时候。但是为什么同样是MATH模块中的函数,abs是关键字,rnd却不是,VB文档里是不会告诉你为什么的,但如果你认真的看了我上面的实验分析,我们就能猜想这是因为VB对需要进一步优化的函数已经做了高度优化处理,VB开发小组为了保护他们的劳动成果,并显示他们对自己优化技术的自信,而禁止我们重写这些函数,同时VB开发小组也承认还有些函数有待进一步优化,所以准许我们重写之。在这里我要提出一个伟大的猜想:凡是能够被重写的函数就能够被优化,就象凡是大于2的偶数就能够被分解成两个质因数的和一样。 说到优化,还应该谈谈直接API调用和使用API类型库的差别,还必须谈谈VB所使用的后端优化器(和VC用的是一样的优化器),还想谈谈如何尽最大可能来使用vTable绑定……(准备在本系列中另写一篇《优化》来谈这些问题)。 看了本地代码,我们再来看看p-code,要是你看了MSDN中关于p-code的原理,你肯定会头大。平心而论p-code真是一个了不起的技术,代码大小平均可以缩小50%。我们把程序2编译成p-code看看,还是用DEPEND来观察,发现它并没有引入__vba开头函数(没有使用优化的VBA函数?),却引入了CallEngine这样的东西(肯定是为了调用p-code伪码解释引擎),而且和Native Code一样都引入了rtcMsgBox(编译生成的p-code在调用MsgBox时应该比在IDE环境中运行的p-code快)。 如果你迫不及待地运行了程序2,你就会发现它将弹出一个应用程序错误对话框,说程序发生异常。别怕,这是因为调用了DebugBreak这个API的缘故,这个API其实就是产生一个Int 3中断,使得我们能够中断程序执行。如果你装了VC这样的支持即时调试的调试器,你可以在错误对话框中点击"取消",这样可以起动调试器来调试程序。我就是这样跟踪程序运行的。如果你想看看VB生成的程序反汇编代码可以自己试试,我们可以用同样的技术在VB或VBA的IDE中来中断程序执行,比如我们完全可以在Word的VB编辑器中运行上面程序2的代码,从而中断于Word的进程中,并可观察到VBA生成的p-code代码。比如VB和VBA在IDE中