COM实现过程1

原创:吴剑明(foxnt)

前言
COM已经成为一个必需的东西了。在我们周围,可以说处处充满了COM – 如果你是在使用WINDOWS,并在其下面编写程序的话。然而,无论你是用VC,还是使用DELPHI进行COM编程时,在大多数情况下,编程工具的 IDE已经向你隐藏了COM的大部分实现过程,使得程序员根本不需要了解COM,只专心致志地写其所关心的逻辑代码。这就意味着,我们很少有机会,能够揭开COM的神秘面纱,来看到它下面到底是什么东西。这对于一个WINDOWS程序员来说,不能不是个遗憾。
因此,本文的宗旨,就是抛开现有的IDE提供的各种向导工具,引导大家从最基本的地方入手,完整地从一个空白的程序里,建立起一个COM程序,从而达到能够比较清晰地了解一个COM,到底是如何生成出来并实现在程序中。
本文假设,您是一个有COM编程经验的DELPHI/VC程序员,并希望了解COM的基本实现过程。限于篇幅和时间,我们只讨论进程内的COM(DLL)的实现,并引导大家亲手建立起一个最简单的COM程序。

COM是什么?
COM有各种表现形式,可以是进程内,也可以是进程外;可以在本机调用,也可以远程调用。记得国外有个研究COM的组织,他的主题就叫作:COM就是爱! 这当然是外国人的幽默,他只是想说明,COM是个多么重要的东西。那么COM到底是个什么东西呢?
很早以前,在我刚开始学习COM的时候,身边就有些程序员告诉我:COM不是DLL,虽然它通常也是以DLL来作为扩展名的,可他完全与DLL完全不同。那么,这种说法是否正确呢?我们来看看,要实现一个进程内的COM,到底需要经过哪些步骤,那么,我们就能很清楚的知道答案了。
完成一个进程内的COM,通常需要以下几步:
1. 建立一个DLL项目,并导出以下四个函数:
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;

2. 定义自定义的接口,同时必须实现Iunknown接口。
3. 建立GUID,以标识这个组件以及自定义的接口。
4. 在注册表中注册以标记这个DLL。
大家都看到了,在第一个步骤里,需要建立一个DLL项目,那么,是不是意味着,COM就是一个DLL呢?在这里,我可以明确地告诉大家,从技术上讲,一个进程内的COM完全可以被认为就是一个普通的DLL—动态连接库!如果你抛弃常用的COM API,比如DELPHI中常用的:
CreateCOMObject()或者
CreateOLEObject()
那么您完全可以直接采用加载普通DLL的方式来调用这个COM组件,比如说,您可以直接用LoadLibrary()函数来加载这个DLL,然后使用 GetProcAddress来调用从这个DLL里输出的接口,从而完成各项操作。这是完全可行的。然而,我不得不告诉大家,把一个COM仅仅看成一个 DLL,那是非常肤浅的看法 – DLL仅仅是一种表现形式而已。更重要的是,COM实现了一种规则。因此我们可以说:
l COM是一种包含了许多处理逻辑、符合了某种接口规范(如Iunknown规范)的DLL组件。
(注:如果没有特别说明,我在本文里所指的COM,都是指进程内的DLL形式的COM)
l COM实现了Iunknown接口。因此,任何只要符合Iunknown规范,实现了Iunknown接口的DLL组件,我们都可以把他看成是一个COM。
那么,什么是Iunknown接口呢?如何实现一个Iunknown接口呢?我们看看,在DELPHI中是如何定义一个Iunknown接口的:
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
IUnknown = IInterface;

简单一点看,我们直接这样理解就行了:
IUnknown = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
在DELPHI里,interface是编译器所认识的一种类型。如果是在VC++中,Iunknown将被定义为一种结构(struct)。如果要实现一个Iunknown接口,我们必须用一个类来实现它,比如在DELPHI中,要实现Iunknown接口可以写成为:
TMyCOMObject = class (Tobject, Iunknown)
……
end;
有心的读者可能会立即问:这个Iunknown接口由Tobject来实现,那么,可不可以是由其他类来实现呢?比如说用Tcomponent类来实现?答案是: 完全可以!!
例如,我们要实现一个自定义的接口IMyCOM,可以写成这样:
IMyCOMTest = interface(Iunknown);
TMyCOMTest = class(Tcomponent, IMyCOMTest)
…….
End;
这样是完全可以的!因为COM关注的只是如何实现一个接口,至于程序员使用什么类来实现,COM是不管的。
后面我们要实现一个COM的例子,而且我打算就用这个IMyCOMTest接口来做。所以我们把这个接口声明成为例1,以便后面使用。

