特别说明 —— 该系列文章皆转自知乎作者 Froser 的COM编程攻略:https://www.zhihu.com/column/c_1234485736897552384
前言
上一篇文章中说到,微软推出了ATL库实现了IUnknown接口,推出了基于继承(CComObject<>)的模板类和基于聚合(CComAggObject<>)的模板类。
这篇文章来将一下,如何创建一个COM对象,它的统一创建函数的签名为何是这样设计的。
一、传统的C++对象创建方式
假设客户的代码运行在A.exe,客户使用了开发商提供的一个库B.dll,B.dll中导出了一个类ClassB,那么在A.exe中,创建一个ClassB对象,假如ClassB是这样的:
interface IRead : IUnknown
{
...
};
interface IWrite : IUnknown
{
...
};
class ClassB : public IRead, public IWrite
{
...
};
那么可以是这样来写:
ClassB* b = new ClassB();
这样,我们便可以将ClassB对象创建出来。不过,它有几个问题:
- new运算符跨越了边界。operator new的行为在A中被实现,例如虚表指针是在哪里,是否在结构中有额外的数据,而ClassB实现在B.dll。除非我们使用兼容的编译器,生成相同的ClassB结构,否则可能会引起一些二进制兼容问题。
- new对象和释放对象可能不在同一个模块。尤其是COM对象,分配在堆上面的COM对象,是通过Release()来释放自己的,如果在另外的模块new出来,在自己模块释放,在某些情况下(如mt, md不一致),会出现问题。为了解决这个问题,我们需要在模块B.dll中暴露一个创建对象的函数。
- 一个ClassB这样的COM对象,我们不关心它的实现类ClassB,而只需要拿它的某一个接口。例如,我们只需要在ClassB中拿到IRead或者IWrite接口,如果我们调用了ClassB中除了这两个接口之外的函数或者成员并调用了,那么它就和ClassB的实现耦合了起来,违背了COM对象解决二进制兼容的原则。所以,在创建对象的时候,我们要指明,我们需要拿哪个接口。
综上,我们设计出第一版创建ClassB的函数(伪代码),它实现在了B.dll:
void* CreateInstance(REFIID iid)
{
ClassB* b = new ClassB();
if (iid是IID_IWrite)
return static_cast<IWrite*>(b);
if (iid是IID_IRead)
return static_cast<IRead*>(b);
return nullptr;
}
上面这一版是我们创建ClassB的雏形,它需要进一步优化。首先,按照COM标准,它需要返回一个值指示是否转换成功。其次,它需要获取什么接口,可以由ClassB::QueryInterface来得到。于是我们设计出了第二版创建函数:
HRESULT CreateInstance(REFIID iid, void** ppvObject)
{
ClassB* b = new ClassB();
return b->QueryInterface(iid, ppvObject);
}
接下来我们再进行一次优化。回想起CComAggObject,它是一个聚合模型,它本身是IUnknown,它所包含的对象也是一个IUnknown。我们创建的IUnknown可能就是要塞入这个CComAggObject聚合对象中的,这种情况下,创建出来的对象必须要感知到,外层对象(也就是CComAggObject所包含的对象)的指针是什么,这样自己在QueryInterface, AddRef或Release的时候才能转调外层。为了支持这种形式的创建,我们最终将签名改为:
HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID iid, void** ppvObject);
上面的例子创建的是继承关系下的reader对象,在ATL中,reader其实就是CComObject。如果是创建一个聚合对象,那么它其实就是创建的CComAggObject。
以上便是COM中最标准的创建函数的签名。当然HRESULT应该返回什么,MSDN上做了详细的说明,大家实现的时候记得要参考MSDN。
二、ATL中的CreateInstance的实现

