COM编程攻略(三 硬核ATL实现IUnknown代码解析)

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



前言

由于单单一个IUnknown过于空洞,它没有存在任何实现,这意味着COM编程的门槛变得很高。在编写任何COM对象之前,开发者需要花很多力气处理AddRef, Release, QueryInterface的基本实现,费时又费力。

因此,微软推出了一套模板库,称为ATL。它意在提供对COM组件默认的实现,帮助开发者抽离底层实现,让开发者能够专注自己的业务需求。


一、IUnknown的实现架构

对于IUnknown而言,哪些是底层实现,哪些是业务需求呢?

底层实现:AddRef(), Release()。它是最通用的逻辑,所有IUnknown对象都(可以)有相同的实现。

业务需求:QueryInterface()。说到头,开发者自己的业务代码,其实就是实现各个接口,因此,一个实例可以转化为怎样的接口,是由开发者来自己实现的。

为此,微软提供了一套模板,来完成加减引用,提供了一套宏,来方便进行QueryInterface。

在直接搬出代码前,我们先来考虑一下这套模板的结构。如果我来设计一套这样的模板,那么至少它是有三层的。我们用A->B来表示,A继承于B,那么有:

Wrapper -> YourClass -> Internal

从最底层说起。Internal是所有AddRef, Release, QueryInterface的基础实现,可想而知,任何IUnknown调用这3个方法,最终都会走进Internal基类中。

YourClass是指的业务代码,也就是客户真正关心的部分。YourClass应当继承于IUnknown,并且它需要指定QueryInterface规则,这通常是一个Map的形式。在YourClass中把能够转换的接口和IID添加到Map,提供给Internal来完成QueryInterface操作。

例如:YourClass可能实现为:

class YourClass : public Internal, public Interface1, public Interface2
{
 ...
};

最外面一层Wrapper,仅仅是简单地将AddRef, Release, QueryInterface传递给Internal::InternalAddRef, Internal::InternalRelease, Internal::InternalQueryInterface,其原因是IUnknown是在YourClass被继承,所以Internal并不知道自己实现的AddRef, Release, QueryInterface是它子类的接口函数,所以需要由IUnknown的子类,也就是YourClass的子类Wrapper来转发。
在这里插入图片描述以上结构,客户只需要专注于实现YourClass,不需要关心Wrapper和Internal。

二、ATL中的IUnknown实现

在这里插入图片描述
以上是微软给出的基础设施图:
Fundamentals of ATL COM Objects

Note:该图显示CComObject从CYourClass派生而来,而CComAggObject和CComPolyObject包含CYourClass作为成员变量。

顶层的CComObject<>, CComAggObject<>,就是我们的Wrapper。它们的区别在于是采用继承还是聚合模型(不记得的话可以看上一篇文章)。

中间的CYourClass就是客户自己的类,也就是上文中的YourClass。

底层的CComObjectRoot,也就是我们刚刚说到的Internal。

按照MSDN说明,所有的CYourClass都要继承CComObjectRoot。在atlcom.h头文件中我们可以看到:

typedef CComObjectRootEx CComObjectRoot;

原来CComObjectRoot是CComObjectRootEx的一个别名。

1. InternalAddRef和InternalRelease的实现

那么我们再看看CComObjectRootEx:

template <class ThreadModel>
class CComObjectRootEx : 
	public CComObjectRootBase
{
public:
	typedef ThreadModel _ThreadModel;
	typedef typename _ThreadModel::AutoCriticalSection _CritSec;
	typedef typename _ThreadModel::AutoDeleteCriticalSection _AutoDelCritSec;
	typedef CComObjectLockT<_ThreadModel> ObjectLock;

	~CComObjectRootEx() 
	{
	}

	ULONG InternalAddRef()
	{
		ATLASSUME(m_dwRef != -1L);
		return _ThreadModel::Increment(&m_dwRef);
	}
	ULONG InternalRelease()
	{
#ifdef _DEBUG
		LONG nRef = _ThreadModel::Decrement(&m_dwRef);
		if (nRef < -(LONG_MAX / 2))
		{
			ATLASSERT(0 && _T("Release called on a pointer that has already been released"));
		}
		return nRef;
#else
		return _ThreadModel::Decrement(&m_dwRef);
#endif
	}

	HRESULT _AtlInitialConstruct()
	{
		return m_critsec.Init();
	}
	void Lock() 
	{
		m_critsec.Lock();
	}
	void Unlock() 
	{
		m_critsec.Unlock();
	}
private:
	_AutoDelCritSec m_critsec;
};

