COM编程攻略(四 COM对象创建的原理及ATL实现)

文章介绍了COM对象的传统创建方式及其问题,引出创建函数的设计,详细解析了ATL中CreateInstance的实现,包括CComObjectRoot和CComCoClass的作用,以及DECLARE_AGGREGATABLE等宏的用法,讨论了不同聚合模型的创建策略。

特别说明 —— 该系列文章皆转自知乎作者 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对象创建出来。不过,它有几个问题:

  1. new运算符跨越了边界。operator new的行为在A中被实现,例如虚表指针是在哪里,是否在结构中有额外的数据,而ClassB实现在B.dll。除非我们使用兼容的编译器,生成相同的ClassB结构,否则可能会引起一些二进制兼容问题。
  2. new对象和释放对象可能不在同一个模块。尤其是COM对象,分配在堆上面的COM对象,是通过Release()来释放自己的,如果在另外的模块new出来,在自己模块释放,在某些情况下(如mt, md不一致),会出现问题。为了解决这个问题,我们需要在模块B.dll中暴露一个创建对象的函数。
  3. 一个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方法创建对象。它们的区别在于:

  1. DECLARE_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject;否则创建CComObject
  2. DECLARE_NOT_AGGREGATABLE: 如果有外部对象pv,创建失败;否则创建CComAggObject
  3. DECLARE_ONLY_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject,否则创建失败。
  4. DECLARE_POLY_AGGREGATABLE: 创建CComPolyObject

以上4个宏,指定的就是创建COM对象的模型。

就这样,我们便可以通过CComCoClass来为CYourClass添加一个标准的创建函数了。

下一篇将讲类厂及如何用COM API来创建COM对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值