我们再次看一下这个继承关系图。上一篇文章里说到,CComObjectRoot提供了内部的AddRef, Release和QueryInterface的实现。那么这一篇文章就介绍CComCoClass<>,它主要是在CYourClass中添加了CreateInstance静态方法,并且可以指定这个对象创建出来是继承模型(CComObject<>)还是聚合模型(CComAggObject<>)。
CComCoClass实现在atlcom.h:
template <class T, const CLSID* pclsid = &CLSID_NULL>
class CComCoClass
{
public:
DECLARE_CLASSFACTORY()
DECLARE_AGGREGATABLE(T)
typedef T _CoClass;
template <class Q>
static HRESULT CreateInstance(
_Inout_opt_ IUnknown* punkOuter,
_COM_Outptr_ Q** pp)
{
return T::_CreatorClass::CreateInstance(punkOuter, __uuidof(Q), (void**) pp);
}
template <class Q>
static HRESULT CreateInstance(_COM_Outptr_ Q** pp)
{
return T::_CreatorClass::CreateInstance(NULL, __uuidof(Q), (void**) pp);
}
上面源码省略了错误处理相关函数,且我们先不用关系第二个模板参数pclsdid的含义。
关注到CreateInstance的两个重载。它调用了模板T中的_CreatorClass类中的CreateInstance函数。区别在于,一个需要传入一个punkOuter,一个不需要,默认传的是NULL,我们便推测得出这两个方法,一个是用于创建聚合对象,聚合对象的QueryInterface将转调punkOuter->QueryInterface;一个是用于创建继承模型下的对象的。
那么,_CreatorClass到底是什么呢?我们看到有一个宏:
DECLARE_AGGREGATABLE(T)
它定义在了altcom.h:
#define DECLARE_AGGREGATABLE(x) public:\
typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
其中CComCreator用于创建一个对象:
template <class T1>
class CComCreator
{
public:
static HRESULT WINAPI CreateInstance(
_In_opt_ void* pv,
_In_ REFIID riid,
_COM_Outptr_ LPVOID* ppv)
{
ATLASSERT(ppv != NULL);
if (ppv == NULL)
return E_POINTER;
*ppv = NULL;
HRESULT hRes = E_OUTOFMEMORY;
T1* p = NULL;
ATLPREFAST_SUPPRESS(6014 28197)
/* prefast noise VSW 489981 */
ATLTRY(p = _ATL_NEW T1(pv))
ATLPREFAST_UNSUPPRESS()
if (p != NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->_AtlInitialConstruct();
if (SUCCEEDED(hRes))
hRes = p->FinalConstruct();
if (SUCCEEDED(hRes))
hRes = p->_AtlFinalConstruct();
p->InternalFinalConstructRelease();
if (hRes == S_OK)
{
hRes = p->QueryInterface(riid, ppv);
_Analysis_assume_(hRes == S_OK || FAILED(hRes));
}
if (hRes != S_OK)
delete p;
}
return hRes;
}
};
我们看到CComCreator,T1在DECLARE_AGGREGATABLE中无非就是CComObject或CComAggObject,说明T1是指定了它的聚合模式。如果T1是CComObject,那么就有
CComObject* p = new CComObject(pv);
如果是T1是CComAggObject,那么
CComAggObject* p = new CComAggObject(pv);
事实上,CComObject中拿到了pv根本没有用上,因为它不存在外部对象。pv只对于CComAggObject才是有效的。
CComCreator2的实现则显得非常鸡贼:
template <class T1, class T2>
class CComCreator2
{
public:
static HRESULT WINAPI CreateInstance(
_In_opt_ void* pv,
_In_ REFIID riid,
_COM_Outptr_ LPVOID* ppv)
{
ATLASSERT(ppv != NULL);
return (pv == NULL) ?
T1::CreateInstance(NULL, riid, ppv) :
T2::CreateInstance(pv, riid, ppv);
}
};
它其实是一个选择器,根据是否有外部对象pv,来决定是调用哪一个的CreateInstance。
在:
#define DECLARE_AGGREGATABLE(x) public:\
typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
定义了DECLARE_AGGREGATABLE的情况下,T1=CComCreator<CComObject>,T2=CComCreator<CComAggObject>,说明存在pv,则用T1创建CComObject,否则用T2创建CComAggObject。
除了这个宏,ATL还有另外几个宏DECLARE_NOT_AGGREGATABLE, DECLARE_ONLY_AGGREGATABLE, DECLARE_POLY_AGGREGATABLE:
#define DECLARE_NOT_AGGREGATABLE(x) public:\
typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
#define DECLARE_ONLY_AGGREGATABLE(x) public:\
typedef ATL::CComCreator2< ATL::CComFailCreator<E_FAIL>, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
#define DECLARE_POLY_AGGREGATABLE(x) public:\
typedef ATL::CComCreator< ATL::CComPolyObject< x > > _CreatorClass;
其中CComFailCreator实现为无论如何都会失败的Creator,并返回HRESULT T:
template <HRESULT hr>
class CComFailCreator
{
public:
static _Always_(_Post_satisfies_(return == hr || return == E_POINTER))
HRESULT WINAPI CreateInstance(
_In_opt_ void*,
_In_ REFIID,
_COM_Outptr_result_maybenull_ LPVOID* ppv)
{
if (ppv == NULL)
return E_POINTER;
*ppv = NULL;
return hr;
}
};
通过以上分析,我们就知道了,CComCoClass需要和DECLARE_AGGREGATABLE, DECLARE_NOT_AGGREGATABLE, DECLARE_ONLY_AGGREGATABLE, DECLARE_POLY_AGGREGATABLE其中一个一起使用,调用它的CreateInstance方法创建对象。它们的区别在于:
- DECLARE_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject;否则创建CComObject
- DECLARE_NOT_AGGREGATABLE: 如果有外部对象pv,创建失败;否则创建CComAggObject
- DECLARE_ONLY_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject,否则创建失败。
- DECLARE_POLY_AGGREGATABLE: 创建CComPolyObject
以上4个宏,指定的就是创建COM对象的模型。
就这样,我们便可以通过CComCoClass来为CYourClass添加一个标准的创建函数了。
下一篇将讲类厂及如何用COM API来创建COM对象。
文章介绍了COM对象的传统创建方式及其问题,引出创建函数的设计,详细解析了ATL中CreateInstance的实现,包括CComObjectRoot和CComCoClass的作用,以及DECLARE_AGGREGATABLE等宏的用法,讨论了不同聚合模型的创建策略。
&spm=1001.2101.3001.5002&articleId=129554624&d=1&t=3&u=03acbeceb68747218924439fbae21d27)
1100

被折叠的 条评论
为什么被折叠?