COM的产生
假如我们已经完成了一个COM,并且已经在系统中注册了。那么,一个客户端需要来调用这个COM,这时,系统中发生了哪些事呢?
一般来说,以DELPHI为例,客户程序使用CreateCOMObject或者CreateOLEObject调用COM组件时,会发生以下几个步骤:
1. CreateCOMObject或者CreateOLEObject的动作。
我们看看这两个函数都干了些什么:

function CreateComObject(const ClassID: TGUID): IUnknown;
begin
OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result));
end;

CreateOLEObject稍微复杂些:

function CreateOleObject(const ClassName: string): IDispatch;
var
ClassID: TCLSID;
begin
ClassID := ProgIDToClassID(ClassName);
OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IDispatch, Result));
end;
看到了吗?CreateOLEObject多了一个ProgIDToClassID函数。这意味着,如果我们要用CreateOLEObject来调用我们的COM组件,我们将要多一些步骤来编写我们的COM。这个我将会在后面说明。现在,我们要关注的是CoCreateInstance API函数。
2. CoCreateInstance API函数将调用CoGetClassObject API,这个调用过程我们是看不到相关的代码的,因为微软已经把他封装好了。而CoGetClassObject函数的作用是什么呢?它将调用 LoadLibrary来寻找我们指定的COM组件(DLL),然后使用GetProcAddress 来寻找组件的入口函数 – 还记得我们上面说过的那四个被导出的函数吗?对,其中的DllGetClassObject 函数就在这里将被调用。该函数的原形在DELPHI中是:
function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;
其中第三个参数:Obj ,将向我们返回COM中的定义的接口。但是,要注意,这个接口并不是我们自定义的接口,而是向我们返回了一个被成为是“类工厂”接口的 IclassFactory的接口。当我们获得类工厂接口后,就可以获得我们所需要的、那个我们自定义的接口了。看看IclassFactory 的接口声明:
IClassFactory = interface(IUnknown)
['{00000001-0000-0000-C000-000000000046}']
function CreateInstance(const unkOuter: IUnknown; const iid: TIID;
out obj): HResult; stdcall;
function LockServer(fLock: BOOL): HResult; stdcall;
end;
看到那个CreateInstance 的方法了吗?对了,它的第三个参数 obj 将向我们返回那个我们定义的接口,比如是我们的IMyCOMTest接口(例1)。这样,我们就可以调用我们自定义的接口方法了。
以上的众多步骤看起来有点让人迷惑。那么我们就用一个简单的流程来描绘我们刚才所发生的步骤。不要被那些步骤吓倒,其实他们是非常简单的。
l CreateCOMObject --à CoCreateInstance。 CoCreateInstance 在注册表中查找COM的注册信息。
l CoCreateInstance -à CoGetClassObject 。注册信息被交给CoGetClassObject。这时候CoGetClassObject将知道COM组件在磁盘上的位置。
l CoGetClassObject -à LoadLibrary 。LoadLibrary 将寻找COM DLL的入口,然后GetProcAddress调用其输出函数DllGetClassObject
l DllGetClassObject 的输出参数将向我们返回“类工厂”接口IClassFactory。
l IclassFactory --à CreateInstance 。CreateInstance方法建立其我们实现接口的类。该类将调用自身的QueryInterface 方法,查看用户指定的接口是否被自己实现,如果实现了,则向返回自定义的接口。
l 调用结束后,COM客户将调用COM的DLL输出函数DllCanUnloadNow 。如果该函数返回S_OK,则释放该组件。

实际的COM例子
下面我们来做一个实际的例子。包括如何建立一个COM Server和一个COM Client。
对于COM Server,我们将实现以下功能:
l 单线程,单客户支持。
l 实现自定义的接口
l 能够使用Regsvr32 在系统中注册和反注册。
l 能够被DELPHI或者VC++程序调用。
我们只关注实现最基本的功能。当大家清楚整个流程后,自然就能写出更高级的功能,比如多线程支持等。
下面,让我们开始COM实现之旅。


COM Server程序
l 在DELPHI中,新建一个DLL工程。注意是DLL,而不是 Activex Library。并把工程名保存为MyCOM。然后依次建立两个单元文件:
MyCOMServer 单元: 此单元描述了COM的逻辑实现
COMDef 单元: 此单元描述了COM的输出函数定义。
l 在MyCOM单元里,我们定义DLL的输出函数,整个代码:

library MyCOM;

uses
SysUtils,
Classes,
COMDef,
MyCOMServer in 'MyCOMServer.pas';

//在这里导出四个函数。
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;

{$R *.res}

begin
end.

先做好定义,不要考虑他们是如何实现的。这个在后面我会做详细解说。在这里我先说明这四个函数的作用:
DllGetClassObject : 返回类工厂接口。
DllCanUnloadNow : 告诉客户�
posted @ 2009-07-27 12:57  Handll  阅读(243)  评论(0编辑  收藏  举报