用汇编语言访问com对象
大量的细节使得Com看上去很复杂,但是使用起来却很简单。最难的部分就是理解里面的数据结构,尽管COM是语言无关的,但是他借用了很多c++的术语来描述自己。
为了能使用某个对象的com接口函数,你必须首先要从类厂中创建这个对象,并且让他来返回接口指针。这个过程被CoCreateInstance这个 API函数完成。当你使用完接口时,要调用Release方法。一个COM对象可以看作是一个服务,调用com的应用程序就是他的客户端。
在调用com接口函数之前,你需要了解接口是什么,一个com接口就是一个函数指针表,我们还是从IUnknown接口开始,如果你创建了一个组件导出了 IUnknown接口,那么你就有了一个全功能的com对象。IUnknown有三个基本的几口方法,既然所有的接口都是从它派生出来,那么我们一定要记住,一个接口实际上就是一个函数指针成员组成的结构体。
例如:
IUnknown STRUCT DWORD
; IUnknown methods
IUnknown_QueryInterface QueryInterface_Pointer ?
IUnknown_AddRef AddRef_Pointer ?
IUnknown_Release Release_Pointer ?
IUnknown ENDS
它只有12个字节长,它具有3个DWORD指针来指向实际的实现函数。对于虚函数表,你一定听说过,这些指针定义如下,因此我们可以让masm在编译我们的调用时进行一些类型检查。
QueryInterface_Pointer typedef ptr QueryInterface_Proto
AddRef_Pointer typedef ptr AddRef_Proto
Release_Pointer typedef ptr Release_Proto
最后我们定义我们的函数如下:
QueryInterface_Proto typedef PROTO :DWORD, :DWORD, :DWORD
AddRef_Pointer typedef PROTO :DWORD
Release_Pointer typedef PROTO :DWORD
为了保持masm32松散的类型检查一致,函数参数都定义为dword
定义接口是一个相当大的编辑就是,masm不支持前向引用。因此,我们不得不颠倒一下定义的顺序。先定义函数头,再定义函数指针,最后定义接口。实际上在使用接口时,你需要一个指向它的指针。
CoCreateInstance函数能用来直接返回一个接口指针。它实际上指向了拥有接口的对象。这个结构看上去如图所示:
这个结构里有大量的间接访问,使用宏可以简化它。
当客户端调用COM库创建com组件时,它传进了一个地址用于存放对象指针。这个就是我们所说的ppv. 从c++的角度来讲,叫做指向指针的指针,void类型代表无类型。它保存了另一个指针pv的地址。pv指向了虚函数表。
例如:我们使用CoCreateInstance函数成功的返回了一个接口指针ppv,我们想看下它是否支持其他的接口,我们可以调用QueryInterface方法。用c++的方法描述QueryInterface如下:
(HRESULT) SomeObject::QueryInterface (this:pObject, IID:pGUID, ppv2:pInterface)
用汇编写法如下:
01 ; get pointer to the object
02 mov eax, ppv
03 ; and use it to find the interface structure
04 mov edx, [eax]
05
06 ; push the function parameters onto the stack
07 push OFFSET ppv2
08 push OFFSET IID_ISomeOtherInterface
09 push dword ppv
10
11 ; and then call that method
12 call dword ptr [edx + 0]
13
14 ;使用invoke调用简化如下:
15
16 ; get pointer to the object
17 mov eax, ppv
18 ; and use it to find the interface structure
19 mov edx, [eax]
20 ; and then call that method
21 invoke (IUnknown PTR [edx]).IUnknown_QueryInterface, ppv,
22 ADDR IID_SomeOtherInterface, ADDR ppv_new
23
注意IUnknown PTR [edx]这个类型转换,是让编译器知道使用哪个结构来得到QueryInterface函数在虚表中的正确偏移。其中有一个模糊的地方,注意我修改了函数名字为"IUnknown_QueryInterface",这个名字修饰时必要的。当你有一个大的com工程,有许多相似的接口,你就会遇到麻烦。不同的接口对应不同的方法表示,是非常有效的。
coinvoke 宏,这个宏定义在oaidl.inc文件中。使用它,可以进一步简化com调用。
01 ;———————————————————————
02 ; coinvoke MACRO
03 ;
04 ;
05 ; pInterface pointer to a specific interface instance
06 ; Interface the Interface’s struct typedef
07 ; Function which function or method of the interface to perform
08 ; args all required arguments
09 ; (type, kind and count determined by the function)
10 ;
11 coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
12 LOCAL istatement, arg
13 FOR arg, <args> ;; run thru args to see if edx is lurking in there
14 IFIDNI <&arg>, <edx>
15 .ERR <edx is not allowed as a coinvoke parameter>
16 ENDIF
17 ENDM
18 istatement CATSTR <invoke (Interface PTR[edx]).&Interface>,<_>,<&Function, pInterface>
19 IFNB <args> ;; add the list of parameter arguments if any
20 istatement CATSTR istatement, <, >, <&args>
21 ENDIF
22 mov edx, pInterface
23 mov edx, [edx]
24 istatement
25 ENDM
26 ;———————————————————————
27
28
29 ;因此,前面的QueryInterface方法调用就可以简化成:
30
31 coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface,
32 ADDR ppnew
注意这里名字修饰是隐藏在宏中处理的。