这个类非常有意思。首先我们关注InternalAddRef和InternalRelease,它调用的是_ThreadModel中的静态方法Increment和Decrement。_ThreadModel正是CComObjectRootEx的模板参数。

参考

typedef CComObjectRootEx CComObjectRoot;

我们便知道,CComObjectRoot的InternalAddRef和InternalRelease调用的分别是CComObjectThreadModel::Increment和CComObjectThreadModel::Decrement。

我们可以在atlbase.h中找到CComObjectThreadModel:

typedef CComSingleThreadModel CComObjectThreadModel;
class CComSingleThreadModel
{
public:
	static ULONG WINAPI Increment(_Inout_ LPLONG p) throw()
	{
		return ++(*p);
	}
	static ULONG WINAPI Decrement(_Inout_ LPLONG p) throw()
	{
		return --(*p);
	}
	typedef CComFakeCriticalSection AutoCriticalSection;
	typedef CComFakeCriticalSection AutoDeleteCriticalSection;
	typedef CComFakeCriticalSection CriticalSection;
	typedef CComSingleThreadModel ThreadModelNoCS;
};

看到这儿一目了然,原来CComObjectThreadModel其实就是单线程模型(CComSingleThreadModel),其AddRef和Release就是简单的++和–操作。它不需要考虑此对象会在多线程中进行,所以不需要原子操作。

同理,看见其

typedef CComFakeCriticalSection AutoDeleteCriticalSection;

CComObjectRoot中的成员m_critsec类型为_AutoDelCritSec,那么也就是CComSingleThreadModel::CComFakeCriticalSection,它实现在atlcore.h中:

class CComFakeCriticalSection
{
public:
	HRESULT Lock() throw()
	{
		return S_OK;
	}
	HRESULT Unlock() throw()
	{
		return S_OK;
	}
	HRESULT Init() throw()
	{
		return S_OK;
	}
	HRESULT Term() throw()
	{
		return S_OK;
	}
};

因此,CComObjectRoot中的Lock()和Unlock(),实际上调用的是CComFakeCriticalSection的Lock()和Unlock(),而它什么都没有做,原因是它只跑在同一个线程,不需要进入关键段,不需要进行同步操作。

看到这里我们明白了,CComObjectRootEx中的ThreadModel,有以下作用:

  1. 决定着引用计数如何增加;
  2. 如何进行同步(Lock(), Unlock());

既然CComObjectRoot被默认定义为CComObjectRootEx,而CComObjectThreadModel又是CComSingleThreadModel,所以我们可以认为,如果一个COM对象是跑在单线程下的,那么它就可以直接继承CComObjectRoot。

除了CComSingleThreadModel之外,对应地多线程的模型也实现在了atlbase.h:

class CComMultiThreadModel
{
public:
	static ULONG WINAPI Increment(_Inout_ LPLONG p) throw()
	{
		return ::InterlockedIncrement(p);
	}
	static ULONG WINAPI Decrement(_Inout_ LPLONG p) throw()
	{
		return ::InterlockedDecrement(p);
	}
	typedef CComAutoCriticalSection AutoCriticalSection;
	typedef CComAutoDeleteCriticalSection AutoDeleteCriticalSection;
	typedef CComCriticalSection CriticalSection;
	typedef CComMultiThreadModelNoCS ThreadModelNoCS;
};

这种情况下,任何内存敏感操作,都需要考虑多线程。

在Debug环境下,Release会运行如下代码:

