作者:SaurabhDasgupta
2001年3月6日
原文链接:
http://www.codeguru.com/Cpp/COM-Tech/activex/tutorials/article.php/c5567/
介绍
对于我来说,理解COM(ComponentObjectModel,组件对象模型)绝不亚于一次长途旅行。我相信,每一个想要理解COM之后基本原理的程序员都必须使用普通的C++编写至少一个简单的COM对象,也就是说,不依靠MFC/ATL所提供的任何模板或宏的支持。在本文中,我将要逐步介绍如何从基本原理出发来创建简单的COM对象。这些组件可用于VC/VB的客户端程序。
作为练习,我们将要尝试设计一个COM组件,这一组件将要实现假想的快速相加算法。它必须传入两个长数据类型的参数,并返回另一个长参数给用户,也就是相加算法的结果。我们现在开始设计接口。
接口
COM对象的接口并不涉及到实际的实现,但是它的方法则标志着COM对象中用来和外界通信的部分。我们将我们的接口命名为IAdd,它的声明使用接口定义语言(InterfaceDefinitionLanguage,IDL)。IDL是用来定义函数标志的语言,它独立于各种程序语言之间,这就使得RPC底层能够在不同的计算机之间对参数进行打包、装载与解包。在我们的IAdd接口中,我们拥有SetFirstNumber和SetSecondNumber方法,它们用来传递加法的参数。还有一个方法,DoTheAddition,它用来完成加法并将结果回传给客户端。
第1步:
创建一个新的Win32DLL工程(比如说AddObj),我们将会在这个文件夹中创建接下来的所有文件。创建一个空文件,然后键入以下内容。将它保存为IAdd.idl。接口的标识符可以使用工具uuidgen.exe来生成。
import"unknwn.idl";
[
object,
uuid(1221db62-f3d8-11d4-825d-00104b3646c0),
helpstring("interfaceIAddisusedforimplementingasuper-fastadditionAlgorithm")
]
interfaceIAdd:IUnknown
{
HRESULTSetFirstNumber(longnX1);
HRESULTSetSecondNumber(longnX2);
HRESULTDoTheAddition([out,retval]long*pBuffer);
};
[
uuid(3ff1aab8-f3d8-11d4-825d-00104b3646c0),
helpstring("InterfacesforCodeGurualgorithmimplementations.")
]
libraryCodeGuruMathLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
interfaceIAdd;
}
第2步:
使用命令行编译器MIDL.exe(注意:midl.exe随VC++一同附带,并且可能会包括一些对于midl的路径问题,你可能需要修改你的路径变量设置)来编译文件IAdd.idl。
经过编译,会生成以下文件:
IAdd.h包含C++风格的接口声明。
dlldata.c包含了代理DLL的代码。用于在不同的进程或计算机上调用该对象的情况。
IAdd.tlb二进制文件,描述了我们的IAdd接口以及它的所有方法。该文件可以被我们COM组件的所有客户端使用。
IAdd_p.c包含了代理DLL的编组代码。用于在不同的进程或计算机上调用该对象的情况。
IAdd_i.c包含了接口的IID。
第3步:
现在我们来创建这个COM对象。创建一个新文件(AddObj.h),声明一个C++类,将其命名为CAddObj,继承自接口IAdd(文件IAdd.h)。请记住,IAdd从IUnknown继承而来,它亦是一个抽象基类。因此,我们要和IUnknown一样为抽象基类IAdd声明所有的方法。
///////////////////////////////////////////////////////////
//
//AddObj.h
//包含了实现IAdd接口的C++类声明
//
#include"IAdd.h"
externlongg_nComObjsInUse;
classCAddObj:
publicIAdd
{
public:
//IUnknown接口
HRESULT__stdcallQueryInterface(
REFIIDriid,
void**ppObj);
ULONG__stdcallAddRef();
ULONG__stdcallRelease();
//IAdd接口
HRESULT__stdcallSetFirstNumber(longnX1);
HRESULT__stdcallSetSecondNumber(longnX2);
HRESULT__stdcallDoTheAddition(long*pBuffer);
private:
longm_nX1,m_nX2;//加法的操作数
longm_nRefCount;//管理引用计数
};
///////////////////////////////////////////////////////////
第4步:
我们将要为IAdd接口的所有方法提供实现。创建一个新文件(AddObj.cpp)并实现如下代码的所有方法。
///////////////////////////////////////////////////////////
//
//AddObj.cpp
//包含IAdd接口的方法实现
//
#include<objbase.h>
#include"AddObj.h"
#include"IAdd_i.c"
HRESULT__stdcallCAddObj::SetFirstNumber(longnX1)
{
m_nX1=nX1;
if(m_bIsLogEnabled)WriteToLog("Junk");
returnS_OK;
}
HRESULT__stdcallCAddObj::SetSecondNumber(longnX2)
{
m_nX2=nX2;
returnS_OK;
}
HRESULT__stdcallCAddObj::DoTheAddition(long*pBuffer)
{
*pBuffer=m_nX1+m_nX2;
returnS_OK;
}
第5步:
我们还需要实现IUnknown的方法。我们将要在相同的文件AddObj.cpp中实现3个固定的方法(AddRef、Release和QueryInterface)。那个private成员m_nRefCount用于维护对象的生存期。m_nRefCount不会直接减少或增加,我们采用了一种线程安全的做法,也就是使用互锁增加和减少的API。
HRESULT__stdcallCAddObj::QueryInterface(
REFIIDriid,
void**ppObj)
{
if(riid==IID_IUnknown)
{
*ppObj=static_cast(this);
AddRef();
returnS_OK;
}
if(riid==IID_IAdd)
{
*ppObj=static_cast(this);
AddRef();
returnS_OK;
}
//
//如果控制达到了这里,那么就让客户端知道
//我们不支持请求的接口
//
*ppObj=NULL;
returnE_NOINTERFACE;
}//QueryInterfac方法
ULONG__stdcallCAddObj::AddRef()
{
returnInterlockedIncrement(&m_nRefCount);
}
ULONG__stdcallCAddObj::Release()
{
longnRefCount=0;
nRefCount=InterlockedDecrement(&m_nRefCount);
if(nRefCount==0)deletethis;
returnnRefCount;
}
第6步:
我们已经完成了AddCOM对象的功能部分。就像每一条COM的准则一样,每一个COM对象都必须有一个接口IClassFactory的单独实现。客户端将会使用这个接口来获得我们IAdd接口实现的一个实例。IClassFactory接口就像其它所有的COM接口一样,也是继承自IUnknown的。因此,我们将要提供IUnknown方法的实现以及IClassFactory方法(LockServer和CreateInstance)的实现。创建一个新文件(名之为AddObjFactory.h),声明一个类CAddFactory,继承自IClassFactory。
///////////////////////////////////////////////////////////
//
//AddObjFactory.h
//包含了IClassFactory实现的C++类声明
//
classCAddFactory:publicIClassFactory
{
public:
//IUnknown接口的方法
HRESULT__stdcallQueryInterface(
REFIIDriid,
void**ppObj);
ULONG__stdcallAddRef();
ULONG__stdcallRelease();
//IClassFactory接口的方法
HRESULT__stdcallCreateInstance(IUnknown*pUnknownOuter,
constIID&iid,
void**ppv);
HRESULT__stdcallLockServer(BOOLbLock);
private:
longm_nRefCount;
};
第7步:
现在需要实现CAddFactory的方法。创建一个新文件(AddObjFactory.cpp),在其中提供IUnknown和IClassFactory所有方法的实体。AddRef、Release和QueryInterface方法就和类CAddObj的实现相似。CreateInstance方法的实现如下,其中实例化了CAddObj类并回传了请求的接口指针。LockServer方法则没有特定的实现。
HRESULT__stdcallCAddFactory::CreateInstance(IUnknown*pUnknownOuter,
constIID&iid,
void**ppv)
{
//
//这一方法可以使得全体客户端来制造组件
//类工厂提供了一种机制,可以控制生成组件的方法。
//通过类工厂,组件的作者就可能决定使每一条许可协议
//的创建生效或失效。
//
//不能聚合
if(pUnknownOuter!=NULL)
{
returnCLASS_E_NOAGGREGATION;
}
//
//创建组件的实例
//
CAddObj*pObject=newCAddObj;
if(pObject==NULL)
{
returnE_OUTOFMEMORY;
}
//
//获得请求的接口
//
returnpObject->QueryInterface(iid,ppv);
}
HRESULT__stdcallCAddFactory::LockServer(BOOLbLock)
{
returnE_NOTIMPL;
}
第8步:
一个进程内的COM对象其实和一个简单的Win32DLL并没有什么两样,它们都遵守一个特定的协议。每个COMDLL都必须有一个名为DllGetClassObject的导出函数,客户端将会调用这个函数来获得类工厂(IUnknown或IClassFactory)的一个实例,然后紧接着是CreateInstance方法。创建一个新文件(Exports.cpp),在其中实现DllGetClassObject。
STDAPIDllGetClassObject(constCLSID&clsid,
constIID&iid,
void**ppv)
{
//
//检查请求的COM对象是否在此DLL之中
//DLL中可以实现多个COM对象
//
if(clsid==CLSID_AddObject)
{
//
//iid为类工厂指定了请求的接口
//客户端可以请求IUnknown、IClassFactory、IClassFactory2
//
CAddFactory*pAddFact=newCAddFactory;
if(pAddFact==NULL)
returnE_OUTOFMEMORY;
else
{
returnpAddFact->QueryInterface(iid,ppv);
}
}
//
//如果控制达到了这里,那么这就表示该DLL中没有实现用户指定的对象
//
returnCLASS_E_CLASSNOTAVAILABLE;
}
第9步:
客户端还需要知道一个COMDLL什么时候可以从内存中卸载。深入一些来讲,进程内的COM对象可以通过调用API函数LoadLibrary来进行显式装载,这就可以将DLL装入客户端的进程地址空间。同样,调用FreeLibrary可以将DLL显式卸载。
COM客户端必须知道DLL什么时候可以被安全卸载,它必须确认当前DLL中没有任何存在的COM对象实例。为了让这个计算更简单一些,我们将会在COMDLL中CAddObj和CAddFactory的C++构造函数中增加一个全局变量(g_nComObjInUse)的值。相似地,我们会在它们各自的析构函数中减少g_nComObjInUse的值。
我们还要导出一个特定的COM函数DllCanUnloadNow,它的实现如下:
STDAPIDllCanUnloadNow()
{
//
//当DLL中没有存在的对象时,它就不在使用中了
//(它所有的对象引用计数为0)
//我们将会检查g_nComObjsInUse的值
//
if(g_nComObjsInUse==0)
{
returnS_OK;
}
else
{
returnS_FALSE;
}
}
第10步:
COM对象的位置还需要被写入注册表,这可以通过一个外部的.REG文件实现,或者让DLL导出一个DllRegisterServer函数。为了清除注册表的内容,我们还要导出另一个DllUnregisterServer函数。这两个函数的实现在Registry.cpp之中。你可以使用一些简单的工具(如regsvr32.exe)来装载一个特定的DLL并执行DllRegisterServer/DllUnregisterServer。
为了使链接器导出这4个函数,我们还需要创建一个模块定义文件(Exports.def)。
;
;包含了DLL中导出的函数列表
;
DESCRIPTION"SimpleCOMobject"
EXPORTS
DllGetClassObjectPRIVATE
DllCanUnloadNowPRIVATE
DllRegisterServerPRIVATE
DllUnregisterServerPRIVATE
第11步:
我们需要最后处理一下我们的Win32DLL工程AddObj。将IAdd.idl文件插入工程的工作区中。
为此文件设置自定义的构建选项。
在“Post-buildstep”对话框中插入一个命令行字符串来在每次构建结束后运行regsvr32.exe。
构建此DLL。将该IDL文件插入工作区将会使得在每一次文件被修改后外部的编辑更加容易。每一次我们的工程成功编译后,COM对象也就注册完成了。
第12步:
现在来在VisualBasic中使用AddObj这个COM对象。创建一个简单的EXE工程,并运行以下几行代码。请确认要添加一个IAdd.tlb类型库的工程引用。
DimiAddAsCodeGuruMathLib.iAdd
SetiAdd=CreateObject("CodeGuru.FastAddition")
iAdd.SetFirstNumber100
iAdd.SetSecondNumber200
MsgBox"total="&iAdd.DoTheAddition()
第13步:
我们之前使用了以下的文件:
IAdd.idl包含了的接口声明。
AddObj.h包含了类CAddObj的C++类声明。
AddObjFactory.h包含了类CAddFactory的C++类声明。
AddObj.cpp包含了类CAddObj的C++类实现。
AddObjFactory.cpp包含了类CAddFactory的C++类实现。
Exports.cpp包含了DllGetClassObject、DllCanUnloadNow和DllMain的实现。
Registry.cpp包含了DllRegisterServer、DllUnregisterServer的实现。
AddObjGuid.h包含了我们的COM对象AddObj的GUID值。
第14步:
类型库也可以随同AddObj.dll一同被发布。为了简化这一过程,IAdd.tlb类型库也可以作为AddObj.dll的一个资源文件嵌入其中。这样一来,就可以只向客户发布DLL文件AddObj.dll了。
第15步:
一个VisualC++客户端可以通过以下几种方法来使用COM接口:
1.#impott"IAdd.tlb"。
2.IAdd.h头文件。在这种情况下,DLL的卖主必须将IAdd.h头文件随同DLL一同发布。
3.使用一些向导工具(例如MFC的ClassWizard)产生C++代码。
对于第1种方法,编译器创建一些包含了接口声明的中间文件(.tlh、.tli)。更进一步说,编译器也在原始接口的基础上定义了智能接口指针。智能接口指针类使得COM程序员能更轻松地管理COM对象的生存期。
在以下的例子中#import直接导入了AddObj.dll,而不是AddObj.tlb,因为我们将TLB文件包含于DLL之中了。否则的话,#import将导入TLB文件。
在VC++中创建一个新的控制台EXE,输入以下的内容并编译。
//
///Client.cpp
//
//客户端从AddObj.dll中使用COM对象的演示
//
#include<objbase.h>
#include<stdio.h>
#import"AddObj.dll"
//
//这里我们对DLL使用了#import,你也可以对.TLB使用#improt。
//#import直接在输出文件夹中生成了两个文件(.tlh/.tli)。
//
voidmain()
{
longn1=100,n2=200;
longnOutPut=0;
CoInitialize(NULL);
CodeGuruMathLib::IAddPtrpFastAddAlgorithm;
//
//IAddPtr并不是实际的IAdd指针,而是一个C++类模板(_com_ptr_t)
//它嵌入了原始的IAdd指针实例
//在析构的时候,析构函数内部会调用原始接口指针的Release()
//更进一步说,它还重载了operator->来直接操作内部原始接口指针的所有方法
//
pFastAddAlgorithm.CreateInstance("CodeGuru.FastAddition");
pFastAddAlgorithm->SetFirstNumber(n1);//“->”重载的运行
pFastAddAlgorithm->SetSecondNumber(n2);
nOutPut=pFastAddAlgorithm->DoTheAddition();
printf("\nOutputafteradding%d&%dis%d\n",n1,n2,nOutPut);
}
点此下载源代码
点此下载演示工程