特别说明 —— 该系列文章皆转自知乎作者 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来创建(进程内服务)对象的基本原理。