if (nRef < -(LONG_MAX / 2))
{
	ATLASSERT(0 && _T("Release called on a pointer that has already been released"));
}

其原因上一篇文章也讲过。nRef是一个ULONG类型,如果它本身从0开始减,那么会得到一个很大的ULONG(0xFFFFFF…FF)。所以Debug下ATL认为,只要你的ULONG大小反向溢出了(-(LONG_MAX >> 2)),那么则弹出一个警告。

以上便是InternalAddRef和InternalRelease的实现,它调用的是ThreadModel中的Increment和Decrement方法。是简单++、–还是原子操作,取决于这个ThreadModel是CComSingleThreadModel还是CComMultiThreadModel。

2. InternalQueryInterface的实现

我们可以发现,CComObjectRootEx继承于CComObjectRootBase。CComObjectRootBase中实现了一个叫做InternalQueryInterface的方法。

ATL中通过一个数组来记录每个IID和对应的指针相对于this的偏移(用于类型转换),它定义在了atlbase.h中:

struct _ATL_INTMAP_ENTRY
{
	const IID* piid;       // the interface id (IID)
	DWORD_PTR dw;
	_ATL_CREATORARGFUNC* pFunc; //NULL:end, 1:offset, n:ptr
};

同时,ATL定义了一组宏,允许开发者在CYourClass中把能够转换的接口放入上面这个结构体数组中:

#define BEGIN_COM_MAP(x) public: \
	typedef x _ComMapClass; \
	static HRESULT WINAPI _Cache(_In_ void* pv, _In_ REFIID iid, _COM_Outptr_result_maybenull_ void** ppvObject, _In_ DWORD_PTR dw) throw()\
	{\
		_ComMapClass* p = (_ComMapClass*)pv;\
		p->Lock();\
		HRESULT hRes = E_FAIL; \
		__try \
		{ \
			hRes = ATL::CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);\
		} \
		__finally \
		{ \
			p->Unlock();\
		} \
		return hRes;\
	}\
	IUnknown* _GetRawUnknown() throw() \
	{ ATLASSERT(_GetEntries()[0].pFunc == _ATL_SIMPLEMAPENTRY); return (IUnknown*)((INT_PTR)this+_GetEntries()->dw); } \
	_ATL_DECLARE_GET_UNKNOWN(x)\
	HRESULT _InternalQueryInterface( \
		_In_ REFIID iid, \
		_COM_Outptr_ void** ppvObject) throw() \
	{ \
		return InternalQueryInterface(this, _GetEntries(), iid, ppvObject); \
	} \
	const static ATL::_ATL_INTMAP_ENTRY* WINAPI _GetEntries() throw() { \
	static const ATL::_ATL_INTMAP_ENTRY _entries[] = { DEBUG_QI_ENTRY(x)

#define COM_INTERFACE_ENTRY(x)\
	{&_ATL_IIDOF(x), \
	offsetofclass(x, _ComMapClass), \
	_ATL_SIMPLEMAPENTRY},

#define END_COM_MAP() \
	__if_exists(_GetAttrEntries) {{NULL, (DWORD_PTR)_GetAttrEntries, _ChainAttr }, }\
	{NULL, 0, 0}}; return _entries;} \
	virtual ULONG STDMETHODCALLTYPE AddRef(void) throw() = 0; \
	virtual ULONG STDMETHODCALLTYPE Release(void) throw() = 0; \
	STDMETHOD(QueryInterface)( \
		REFIID, \
		_COM_Outptr_ void**) throw() = 0;

用户可以在CYourClass中写出这样的代码:

BEGIN_COM_MAP(CComClassFactory2<license>)
	COM_INTERFACE_ENTRY(IClassFactory)
	COM_INTERFACE_ENTRY(IClassFactory2)
END_COM_MAP()

虽然BEGIN_COM_MAP这3个宏看上去很复杂,但是其实它做的事情很简单,BEGIN_COM_MAP在CYourClass中定义了一个这样的方法和静态变量:

