COM 实现过程
原创:吴剑明(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 : 告诉客户端该COM是否可以被正常卸载。
DllRegisterServer : 向系统注册COM组件信息。Regsvr32.exe 就是调用这个函数来进行注册的。
DllUnregisterServer :从系统中反注册一个COM。Regsvr32.exe 就是调用这个函数来进行反注册的。
l 现在为我们的COM定义GUID。注意,在一个COM组件里,有三种GUID:
CLASS_xx 类型的GUID,是用来标识COM组件的。
IID_xx类型的GUID,是用来标识一个接口的。可以有多个。
LIBID_xx类型的GUID,是用来标识一个TypeLib的。关于如何实现一个TypeLib,我会在COM实现过程(2)中会讲到。在这里先略过。
现在,我们定义我们的COM组件的GUID,在COMDef单元里加入的代码为:
const
Class_MyCOM: TGUID = '{CE38847E-A386-4753-89F1-34BE80042107}';
LIBID_MyCOM: TGUID = '{C2387E2A-0F08-442E-8947-D1AB36A9BDD0}';
l 开始定义我们的接口。我们采用例一所定义的接口,在MyCOMServer单元里加入代码:
IMyCOMTest = interface(IUnknown)
['{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}']
function msg: integer;stdcall;
end;
注意,在这里,我们为IMyCOMTest 定义了一个IID类型的GUID,它唯一表示我们的接口。同时我们定义了一个方法 msg ,它向我们返回一个整数值。
定义了接口,当然要实现它:
TMyCOMServer = class(TObject, IMyCOMTest)
protected
FLock: integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
Constructor Create;
function msg: integer;stdcall;
end;
现在,我们一个个的来说明并实现TMyCOMServer 的几个方法。
_AddRef 方法:
该方法用于增加一个COM的计数器。我们知道COM Server是需要支持多客户调用的,在COM中每实现一次调用,就必须进行计数。如果COM的计数>0 ,则COM继续生存。否则,该COM接口实例就必须被注销。
_Release 方法:
该方法和_AddRef方法相反,用于减少一个计数,当计数为0,则注销该接口。
QueryInterface 方法:
该方法用于查询一个接口是否存在,如果存在则在输出参数里返回该接口指针。
为实现这几个方法,所添加的代码如下:
function TMyCOMServer._AddRef: Integer;
begin
Inc(FLock);
end;
function TMyCOMServer._Release: Integer;
begin
Dec(FLock);
if FLock = 0 then
Free;
end;
function TMyCOMServer.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
//在类工厂中实现的一个MC接口,在这里返回。由于我们只有一个接口,所以那个IID参数没有用。当然,如果我们实现了多个接口,就需要维护一个数组,用个CASE语句,根据不同的IID,来返回不同的接口。
Pointer(Obj) := Pointer(MC);
Result := S_OK;
end;
其中,MC是一个IMyCOMTest 接口,是个全局变量:
var
MC: IMyCOMTest;
CF: IClassFactory;
别忘了还有我们的构造器:
constructor TMyCOMServer.Create;
begin
Inc(FLock);
end;
在一开始就增加一个引用计数,使得COM计数大于1,这样COM就不会自动销毁。
然后去实现我们的接口方法:
function TMyCOMServer.msg: integer;
begin
result := 1978;
end;
该方法直接向我们返回一个整数值。
l 类工厂的实现
正如我前面所说的,一个类工厂必须去建立我们自定义的接口。在上面,我们定义了自定义的接口,并由类TMyCOMServer 去实现。那么,现在我们还要做的是,实现类工厂,然后由类工厂建立一个TMyCOMServer 的接口实例。类工厂接口定义如下:
IClassFactory = interface(IUnknown)
['{00000001-0000-0000-C000-000000000046}']
function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID;
out Obj): HResult; stdcall;
function LockServer(fLock: Boolean): HResult; stdcall;
end;
注意,IclassFactory是系统预先定义了的,在ACTIVEX单元有,所以不需要自己再去定义一次。我们只要去实现它就是:
TClassFactory = class(TObject, IClassFactory)
protected
FLock: integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
Constructor Create;
function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID;
out Obj): HResult; stdcall;
function LockServer(fLock: Boolean): HResult; stdcall;
end;
我们只关注CreateInstance 方法。LockServer 用于在多客户调用COM时,锁定COM,以免一个客户退出时销毁了COM,那么其他客户的调用将发生错误。但是我们在这里只实现单客户,所以不考虑这个函数,把他置空就是。
function TClassFactory.CreateInstance(const UnkOuter: IInterface;
const IID: TGUID; out Obj): HResult;
begin
//我们的自定义接口,就是在这里被创建的。
MC := TMyCOMServer.Create;
Pointer(Obj) := Pointer(MC);
end;
function TClassFactory.LockServer(fLock: Boolean): HResult;
begin
end;
同样的,TclassFactory也必须实现引用计数,因为它也实现了Iunknown接口。
function TClassFactory._AddRef: Integer;
begin
Inc(FLock);
end;
function TClassFactory._Release: Integer;
begin
Dec(FLock);
if FLock = 0 then
Free;
end;
function TClassFactory.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
end;
其中,QueryInterface 我把它置空,因为在这个例子中,不需要向它查询什么接口。如果以后读者需要向它查询接口时,自己实现相关代码。
同样,在它的构造器中,也预先对计数加1
constructor TClassFactory.Create;
begin
Inc(FLock);
end;
到目前为止,我们已经基本实现了一个COM需要的大部分功能。现在,我们需要把它注册到系统中,以便被其他程序调用。
l COM的注册和反注册
我们回过头来,看看如何去实现那四个DLL的输出函数。这四个函数的原形如下:
function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;stdcall;
function DllCanUnloadNow: HResult;stdcall;
function DllRegisterServer: HResult;stdcall;
function DllUnregisterServer: HResult;stdcall;
我们上面所说的类工厂的实例,就是在DllGetClassObject 中创建的。代码如下:
function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult;
begin
CF := TClassFactory.Create;
Pointer(obj) := Pointer(CF);
Result := S_OK;
end;
同样的,我们只有一个类工厂,所以可以不理会前面那两个参数。否则,就要根据不同GUID,来创建不同的类工厂对象。在这里,我们直接把类工厂对象给返回了。
函数DllCanUnloadNow 用来注销一个COM。在正常使用中,要根据引用计数,来判断是否允许用户注销。在这里我们直接返回S_OK,让用户直接注销。
function DllCanUnloadNow: HResult;
begin
Result := S_OK;
end;
函数DllRegisterServer 用来向注册表注册一个COM组件信息。要注册一个COM,用户必须知道COM在注册表中的信息是如何组织的。结构如下:
HKEY_CLASSES_ROOT
---- CLSID
---- GUID
----- InprocServer32 标明 COM所在磁盘的路径以及线程模型
----- ProgID 标明COM所实现的接口
----- TypeLib 标明 COM 的类型库的GUID
----- Version 标明 COM的版本号。
当发生CreateCOMObject()调用时,输入参数为COM的CLASS类型的GUID,系统将在注册表中搜索到相关信息,然后就可以找到该COM的位置,就可以开始调用了。
注意,如果您希望COM组件支持客户端的CreateOLEObject()函数的调用,您必须还要注册一个信息:
HKEY_CLASSES_ROOT
----- 接口声明
----- CLSID 标明 COM 接口和CLASS类型的GUID的对应关系。
那么,当发生 CreateOLEObject 调用时,系统将会根据输入参数(一个COM接口声明,如a.b),去查找和接口对应的CLASS GUID,然后就可以读到COM的相关信息了。
全部代码如下:
function DllRegisterServer: HResult;
var
lp: pchar;
ns: Dword;
begin
Result := S_FALSE;
Reg := TRegistry.Create;
GetMem(lp, 255);
try
Reg.RootKey := HKEY_CLASSES_ROOT;
if Reg.OpenKey('\MyCOM.MyCOMTest',true) then
begin
Reg.CreateKey('CLSID');
if Reg.OpenKey('CLSID',true) then
Reg.WriteString('',GUIDToString(Class_MyCOM));
end;
if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM), true) then
begin
if Reg.CreateKey('InprocServer32') = false or
Reg.CreateKey('ProgID') = false or
Reg.CreateKey('TypeLib') = false or
Reg.CreateKey('Version') = false then
Exit;
Reg.WriteString('','MyCOM');
if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) +
'\InprocServer32', false) then
begin
Windows.GetModuleFileName(HInstance,lp, 255);
Reg.WriteString('', lp);
Reg.WriteString('ThreadingModel', 'Single');
end;
if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\ProgID', false) then
Reg.WriteString('','MyCOM.MyCOMTest');
if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\Version', false) then
Reg.WriteString('','1.0');
if Reg.OpenKey('\CLSID\' + GUIDToString(Class_MyCOM) + '\TypeLib', false) then
Reg.WriteString('',GUIDToString(LIBID_MyCOM));
Reg.CloseKey;
Result := S_OK;
end;
finally
begin
FreeMem(lp);
Reg.Free;
end;
end;
end;
函数DllUnregisterServer 则向系统注销一个COM的注册信息。它比较简单,直接把COM的相关注册键给删除就是:
function DllUnRegisterServer: Hresult;
begin
Result := S_False;
Reg := TRegistry.Create;
try
Reg.RootKey := HKEY_CLASSES_ROOT;
Reg.DeleteKey('\CLSID\' + GUIDToString(Class_MyCOM));
Reg.CloseKey;
Finally
Reg.Free;
end;
end;
l 最后工作。
现在,我们编译程序,然后生成一个DLL文件,在命令行下,使用:
regsvr32 MyCOM.dll
向系统注册COM。
COM Client程序
在DELPHI中调用
新建一个项目,然后在单元中,定义接口信息:
IMyCOMTest = interface(IUnknown)
['{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}']
function msg: integer;stdcall;
end;
定义变量:
class_wjm: TGUID = '{CE38847E-A386-4753-89F1-34BE80042107}';
a: IMyCOMTest;
然后在窗口的OnCreate 事件里,添加如下代码:
procedure TForm1.FormCreate(Sender: TObject);
begin
//随便用哪个都可以
a := createcomobject(class_wjm) as IMyCOMTest;
//或者使用 a := createoleobject('MyCOM.MyCOMTest') as IMyCOMTest;
end;
然后,放一个按钮,并在其事件里添加代码:
procedure TForm1.Button1Click(Sender: TObject);
begin
showmessage(inttostr(a.msg));
end;
在窗口的OnCLose事件里加上:
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
a := nil;
end;
注意一定要释放接口,否则可能是个难看的 AV 错误哦。
运行我们的程序,点下按钮,你将看到输出信息"1978"。
如果你看不到正确的信息,请仔细查看你的代码是否和文中一致。你也可以直接向我索要源代码。
在VC6中调用
稍微复杂点。先把GUID翻译过来:
//{CE38847E-A386-4753-89F1-34BE80042107};
static const CLSID CLSID_MyCOM = {0xCE38847E,0xA386,0x4753,
{0x89,0xF1,0x34,0xBE,0x80,0x04,0x21,0x07}};
//{D1C4A022-7F6F-42F0-A9B0-4A91703EB124}
static const IID IID_MyCOM = {0xD1C4A022,0x7F6F,0x42F0,
{0xA9,0xB0,0x4A,0x91,0x70,0x3E,0xB1,0x24}};
然后在声明一次接口的定义:
struct IMyCOMTest : public IUnknown
{
virtual LONG __stdcall msg();
};
IMyCOMTest* pLink;
然后放个按钮上去,并在相关事件里写代码:
void CMyvcView::OnButton6()
{
pLink = NULL;
int a =0;
CoInitialize(NULL);
a = CoCreateInstance(CLSID_MyCOM, NULL,
CLSCTX_INPROC_SERVER,IID_MyCOM, (void**)&pLink);
if (a==S_OK){
LONG a= pLink->msg();
};
}
注意,一定要记住调用 CoInitialize(NULL); 这个函数,否则COM无法使用的。
编译运行,你应该能看到 a 是等于1978 的。
总结
到目前为止,我们成功的编写了一个最简单的COM组件,并且在DELPHI和VC中成功调用。这都说明我们的工作是成功的。同时我们也看到,实现一个COM,并不难。
关于进程外的COM以及DCOM,前者是基于LPC 本地过程调用,后者是基于RPC远程过程调用。除了协议不同外,其他的都一样。大家有兴趣,可以以后继续讨论。
关于COM的线程模式,我曾经以为,是COM向导中自动会产生对应的线程代码,来实现多线程的。但是我后来又认为,根本没有这回事,COM只是做了个标记,告诉操作系统他的线程模型,至于如何产生线程,则是操作系统做的。有关这方面的讨论,还需要进一步研究。
一个小尾巴
我们知道,在DELPHI里,有一个Import Type Library 的功能。可以把一个COM组件导到DELPHI中直接使用。但是,如果我们试图把我们刚才写的那个组件,也ADD进去的时候,DELPHI会提示:
加载类型库/DLL时出错。
这是怎么回事呢? 原来,这是MS/BORLAND的一个小花招。我们看看VCL的代码就知道了,在DELPHI的向导为你创建一个COM时,它偷偷地加了一个IprovideClassInfo 的接口进去,该接口使用ItypeInfo 接口,主要用于向外提供COM的类型信息的。大家仔细跟下TtypedComObject 这个类,就会发现这个奥秘了。在前例中,我们没有实现这个接口,那么当然无法被DELPHI加载了。关于如何实现这个接口,已经超出了本文的范围,所以不于讨论。
有兴趣的朋友,可以继续关注 "COM实现过程(2)",主要讲述如何实现类型库的。
2002/6/27
版权所有
转载时请包括作者姓名