源自: http://www.4oa.com/Article/html/6/32/468/2005/16547.html
接下来, 我们来看看COM如何编程.
1. 我见过很多人学COM,看完一本书后觉得对COM的原理比较了解了,COM也不过如此,可是就是不知道该怎么编程序,
我自己也有这种情况,我也是经历了这样的阶段走过来的。
2. COM基本原理:"COM技术内幕";
用VC编程必须掌握的几个关键概念:
2.1 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组件就是从这些纯虚类继承下来的派生类,它实现了这些虚函数,仅此
//而已。从上面也可以看出,COM组件是以 C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数
//都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。
2.2 COM组件有三个最基本的接口类, 分别是: IUnknown, IClassFactory, IDispatch.
2.2.1 IUnknow
OM规范规定任何组件、任何接口都必须从"IUnknown继承",IUnknown包含三个函数,分别是 QueryInterface、
AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。QueryInterface用于
查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计
数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么
理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给你一
个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,
那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM
组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调
用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的
使用已经结束了,那它就把它自己给释放了。引用计数是COM编程里非常容易出错的一个地方,但所幸VC的
各种各样的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过
AddRef,我们只需在适当的时侯"调用Release"。至少有两个时侯要记住调用Release,第一个是调用了
QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函
数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。 IUnknown的这三个函数的实现非
常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。
2.2.2 IClassFactory -- 创建COM组件
我们已经知道COM组件实际上就是一个类,那我们平常是怎么"实例化一个类对象"的?是用‘new’命令!很简单吧,
COM组件也一样如此。但是谁来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果
客户知道组件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表着"组
"件的128位的数字串"而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一下,如果组件是在远
程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是"类厂"。
每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时
,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及
远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复
杂的操作都交给类厂对象去做了。IClassFactory最重要的一个函数就是"CreateInstance",顾名思议就是创
建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由
我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则
是太少太少了。
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的代码就会惊叹它实现的复杂了,因为你必须考虑
各种参数类型的情况,所幸我们不需要自己来做这件事,而且可能永远也没这样的机会。:-)
2.3 dispinterface 接口, Dual接口以及Custom接口
ATL编程用到的术语, 自动化接口的好处及缺点.
所谓的自动化接口就是用"IDispatch实现的接口"。我们已经讲解过IDispatch的作用了,它的好处就是脚本语言象
VBscript、 Javascript也能用COM组件了,从而基本上做到了与语言无关它的缺点主要有两个,"第一个"就是速度
慢效率低。这是显而易见的,通过虚函数表一下子就可以调用函数了,而通过Invoke则等于中间转了道手续,尤
其是需要把函数参数转换成一种规范的格式才去调用函数,耽误了很多时间。所以一般若非是迫不得已我们都想
用VTable的方式调用函数以获得高效率。"第二个缺点"就是只能使用规定好的所谓的自动化数据类型。如果不用
IDispatch我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度代码。而用自动化接口就
不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预料到我们要用到的所有类型,它只能根据一些
常用的数据类型来写它的处理代码,而且它也要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成
的调度代码只适用于它所规定好的那些数据类型,当然这些数据类型已经足够丰富了,但不能满足自定义数据结
构的要求。你也可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。考虑到IDispatch
的种种缺点(它还有一个缺点,就是使用麻烦,:-) )现在一般都推荐写双接口组件,称为dual接口,实际上就是
从IDispatch继承的接口。我们知道任何接口都必须从 IUnknown继承,IDispatch接口也不例外。那从IDispatch
继承的接口实际上就等于有两个基类,一个是IUnknown,一个是IDispatch,所以它可以以两种方式来调用组件,
可以通过 IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用。这就有
了很大的灵活性,这个组件既可以用于C++的环境也可以用于脚本语言中,同时满足了各方面的需要。
--相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口 (虽然它实际上不是的),
--这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。
--Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了
2.4 COM组件有三种, 进程内, 本地, 远程. 对于后两者情况必须调度"接口指针"及"函数参数".
COM是一个DLL,它有三种运行模式。它可以是进程内的,即和调用者在同一个进程内,也可以和调用者在同
一个机器上但在不同的进程内,还可以根本就和调用者在两台机器上。这里有一个根本点需要牢记,就是COM组件它
只是一个"DLL",它自己是运行不起来的,必须有一个进程象父亲般照顾它才行,即COM组件必须在一个进程内.那
谁充当看护人的责任呢?先说说调度的问题。调度是个复杂的问题,以我的知识还讲不清楚这个问题,我只是
一般性的谈谈几个最基本的概念。我们知道对于WIN32程序,"每个进程都拥有4GB的虚拟地址空间",每个进程都
有其各自的编址,"同一个数据块"在不同的进程里的编址很可能就是不一样的,所以存在着进程间的地址转换问
题。这就是调度问题。"对于本地和远程进程来说,DLL 和客户程序在不同的编址空间",所以要传递接口指针到
客户程序必须要经过调度。Windows 已经提供了现成的调度函数,就不需要我们自己来做这个复杂的事情了。
对远程组件来说函数的参数传递是另外一种调度。DCOM是以"RPC"为基础的,要在网络间传递数据必须遵守标准的
网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调度,这个过程很复杂,不
过Windows已经把一切都给我们做好了,一般情况下我们不需要自己来编写调度DLL。
我们刚说过一个COM组件必须在一个进程内。对于本地模式的组件一般是以EXE的形式出现,所以它本身就已经
是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了调度代码以实现基本的调度。这个进程就
是dllhost.exe。这是COM默认的DLL代理。实际上在分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着
很强大的功能,是专门的用于管理分布式DLL组件的工具。
调度离我们很近又似乎很远,我们编程时很少关注到它,这也是COM的一个优点之一,既平台无关性,无论你
是远程的、本地的还是进程内的,编程是一样的,一切细节都由COM自己处理好了,所以我们也不用深究这个
问题,只要有个概念就可以了,当然如果你对调度有自己特殊的要求就需要深入了解调度的整个过程了,这
里推荐一本《COM+技术内幕》,这绝对是一本讲调度的好书。
2.5 COM组件的核心是IDL
我们希望软件是一块块拼装出来的,但不可能是没有规定的胡乱拼接,总是要遵守一定的标准,各个模块之
间如何才能亲密无间的合作,必须要事先共同制订好它们之间交互的规范,这个规范就是接口。我们知道接
口实际上都是纯虚类,它里面定义好了很多的纯虚函数,等着某个组件去实现它,这个接口就是两个完全不
相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,
我们没有时间自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们需要
的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂商已经在自己的组件中
实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组件,我们不关心组件从哪来,它有什么其
它的功能,我们只关心它是否很好的实现了我们制订好的接口。这种接口可能是业界的标准,也可能只是你
和几个厂商之间内部制订的协议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,
是COM组件通信的标准。
COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直
是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关
的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正
是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来
定义接口,不论放到哪个语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL
开始,先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,有的人可能
善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你把最终的DLL 拿给我。这是一种多么好的
开发模式,可以用任何语言来开发,也可以用任何语言来欣赏你的开发成果。
2.6 COM组件的运行机制, 即COM是怎么跑起来的
这一部分, 我们将构造一个创建COM组件的最小框架结构, 然后看一看其内部处理流程是怎样的.
//1
IUnknow *pUnk = NULL;
IObject *pObject = NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknow, (void**)&pUnk);
//2
pUnk->QueryInterface(IID_IObject, (void**)&pObject);
pUnk->Release();
pObject->Func();
pObject->Release();
CoUninitialize();
这是一个典型的创建COM组件的框架, 不过我的兴趣在CoCreateInstance身上, 让我们来看看它
内部做了些什么事情. 以下是它内部实现的一个伪代码:
//1.1
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的内部伪码:
//1.2
CoGetClassObject(.....)
{
//通过查注册表CLSID_Object,得知组件DLL的位置、文件名
//装入DLL库
//使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。
//调用DllGetClassObject
}
DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.
//1.3
下面是DllGetClassObject的伪码:
DllGetClassObject(...)
{
......
CFactory* pFactory= new CFactory; //类厂对象
pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
//查询IClassFactory指针
pFactory->Release();
......
}
//1.4
CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码:
CFactory::CreateInstance(.....)
{
...........
CObject *pObject = new CObject; //组件对象
pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
pObject->Release();
...........
}
2.7 一个典型的自注册的COM DLL所必有的四个函数
DllGetClassObject : 用于获得类长指针
DllRegisterServer : 注册一些必要的信息到注册表中
DllUnregisterServer: 卸载注册信息
DllCanUnloadNow : 系统空闲时会调用这个函数, 以确定是否可以卸载DLL
DLL还有一个函数是DllMain, 这个函数在COM中并不要求一定要实现它, 但是在VC生成的组件中自动
都包含了它,它的作用主要是得到一个全局的实例对象.
2.8 注册表在COM中的重要作用
首先要知道GUID的概念,COM中所有的类、接口、类型库都用"GUID"来唯一标识,GUID是一个128位的字串,
根据特制算法生成的GUID可以保证是全世界唯一的。 COM组件的创建,查询接口都是通过注册表进行的。
有了注册表,应用程序就不需要知道组件的DLL文件名、位置,只需要根据"CLSID"查就可以了。当版本升
级的时侯,只要改一下注册表信息就可以神不知鬼不觉的转到新版本的DLL。
转载于:https://blog.51cto.com/tuoxie174/416619