HRESULT _InternalQueryInterface(
	_In_ REFIID iid,
	_COM_Outptr_ void** ppvObject) throw()
{
	return InternalQueryInterface(this, _GetEntries(), iid, ppvObject);
}
const static ATL::_ATL_INTMAP_ENTRY* WINAPI _GetEntries() throw() {
static const ATL::_ATL_INTMAP_ENTRY _entries[] = { DEBUG_QI_ENTRY(x)

首先它增加了一个_InternalQueryInterface来转调基类CComObjectRootBase::InternalQueryInterface。接下来它实现了里面的_GetEntries(),我们看到_GetEntries()只写了一部分,剩下的部分由COM_INTERFACE_ENTRY来补充:

{&_ATL_IIDOF(x), offsetofclass(x, _ComMapClass), _ATL_SIMPLEMAPENTRY},

COM_INTERFACE_ENTRY获取了客户定义的类的IID,以及相对于客户类(CYourClass)中的偏移,塞进了_ATL_INTMAP_ENTRY _entries[]数组中,最后用END_COM_MAP宏,把_GetEntries()补全,并加上AddRef和Release的纯虚函数签名,表示一旦用了这些宏,客户必须实现AddRef和Release。

简单来说,BEGIN_COM_MAP这一系列函数,其实就是为CYourClass增加了_InternalQueryInterface的实现,并且填充了_ATL_INTMAP_ENTRY数组。我们的结构图可以相应调整一下:
在这里插入图片描述
InternalQueryInterface在CComObjectRootBase中的实现很简单:

static HRESULT WINAPI InternalQueryInterface(
		_Inout_ void* pThis,
		_In_ const _ATL_INTMAP_ENTRY* pEntries, 		
		_In_ REFIID iid, 
		_COM_Outptr_ void** ppvObject)
{
	return AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject);
}

以上是简化后的代码,它交个Atl的一个库函数去取出ppvObject。其原理无非就是判断IID,然后返回pThis+偏移值,具体的实现代码大家就自己去看看吧。

以上便是QueryInterface在ATL中的实现。

3. Wrapper层的实现

ATL基础设施最外层的包装类,主要介绍2个: CComObject和CComAggObject。

CComObject表示一个继承关系的COM对象,它实现在atlcom.h:

template <class Base>
class CComObject : 
	public Base
{
public:
	typedef Base _BaseClass;
	CComObject(_In_opt_ void* = NULL)
	{
		_pAtlModule->Lock();
	}
	// Set refcount to -(LONG_MAX/2) to protect destruction and 
	// also catch mismatched Release in debug builds
	virtual ~CComObject()
	{
		m_dwRef = -(LONG_MAX/2);
		FinalRelease();
#if defined(_ATL_DEBUG_INTERFACES) && !defined(_ATL_STATIC_LIB_IMPL)
		_AtlDebugInterfacesModule.DeleteNonAddRefThunk(_GetRawUnknown());
#endif
		_pAtlModule->Unlock();
	}
	//If InternalAddRef or InternalRelease is undefined then your class
	//doesn't derive from CComObjectRoot
	STDMETHOD_(ULONG, AddRef)() 
	{
		return InternalAddRef();
	}
	STDMETHOD_(ULONG, Release)()
	{
		ULONG l = InternalRelease();
		if (l == 0)
		{
			// Lock the module to avoid DLL unload when destruction of member variables take a long time
			ModuleLockHelper lock;
			delete this;
		}
		return l;
	}
	//if _InternalQueryInterface is undefined then you forgot BEGIN_COM_MAP
	STDMETHOD(QueryInterface)(
		REFIID iid, 
		_COM_Outptr_ void** ppvObject) throw()
	{
		return _InternalQueryInterface(iid, ppvObject);
	}
	template <class Q>
	HRESULT STDMETHODCALLTYPE QueryInterface(
		_COM_Outptr_ Q** pp) throw()
	{
		return QueryInterface(__uuidof(Q), (void**)pp);
	}

	static HRESULT WINAPI CreateInstance(_COM_Outptr_ CComObject<Base>** pp) throw();
};

先观察到:

template <class Base>
class CComObject : 
	public Base

微软规定CYourClass要被CComObject(或者其它Wrapper)包围,变成CComObject,这也就等效于class CComObject : public CYourClass,CComObject中的所有方法将会被添加到CYourClass中,实现一个包装(Wrapper)。

如之前我们说的那样,AddRef就是简单地调用一下InternalAddRef,Release也是转调一下InternalRelease,如果计数为0,则删除自己。QueryInterface则调用由BEGIN_COM_MAP在CYourClass中的_InternalQueryInterface,十分简单。

CComAggObject表示一个聚合(组合)关系的COM对象:

template <class contained>
class CComAggObject :
	public IUnknown,
	public CComObjectRootEx< typename contained::_ThreadModel::ThreadModelNoCS >
{
public:
	typedef contained _BaseClass;
	CComAggObject(_In_opt_ void* pv) : 
		m_contained(pv)
	{
		_pAtlModule->Lock();
	}
	HRESULT _AtlInitialConstruct()
	{
		HRESULT hr = m_contained._AtlInitialConstruct();
		if (SUCCEEDED(hr))
		{
			hr = CComObjectRootEx< typename contained::_ThreadModel::ThreadModelNoCS >::_AtlInitialConstruct();
		}
		return hr;
	}
	//If you get a message that this call is ambiguous then you need to
	// override it in your class and call each base class' version of this
	HRESULT FinalConstruct()
	{
		CComObjectRootEx<contained::_ThreadModel::ThreadModelNoCS>::FinalConstruct();
		return m_contained.FinalConstruct();
	}
	void FinalRelease()
	{
		CComObjectRootEx<contained::_ThreadModel::ThreadModelNoCS>::FinalRelease();
		m_contained.FinalRelease();
	}
	// Set refcount to -(LONG_MAX/2) to protect destruction and 
	// also catch mismatched Release in debug builds
	virtual ~CComAggObject()
	{
		m_dwRef = -(LONG_MAX/2);
		FinalRelease();
#if defined(_ATL_DEBUG_INTERFACES) && !defined(_ATL_STATIC_LIB_IMPL)
		_AtlDebugInterfacesModule.DeleteNonAddRefThunk(this);
#endif
		_pAtlModule->Unlock();
	}

	STDMETHOD_(ULONG, AddRef)() 
	{
		return InternalAddRef();
	}
	STDMETHOD_(ULONG, Release)()
	{
		ULONG l = InternalRelease();
		if (l == 0)
		{
			ModuleLockHelper lock;
			delete this;
		}
		return l;
	}
	STDMETHOD(QueryInterface)(
		_In_ REFIID iid, 
		_COM_Outptr_ void** ppvObject)
	{
		ATLASSERT(ppvObject != NULL);
		if (ppvObject == NULL)
			return E_POINTER;
		*ppvObject = NULL;

		HRESULT hRes = S_OK;
		if (InlineIsEqualUnknown(iid))
		{
			*ppvObject = (void*)(IUnknown*)this;
			AddRef();
		}
		else
			hRes = m_contained._InternalQueryInterface(iid, ppvObject);
		return hRes;
	}
	template <class Q>
	HRESULT STDMETHODCALLTYPE QueryInterface(_COM_Outptr_ Q** pp)
	{
		return QueryInterface(__uuidof(Q), (void**)pp);
	}
	CComContainedObject<contained> m_contained;
};

它本身是一个IUnknown对象,它持有一个CYourClass实例。我们的所有QueryInterface将会转发给CYourClass的_InternalQueryInterface (注意:CYourClass没有QueryInterface,只有由BEGIN_COM_MAP实现的_InternalQueryInterface)。而对于QI取出的CYourClass的对象的AddRef和Release,其实更改的是外层对象(也就是CComAggObject本身)的引用计数。所以我们要分两部分来看:

  1. CComAggObject的AddRef和Release,可以直接调用InternalAddRef和InternalRelease,因为它更改的是自己本身的计数;
  2. 持有的对象CYourClass的AddRef和Release,调用的是外层CComAggObject的AddRef和Release。我们可以看到,被持有的CYourClass被模板CComContainedObject包围起来了:
template <class Base> //Base must be derived from CComObjectRoot
class CComContainedObject : 
	public Base
{
public:
	typedef Base _BaseClass;
	CComContainedObject(_In_opt_ void* pv) 
	{
		m_pOuterUnknown = (IUnknown*)pv;
	}

	STDMETHOD_(ULONG, AddRef)() throw() 
	{
		return OuterAddRef();
	}
	STDMETHOD_(ULONG, Release)() throw() 
	{
		return OuterRelease();
	}
	STDMETHOD(QueryInterface)(
		REFIID iid, 
		_COM_Outptr_ void** ppvObject) throw()
	{
		return OuterQueryInterface(iid, ppvObject);
	}
	template <class Q>
	HRESULT STDMETHODCALLTYPE QueryInterface(
		_COM_Outptr_ Q** pp)
	{
		return QueryInterface(__uuidof(Q), (void**)pp);
	}
};

上面源码中的Base,指的就是CYourClass。在对它调用AddRef和Release的时候,其实调用的是OuterAddRef和OuterRelease。CComContainedObject在构造的时候会传入一个外部对象,也就是CComAggObject对象,然后保存为成员m_pOuterUnknown,在基类CComObjectRootBase中,OuterAddRef和OuterRelease实现非常简单:

ULONG OuterAddRef()
{
	return m_pOuterUnknown->AddRef();
}
ULONG OuterRelease()
{
	return m_pOuterUnknown->Release();
}

这使我们可以重新认识CComObjectRootBase的角色:它既可以支持继承关系COM对象,又可以支持聚合关系COM对象。

在聚合模型的QueryInterface中,同样也有2点需要注意:

  1. 外部对象(CComAggObject)的QueryInterface,取出的其实是CYourClass中的接口,所以它实际上是转调了CYourClass::_InternalQueryInterface。
  2. 如果外部对象请求的接口是IUnknown,它必须返回自己。(原因请看上一篇文章QueryInterface第6点说明)
  3. 如果用户通过CComAggObject::QueryInterface拿到了CYourClass的某个接口,接下来再调用QueryInterface接口,那么它其实应当转调CComAggObject::QueryInterface。

第2点实现在了CComAggObject::QueryInterface中:

STDMETHOD(QueryInterface)(
	_In_ REFIID iid, 
	_COM_Outptr_ void** ppvObject)
{
	ATLASSERT(ppvObject != NULL);
	if (ppvObject == NULL)
		return E_POINTER;
	*ppvObject = NULL;

	HRESULT hRes = S_OK;
	if (InlineIsEqualUnknown(iid)) // 如果请求IUnknown接口,则返回自己
	{
		*ppvObject = (void*)(IUnknown*)this;
		AddRef();
	}
	else // 否则转调
		hRes = m_contained._InternalQueryInterface(iid, ppvObject);
	return hRes;
}

第3点实现在了CComContainedObject::QueryInterface中,它调用了CComObjectRootBase::OuterQueryInterface:

HRESULT OuterQueryInterface(
	_In_ REFIID iid, 
	_COM_Outptr_ void** ppvObject)
{
	return m_pOuterUnknown->QueryInterface(iid, ppvObject);
}

它的实现和CComObjectRootBase::OuterAddRef,CComObjectRootBase::OuterRelease如出一辙。

通过外部对象(CComAggObject)和实际包含的对象(CYourClass)相互转调,ATL实现了聚合下的COM对象。ATL甚至还提供了一个既可以继承又可以聚合的CComPolyObject类,原理和上面一致,就不啰嗦了:
CComPolyObject Class


以上便是ATL实现IUnknown接口的大部分细节。

至此,我们已经知道了IUnknown的几种实现方式,ATL对于它的实现原理,下一篇我将讲述COM对象如何被创建,以及ATL如何创建一个COM对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值