特别说明 —— 该系列文章皆转自知乎作者 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,有以下作用:
- 决定着引用计数如何增加;
- 如何进行同步(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本身)的引用计数。所以我们要分两部分来看:
- CComAggObject的AddRef和Release,可以直接调用InternalAddRef和InternalRelease,因为它更改的是自己本身的计数;
- 持有的对象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点需要注意:
- 外部对象(CComAggObject)的QueryInterface,取出的其实是CYourClass中的接口,所以它实际上是转调了CYourClass::_InternalQueryInterface。
- 如果外部对象请求的接口是IUnknown,它必须返回自己。(原因请看上一篇文章QueryInterface第6点说明)
- 如果用户通过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对象。