COM编程攻略(五 使用COM API创建COM对象)

特别说明 —— 该系列文章皆转自知乎作者 Froser 的COM编程攻略:https://www.zhihu.com/column/c_1234485736897552384



前言

上一篇讲了创建COM对象的基本原理,引出了一个通用的创建接口:

HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID iid, void** ppvObject);

这个接口,可以创建聚合或者非聚合的COM对象,返回指定IID接口。

这一篇讲,在Windows中,我们如何使用COM提供的API来创建COM对象。


提示:以下是本篇文章正文内容,下面案例可供参考

一、类厂

所谓类厂,就是创建某个类的工厂,和设计模式中说的工厂模式一致。

例如,假如我需要创建一个类MyClass,那么我可以写一个MyClassFactory:

struct MyClassFactory
{
  MyClass* createInstance();
};

接下来,我们只需要拿到一个MyClassFactory实例,便可以创建我们需要的MyClass类的实例了:

MyClassFactory* factory = GetMyClassFactory(); // 假设我们可以通过GetMyClassFactory拿到工厂实例
MyClass* myNewClass = factory->createInstance();

通过类厂创建实例,其实也就是以上两步。首先拿到一个工厂对象,然后再调用其创建实例的方法。

COM中,由于不关心我们生成的实例的类型(因为只需要拿它实现的接口),所以将类厂就称之为类(Class)。由于COM对象创建具有通用性,因此它引出了一个接口IClassFactory:

interface IClassFactory : public IUnknown
{
    public:
        virtual HRESULT STDMETHODCALLTYPE CreateInstance( 
            _In_opt_  IUnknown *pUnkOuter,
            _In_  REFIID riid,
            _COM_Outptr_  void **ppvObject) = 0;
     ...
};

这个接口所申明的CreateInstance也就是我们上一篇所说的CreateInstance的标准签名,其含义也一模一样。COM规定,任何COM类都需要继承这个接口,这样方便COM API来创建对象实例。

二、一般对象的创建方法

假如我们有一个客户端A.exe,以及一个库B.exe,B.exe中有如下接口定义:

interface IRead : IUnknown
{
 ...
};

interface IWrite : IUnknown
{
 ...
};

class ReadWriteFactory : public IClassFactory
{
    public:
        // CreateInstance 创建一个拥有IRead和IWrite的实例ReadWrite
        virtual HRESULT STDMETHODCALLTYPE CreateInstance( 
            _In_opt_  IUnknown *pUnkOuter,
            _In_  REFIID riid,
            _COM_Outptr_  void **ppvObject);
};

class ReadWrite : public IRead, public IWrite
{
 ...
};

此时,如果我们需要在A.exe创建一个ReadWrite实例,并且使用它的IRead接口(当然,我们在A.exe中是不知道ReadWrite这个类的,我们只知道ReadWriteFactory会创建一个IRead对象),那么首先我们需要能拿到ReadWriteFactory的一个实例。这意味着,我们在B.dll中至少得要暴露出一个函数来取:

// B.dll
IClassFactory* declspec(__dllexport) getReadWriteFactory()
{
  static ReadWriteFactory s_factory;
  return &s_factory;
}

然后我们就可以在A中愉快地创建我们需要的IRead实例了:

// A.exe
HMODULE hLib = LoadLibrary("B.dll");
typedef IClassFactory* (*GetReadWriteFactoryProc)();
GetReadWriteFactoryProc getReadWriteFactory = (GetReadWriteFactoryProc)GetProcAddress(hLib, "getReadWriteFactory");
IClassFactory* factory = getReadWriteFactory();
IRead* reader;
factory->CreateInstance(NULL, IID_IREAD, (void**)&reader);

以上就是我们最通用的创建对象的方法。由于一个dll中,并不一定只有一个factory,我们可以设计得更通用一点,能返回各种factory:

// B.dll
IClassFactory* declspec(__dllexport) getReadWriteFactory(int which);

我们根据参数which,可以返回dll中指定的不同的工厂类。

三、COM对象的创建方法

COM对象的创建流程和上面类似。首先,我们必须要加载类所在的DLL,然后我们要找到需要创建的实例的类,获取其IClassFactory接口,接下来再调用类的IClassFactory::CreateInstance()。

COM API将上面的步骤合并成了一步。类和接口一样,也有一个唯一的ID,接口叫做IID,类叫做CLSID,它们本质上都是GUID结构。

我们的目的是,将一个本地DLL,加载到exe的进程中,所以属于进程内运行的模式。COM API提供了下面这个API来获取接口:

HRESULT CoGetClassObject(
  REFCLSID rclsid,
  DWORD    dwClsContext,
  LPVOID   pvReserved,
  REFIID   riid,
  LPVOID   *ppv
);

rclsid是指class id。所有的class id均保存在注册表的HKCR处:
在这里插入图片描述
在这里插入图片描述
CLSID下每一个键都是一个CLSID,它的默认值指明了类名,如上方截图的LocalCopyHelper。在它下面,如果存在一个子键InProcServer32,里面是CLSID对应的dll的路径,那么说明我们将这个dll已经暴露给了COM,它支持客户使用CoGetClassObject来获取里面的接口。我们传入对应的class id到CoGetClassObject的第一个参数,那么对应的这个DLL就会被加载起来。

第二个参数dwClsContext是表示创建特征,由于我们是在进程内加载DLL,所以传入CLSCTX_INPROC_SERVER。

第三个参数和RPC调用有关。我们本地加载DLL,所以传入NULL。

第四个参数表示需要返回的接口lD,在这里我们填写IID_IClassFactory。

第五个参数接受返回的结果。

当上述参数的CoGetClassObject被调用后,它首先从注册表找到对应的dll,并且加载它,然后运行dll的一个导出函数

HRESULT DllGetClassObject(
  REFCLSID rclsid,
  REFIID   riid,
  LPVOID   *ppv
);

这个函数必须由dll提供方,也就是上面我们说的B.dll来实现。B.dll将通过class id创建对应的Factory,并调用QueryInterface将结果写入ppv返回。

此时,客户已经拿到了IClassFactory了,于是可以通过CreateInstance创建实例了。整个过程看起来就是这样:

// A.exe
IRead* reader;
IClassFactory* pcf = NULL;
HRESULT hr = CoGeClassObject(CLSID_ReadWriteFactory,
  CLSCTX_INPROC_SERVER,
  NULL,
  IID_IClassFactory,
  (void**)&pcf);
if (SUCCEEDED(hr))
{
  hr = pcf->CreateInstance(NULL, IID_IRead, (void**)&ireader);
  pcf->Release(); // 不要忘记减引用计数
}

出于效率的原因,微软将上面的3部操作(CoGetClassObject, CreateInstance, Release)合为了一个函数及它的扩展版本:CoCreateInstance, CoCreateInstanceEx。
CoCreateInstanceEx function
以上是微软的文档,CoCreateInstanceEx比不带Ex版本的区别在于,它可以一次返回创建后的多个接口。在上面的例子中,我们的代码就可以简化为:

IRead* reader;
HRESULT hr = CoCreateInstance(CLSID_ReadWriteFactory, NULL, CLSCTX_INPROC_SERVER, IID_IRead, (void**)&reader); 

在ATL中,我们在CYourClass中可以简单地写上**DECLARE_CLASSFACTORY()**宏,来指定你 默认工厂类:
DECLARE_CLASSFACTORY()
这个宏,会在ATL的AtlComModuleGetClassObject函数中被用到,这个我们以后再详细说明。

四、DLL的注册和反注册

我们看到,为了使得我们的B.dll能够被CoCreateInstance激活,我们必须要修改注册表。微软提供了一个regsvr32.exe,可以来安装、反安装我们dll的服务。

在regsvr32.exe指定安装一个dll服务时,它会调用下面的导出方法:

STDAPI DllRegisterServer(void);

上面这个函数由B.dll实现,做的事情无非就是写注册表,注册CLSID等信息。

对应的,在regsvr32.exe指定反安装一个dll服务时,调用下面的方法:

STDAPI DllUnregisterServer(void);

这个方法也由B.dll实现,职责是清除注册时写入注册表的信息。

由于写注册表工作非常重复而且繁琐,所以ATL提供了一个类,来专门完成注册表写入和删除相关事情:

class CATLReadWriteModule : public ATL::CAtlDllModuleT< CATLReadWriteModule >
{
   ...
};
CATLReadWriteModule _AtlModule;
STDAPI DllRegisterServer(void)
{
	// 注册对象、类型库和类型库中的所有接口
	HRESULT hr = _AtlModule.DllRegisterServer();
	return hr;
}

// DllUnregisterServer - 移除系统注册表中的项。
_Use_decl_annotations_
STDAPI DllUnregisterServer(void)
{
	HRESULT hr = _AtlModule.DllUnregisterServer();
	return hr;
}

一般情况下,我还是建议使用ATL的。虽然ATL很难看,但是,它毕竟是官方的,可靠性较高。

以上便是通过COM API来创建(进程内服务)对象的基本原理。


  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值