比较旧的东西,以前写的文章现在发出来,转载请注明~!
1 COM编程思想--面向组件编程思想
1.1 面向组件编程
众所周知,由C到C++,实现了由面向过程编程到面向对象编程的过渡。而COM的出现,又引出了面向组件的思想。其实,面向组件思想是面向对象思想的一种延伸和扩展。
下面,我就简单介绍一下面向组件的思想。在以前,应用程序总是被编写成一个单独的模块,就是说一个应用程序就是一个单独的二进制文件。后来在引入了面向组件的编程思想后,原本单个的应用程序文件被分隔成多个模块来分别编写,每个模块具有一定的独立性,也应具有一定的与本应用程序的无关性。一般来说,这种模块的划分是以功能作为标准的。这样做的好处有很多,比如当对软件进行升级的时候,只要对需要改动的模块进行升级,然后用重新生成的一个新模块来替换掉原来的旧模块(但必须保持接口不变),而其他的模块可以完全保持不变。
总结一下:面向组件编程思想,归结起来就是四个字:模块分隔。这里的“分隔”有两层含义,第一就是要“分”,也就是要将应用程序(尤其是大型软件)按功能划分成多个模块;第二就是要“隔”,也就是每一个模块要有相当程度的独立性,要尽量与其他模块“隔”开。这四个字是面向组件编程思想的精华所在,也是COM的精华所在!
1.2 COM的几个重要概念
1.2.1 组件
上面已经解释过组件,现在我只想强调一下组件需要满足的一些条件。首先是封装性,组件必须向外部隐藏其内部的实现细节,使从外部所能看到的只是接口。然后是组件必须能动态链接到一起,而不必像面向对象中的class一样必须重新编译。现在我只想强调一下组件需要满足的一些条件。首先是封装性,组件必须向外部隐藏其内部的实现细节,使从外部所能看到的只是接口。然后是组件必须能动态链接到一起,而不必像面向对象中的class一样必须重新编译。
1.2.2 接口
由于组件向外部隐藏了其内部的细节,因此客户要使用组件时就必须通过一定的机制,也就是说要通过一定的方法来实现客户与组件之间的通信,这就需要接口。所谓接口就是组件对外暴露的、向外部客户提供服务的“连接点”。 。外部的客户见不到组件内部的细节,它所能看到的只是接口,这有点像OSI网络协议分层模型,每一层就像一个组件,它内部的实现细节对于其他层是不可见的;而每一层通过“服务接入点”向其上层提供服务。
1.2.3 客户
这里所说的客户不是指使用软件的用户,而是指要使用某一个组件的程序或模块。也就是说,这里的客户是相对组件来说的。
2 COM原理
2.1 COM与虚函数列表
COM中的接口实际上是一个函数地址表,当组件实现了这个接口后,这个函数地址表中就填满了组件所实现的那些接口函数的地址。而客户也就是通过这个函数地址表获得组件中那些接口函数的指针,从而获得组件所提供的服务的。从某种意义上说,我们可以把接口理解为c++中的虚拟基类;或者说,在c++中可以用虚拟基类来实现接口!这是因为COM中规定的接口的存储结构,和c++中的虚拟基类在内存中的结构是一致的,我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:
class IObject
{
public:
virtual Function1(...) = 0;
virtual Function2(...) = 0;
....
};
class MyObject : public IObject
{
public:
virtual Function1(...){...}
virtual Function2(...){...}
....
};
IObject就是我们常说的接口,MyObject就是所谓的COM组件。记住接口都是纯虚类。COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。为了让大家确切了解一下虚函数表是什么样子,从《COM+技术内幕》中COPY了下面这个示例图:
2.2 COM基本接口类
2.2.1 IUnknown
COM规范规定任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是 QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。
引用计数。AddRef用于增加引用计数。Release用于减少引用计数。首先我们考虑com对象只实现一个接口的情况,不妨把接口成为IsomeInterface, 因为IsomeInterface继承与IUnknown,所以ISomeInterface接口成员函数中包含IUnknown的三个函数。假设有一个客户程序的许多个逻辑模块使用到了该com对象, 从而在客户程序很多地方保持了该接口指针的引用, 比如说有三个地方分别用了pSomeInterface1 pSomeInterface2, pSomeInterface3只想该接口指针。 在客户程序这三个逻辑块中, 它可以调用接口成员函数一伙的接口所提供的服务,如果他一直要该接口提供的服务,把他就需要控制该对象使它一直保持的内存中。如果用完了该对象,那他就应该通知接口不再需要服务。 由于每个逻辑模块并不知道其他的逻辑模块是否在继续使用COM对象, 他们只能知道自己是否还需要该对象。而对于COM对象来说, 只要有任意一个逻辑模块还需要使用它, 那么他就必须驻留在内存中不能释放自己。COM采用了引用计数来解决这个问题。 当客户得到一个指向该对象接口的指针时,计数加1,用完计数减1;当对接口指针复制或赋值,引用计数加1.如果一个COM对象实现了多个接口,则可以采用同蝼蚁计数计数。只要计数不为0 它就继续生存下去,反之,则表示客户不再使用该对象,它就可以被清除了。
接口查询。当客户创建COM对象之后, 创建函数总会为我们返回一个接口指针, 因为搜有的接口都继承了IUnknown,所以我们就可以通过QueryInterface一个接口指针。
接口原则性:
(1)对于同一个COM对象的不同接口,查询到的IUnknown接口必须完全相同,也就是说每个IUnknown接口指针是唯一的,因此对于两个接口指针我们可以通过判断其查询到的IUnknown指针是否相同来判断他们是否指向同一个对象。 反之如果查询的不是IUnknown接口,而是其他接口,则通过不同而途径得到的接口指针允许不一样。这就允许有的对象可以在必要的时候才动态生成接口指针, 不用的时候可以把接口指针释放掉。
(2)接口对成型。对每一个接口查询其自身总应该成功。
(3)自反省。如果从一个接口指针查询到另一个接口指针, 则从第二个接口指针再回到第一个接口指针也必定成功。
(4)接口的传递性
(5)接口查询时间无关性
我们来看看 QueryIInterface的实现方法, 我们考虑这种支持多接口对象的的实现方法。在c++中实现多接口COm对象有两种简单方法, 一种是使用多重继承,八所支持的接口类作为基类,然后在对象类中实现接口函数。另一种试试先内嵌接口类成员。 我们这里使用多重集成的办法实现多个接口支持。
我们用字典对象做列子,字典对象实现两个接口:IDictionary和ISpellCheck首先我们来看一看
接口的转换过程中存在虚列表的裁剪问题。
2.2.2 IClassFactory
IClassFactory的作用是创建COM组件。COM类用一个全局唯一的ID(GUID)来标识,称为CLSID,COM利用类厂(ClassFactory)来得到实例化的COM对象。系统用公共空间保存所有可被重用的COM类的CLSID和其具体位置的对应(在Windows下是保存在注册表中),这样所有的用户只要知道CLSID,都能顺利找到COM类。然后COM类可以利用类厂生成COM对象。COM类和类厂的实现代码可以在DLL中,也可以在EXE中,可以在本地,也可以在网络的另一端。COM对象要实现多个接口(Interface),每个接口都包含一组函数,也用一个GUID来标识,称为IID。QueryInterface()就可以根据IID来得到接口指针。IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了。
COM规定,每个COM对象类应该有一个相应的类厂对象,如果一个组件实现了多个COM对象类,则会有多个类厂。记下来我们看看类厂是如何避实用的。因为类厂本身也是一个COM对象,他被用于其他COM对象的创建过程。那么类厂对象又是谁创建的呢?答案是DllGetClassObject函数, 这个函数并不是COM库函数,而是一个组件程序的导出函数(类似DLL导出函数),我们来看下一下这个函数(ATL中自动生成)
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv)
{
return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}
在COM库中有三个函数用于COM接口的创建,他们分别是CoGetClassObject,CoCreateInstance,CoCreateInstanceEx。CoGetClassObject一般用来创建类厂接口。我们一般使用CoCreateInstance来创建COM接口指针。但是CoCreateInstance不能创建远程机器上对象如果要创建远程对象要使用CoCreateInstanceEx。
2.2.3 IDispatch
它的作用何在呢?除了C++还有很多别的语言,比如VB、 VJ、VBScript、JavaScript等等。可以这么说,如果没有这么多乱七八糟的语言,那就不会有IDispatch。COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如 VBScript、JavaScript。不行的原因在于它们并不支持指针。现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,只能另寻他法,IDispatch应运而生。 调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,仅给一个编号怎么调用一个函数,还要知道要调用的函数要带什么参数,参数类型什么以及返回什么东西,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看看MS的类库里实现 Invoke的代码就会惊叹它实现的复杂了,因为必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事。在ATL中
2.3 CLSID
CLSID其实就是一个号码,或者说是一个16字节的数。观察注册表,在HKEY_CLASSES_ROOT\CLSID\{......}主键下,LocalServer32(DLL组件使用InprocServer32) 中保存着程序路径名称。CLSID 的结构定义如下:
typedef struct _GUID {
DWORD Data1; // 随机数
WORD Data2; // 和时间相关
WORD Data3; // 和时间相关
BYTE Data4[8]; // 和网卡MAC相关
} GUID;
typedef GUID CLSID; // 组件ID
typedef GUID IID; // 接口ID
2.4 COM组件核心IDL
COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。
什么是IDL和MIDL?
IDL是接口定义语言。MIDL是Microsoft的IDL编译器。在用IDL对接口和组件进行了描述后,可以用MIDL进行编译,生成相应的代理和存根DLL的C代码。为得到一个代理/存根DLL,需要编译和链接MIDL生成的C文件。宏REGISTER_PROXY_DLL将完成代理/存根DLL在注册表中的注册操作。
客户与一个模仿组件的DLL进行通信,这个DLL可以完成参数的列集,此组件被称为代理。一个代理就是同另一个组件行为相同的组件组件还需要一个存根的DLL,以便对从客户传来的数据进行散集。存根也将对传回给客户的数据进行列集。
2.5 COM组件运行机制
构造一个创建COM组件的最小框架结构
IUnknown *pUnk=NULL;
IObject *pObject=NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);
pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
pUnk->Release();
pObject->Func();
pObject->Release();
CoUninitialize();
这就是一个典型的创建COM组件的框架,看看CoCreateInstance内部做了一些什么事情。以下是它内部实现的一个伪代码:
CoCreateInstance(....)
{
.......
IClassFactory *pClassFactory=NULL;
CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
pClassFactory->Release();
........
}
它的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。继续深入一步,看看CoGetClassObject的内部伪码:
CoGetClassObject(.....)
{
//通过查注册表CLSID_Object,得知组件DLL的位置、文件名
//装入DLL库
//使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。
//调用DllGetClassObject
}
DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.
下面是DllGetClassObject的伪码:
DllGetClassObject(...)
{
......
CFactory* pFactory= new CFactory; //类厂对象
pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
//查询IClassFactory指针
pFactory->Release();
......
}
CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码:
CFactory::CreateInstance(.....)
{
...........
CObject *pObject = new CObject; //组件对象
pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
pObject->Release();
...........
}
//见MyCom的例子
2.6 连接点
COM 中的典型方案是让客户端对象实例化服务器对象,然后调用这些对象。然而,没有一种特殊机制的话,这些服务器对象将很难转向并回调到客户端对象。COM 连接点便提供了这种特殊机制,实现了服务器和客户端之间的双向通信。使用连接点,服务器能够在服务器上发生某些事件时调用客户端。
有了连接点,服务器可通过定义一个接口来指定它能够引发的事件。服务器上引发事件时,要采取操作的客户端会向服务器进行自行注册。随后,客户端会提供服务器所定义接口的实现。客户端可通过一些标准机制向服务器进行自行注册。COM 为此提供了 IConnectionPointContainer 和 IConnectionPoint 接口。