void f(void) { IUnknown *pUnk = 0; // 调用 HRESULT hr = GetSomeObject(&pUnk); if (SUCCEEDED(hr)) { // 使用 UseSomeObject(pUnk); // 释放 pUnk->Release(); } }这个模式在COM程序员心中是如此根深蒂固,以至于他们常常不写实际使用指针的语句,而是先在代码块末尾敲入Release语句。这很像C程序员使用switch语句时的条件反射一样,先敲入break再说。
void f(void) { IUnknown *rgpUnk[3]; HRESULT hr = GetObject(rgpUnk); if (SUCCEEDED(hr)) { hr = GetObject(rgpUnk + 1); if (SUCCEEDED(hr)) { hr = GetObject(rgpUnk + 2); if (SUCCEEDED(hr)) { UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]); rgpUnk[2]->Release(); } rgpUnk[1]->Release(); } rgpUnk[0]->Release(); } }像这样的语句常常促使程序员将TAB键设置成一个或两个空格,甚至情愿使用大一点的显示器。但事情并不总是按你想象的那样,由于种种原因项目团队中的COM组件编程人员往往得不到 所想的硬件支持,而且在公司确定关于TAB键的使用标准之前,程序员常常求助于使用有很大争议但仍然有效的“GOTO”语句:
void f(void) { IUnknown *rgpUnk[3]; ZeroMemory(rgpUnk, sizeof(rgpUnk)); if (FAILED(GetObject(rgpUnk))) goto cleanup; if (FAILED(GetObject(rgpUnk+1))) goto cleanup; if (FAILED(GetObject(rgpUnk+2))) goto cleanup; UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]); cleanup: if (rgpUnk[0]) rgpUnk[0]->Release(); if (rgpUnk[1]) rgpUnk[1]->Release(); if (rgpUnk[2]) rgpUnk[2]->Release(); }这样的代码虽然不那么专业,但至少减少了屏幕的水平滚动。
void f(void) { IUnknown *rgpUnk[3]; ZeroMemory(rgpUnk, sizeof(rgpUnk)); __try { if (FAILED(GetObject(rgpUnk))) leave; if (FAILED(GetObject(rgpUnk+1))) leave; if (FAILED(GetObject(rgpUnk+2))) leave; UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]); } __finally { if (rgpUnk[0]) rgpUnk[0]->Release(); if (rgpUnk[1]) rgpUnk[1]->Release(); if (rgpUnk[2]) rgpUnk[2]->Release(); }可惜Win32 SHE在C++中的表现并不如想象得那么好。较好的方法是使用内建的C++异常处理模型,同时停止使用没有加工过的指针。标准C++库有一个类:auto_ptr,在其析构函数中定 死了一个操作指针的delete调用(即使在出现异常时也能保证调用)。与之类似,ATL有一个COM智能指针,CComPtr,它的析构函数会正确调用Release。
CComPtr<IUnknown> unk; CComPtr<IClassFactory> cf;缺省的构造函数将这个原始指针数据成员初始化为NULL。智能指针也有构造函数,它的参数要么是原始指针,要么是相同类型的智能参数。不论哪种情况,智能指针都调用AddRef控制引用。CComPtr的赋值操作符 既可以处理原始指针,也可以处理智能指针,并且在调用新分配指针的AddRef之前自动释放保存的指针。最重要的是,CComPtr的析构函数释放保存的接口(如果非空)。请看下列代码:
void f(IUnknown *pUnk1, IUnknown *pUnk2) { // 如果非空,构造函数调用pUnk1的AddRef CComPtr unk1(pUnk1); // 如果非空,构造函数调用unk1.p的AddRef CComPtr unk2 = unk1; // 如果非空,operator= 调用unk1.p的Release并且 //如果非空,调用unk2.p的AddRef unk1 = unk2; //如果非空,析构函数释放unk1 和 unk2 }除了正确实现COM的AddRef 和 Release规则之外,CComPtr还允许实现原始和智能指针的透明操作,参见 附表二 所示。也就是说下面的代码按照你所想象的方式运行:
void f(IUnknown *pUnkCO) { CComPtr cf; HRESULT hr; // 使用操作符 & 获得对 &cf.p 的存取 hr = pUnkCO->QueryInterface(IID_IClassFactory,(void**)&cf); if (FAILED(hr)) throw hr; CComPtr unk; // 操作符 -> 获得对cf.p的存取 // 操作符 & 获得对 &unk.p的存取 hr = cf->CreateInstance(0, IID_IUnknown, (void**)&unk); if (FAILED(hr)) throw hr; // 操作符 IUnknown * 返回 unk.p UseObject(unk); // 析构函数释放unk.p 和 cf.p }除了缺乏对Release的显式调用外,这段代码像是纯粹的COM代码。有了CComPtr类的武装,前面所遇到的麻烦问题顿时变得简单了:
void f(void) { CComPtr<IUnknown> rgpUnk[3]; if (FAILED(GetObject(&rgpUnk[0]))) return; if (FAILED(GetObject(&rgpUnk[1]))) return; if (FAILED(GetObject(&rgpUnk[2]))) return; UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]); }由于CComPtr对操作符重载用法的扩展,使得代码的编译和运行无懈可击。
CComQIPtr<IDataObject, &IID_IDataObject> do; CComQIPtr<IPersist, &IID_IPersist> p;CComQIPtr的优点是它有重载的构造函数和赋值操作符。同类版本(例如,接受相同类型的接口)仅仅AddRef右边的赋值/初始化操作。这实际上就是CComPtr的功能。异类版本(接受类型不一致的接口)正确调用QueryInterface来决定是否这个对象确实支持所请求的接口:
void f(IPersist *pPersist) { CComQIPtr<IPersist, &IID_IPersist> p; // 同类赋值 - AddRef''s p = pPersist; CComQIPtr<IDataObject, &IID_IDataObject> do; // 异类赋值 - QueryInterface''s do = pPersist; }在第二种赋值语句中,因为pPersist是非IDataObject *类型,但它是派生于IUnknown的接口指针,CComQIPtr通过pPersist调用QueryInterface来试图获得这个对象的IDataObject接口指针。如果QueryInterface调用成功,则此智能指针将含有作为结果的原始IDataObject指针。如果QueryInterface调用失败,则do.p将被置为null。如果QueryInterface返回的HRESULT值很重要,但又没有办法从赋值操作获得其值时,则必须显式调用QueryInterface。
CComPtr<IUnknown> unk;从功能上将它等同于
CComQIPtr<IUnknown, &IID_IUnknown> unk;前者正确。后者是错误的用法。如果你这样写了,C++编译器将提醒你改正。
void f(void) { IFoo *pFoo = 0; HRESULT hr = GetSomeObject(&pFoo); if (SUCCEEDED(hr)) { UseSomeObject(pFoo); pFoo->Release(); } }将它自然而然转换到使用CComPtr。
void f(void) { CComPtr<IFoo> pFoo = 0; HRESULT hr = GetSomeObject(&pFoo); if (SUCCEEDED(hr)) { UseSomeObject(pFoo); pFoo->Release(); } }注意CComPtr 和 CComQIPtr输出所有受控接口成员,包括AddRef和Release。可惜当客户端通过操作符->的结果调用Release时,智能指针很健忘 ,会二次调用构造函数中的Release。显然这是错误的,编译器和链接器也欣然接受了这个代码。如果你运气好的话,调试器会很快捕获到这个错误。
void f(IUnknown *pUnk) { CComPtr unk = pUnk; // 隐式调用操作符IUnknown *() CoLockObjectExternal(unk, TRUE, TRUE); }这段代码能正确运行,但是下面的代码也不会产生警告信息,编译正常通过:
HRESULT CFoo::Clone(IUnknown **ppUnk) { CComPtr unk; CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL, IID_IUnknown, (void **) &unk); // 隐式调用操作符IUnknown *() *ppUnk = unk; return S_OK; }在这种情况下,智能指针(unk)对原始值针**ppUnk的赋值触发了与前面代码段相同的强制类型转换。在第一个例子中,不需要用AddRef。在第二个例子中,必须要用AddRef。
interface IMessageSource : IUnknown { HRESULT GetNextMessage([out] OLECHAR **ppwsz); } interface IPager : IUnknown { HRESULT SendMessage([in] const OLECHAR *pwsz); } interface IPager2 : IPager { HRESULT SendUrgentMessage(void); }这些C++类定义实现了三个接口:
class CPager : public IMessageSource, public IPager2 { LONG m_dwRef; public: CPager(void) : m_dwRef(0) {} virtual ~CPager(void) {} STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetNextMessage(OLECHAR **ppwsz); STDMETHODIMP SendMessage(const COLECHAR * pwsz); STDMETHODIMP SendUrgentMessage(void); };如果在堆中创建对象(也就是说用new操作符在内部创建)并且只用单线程公寓(STA)模式运行,下面是合理的AddRef 和Release实现:
STDMETHODIMP_(ULONG) CPager::AddRef() { return ++m_dwRef; } STDMETHODIMP_(ULONG) CPager::Release(){ ULONG result = -m_dwRef; if (result == 0) delete this; return result; }如果输出的对象是以多线程公寓(MTA)模式运行,则++和--操作符就必须用Win32的原子增量和减量(Increment/Decrement)例程调用来代替:
STDMETHODIMP_(ULONG) CPager::AddRef() { return InterlockedIncrement(&m_dwRef); } STDMETHODIMP_(ULONG) CPager::Release(){ ULONG result = InterlockedDecrement(&m_dwRef); if (result == 0) delete this; return result; }无论哪一种线程模式,下面的QueryInterface实现都是正确的:
STDMETHODIMP CPager::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = (IMessageSource*)this; else if (riid == IID_IMessageSource) *ppv = (IMessageSource*)this; else if (riid == IID_IPager) *ppv = (IPager*)this; else if (riid == IID_IPager2) *ppv = (IPager2*)this; else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }QueryInterface的最后四行代码对所有的对象都一样。其余的部分则根据这个对象类型层次上的类不同而有所不同。
/D _ATL_SINGLE_THREADED来编译工程可以改变服务器缺省的线程模型,让它只支持一个基于STA的线程。它适合于进程外的且不创建自拥有线程的基于STA的服务器情况,当你用这个选项时,所有对ATL全局状态的存取将都是不加锁的,并发的。尽管此选项似乎很有效,但它实质上限制了ATL服务器只能是一个单线程的。
/D _ATL_APARTMENT_THREADED来编译工程可以改变服务器缺省的线程模型支持多个基于STA的线程。它适合于建立注册表项ThreadingModel=Apartment的进程内服务器。如果要创建基于STA的进程外服务器且还要建立附加的基于STA的线程,那么这个指令也是必须的。使用这个选项导致ATL用能安全存取线程的临界区来保护它的全局状态。
/D _ATL_FREE_THREADED可以创建与任何线程环境兼容的服务器。也就是说ATL的全局状态将在临界区中被锁定,并且每个对象将拥有它自己的私有临界区来保护它的实例状态。如果没有定义这些指令,则ATL头文件假设为使用_ATL_FREE_THREADED。
ATL类型定义 | _ATL_SINGLE_THREADED | _ATL_APARTMENT_THREADED | _ATL_FREE_THREADED |
CComGlobalsThreadModel | CComSingleThreadModel | CComMultiThreadModel | CComMultiThreadModel |
CComObjectThreadModel | CComSingleThreadModel | CComSingleThreadModel | CComMultiThreadModel |
只要给定了上述的线程模型类型层次,你就能将相应的参数化线程行为添加到任何COM类。请看下列代码:
参数化的线程 class CPager : public IPager { LONG m_dwRef; typedef CComObjectThreadModel _ThreadModel; _ThreadModel::CComAutoCriticalSection m_critsec; : : : : STDMETHODIMP_(ULONG) CPager::AddRef() { return _ThreadModel::Increment(&m_dwRef); } STDMETHODIMP_(ULONG) CPager::Release(){ ULONG res = _ThreadModel::Decrement(&m_dwRef); if (res == 0) delete this; return res; } STDMEHTHODIMP SendUrgentMessage() { // 保证只有一个线程 m_critsec.Lock(); // 实现任务 this->GenerateMessage(); this->WakeUpUser(); // 允许其它线程 m_critsec.Unlock(); return S_OK; } };使用缺省选项(_ATL_FREE_THREADED)的编译则将一个实临界区添加到对象,并执行Lock和Unlock方法将内联调用映射到EnterCriticalSection/LeaveCriticalSection API函数。同时,AddRef和Release方法将使用InterlockedIncrement/InterlockedDecrement来安全地改变这个对象的引用计数。
typedef CComObjectThreadModel _ThreadModel;细化到
typedef CComMultiThreadModelNoCS _ThreadModel;那么针对每一个对象,你不必付出CRITICAL_SECTION的开销(CComAutoCriticalSection 会映射到 CComFakeCriticalSection)就可以得到线程安全的AddRef和Release(将Increment 和 Decrement方法映射到InterlockedIncrement和InterlockedDecrement)。
union { long m_dwRef; IUnknown *m_pOuterUnknown; };根据使用这个类的实际方式,联合中的成员将被用于保存给定类实例的生命周期。大多数情况下要用到m_dwRef,m_pOuterUnknown只有在支持聚合或tear-offs时用到。CComObjectRootBase提供了OuterQueryInterface,OuterAddRef和OuterRelease方法,通过m_pOuterUnknown成员转发IUnknown请求。
static HRESULT WINAPI CComObjectRootBase::InternalQueryInterface(void *pThis, const _ATL_INTMAP_ENTRY *pEntries, REFIID riid, void **ppv);使用ATL实现IUnknown的每一个类必须制定一个接口映射来提供InternalQueryInterface。ATL的接口映射是IID/DWORD/函数指针数组,它指示当QueryInterface请求一个给定的IID时要采取什么样的行动。其类型都是_ATL_INTMAP_ENTRY。
struct _ATL_INTMAP_ENTRY { const IID* piid; // 接口ID (IID) DWORD dw; // 多用途值 HRESULT (*pFunc)(void*, REFIID, void**, DWORD); };
这个结构的第三个成员pFunc的取值有三种情况。如果pFunc等于常量_ATL_SIMPLEMAPENTRY,则结构成员dw为对象偏移量,这时不需要函数调用,并且InternalQueryInterface完成下列操作:
(*ppv = LPBYTE(pThis) + pEntries[n].dw)->AddRef();这个偏移量的初始化通常参照基类接口的偏移量。如果pFunc非空且不等于_ATL_SIMPLEMAPENTRY,则它指向的函数将被调用,将这个指针作为第一个参数传递给对象而第二个参数是多用途值dw。
return pEntries[n].pFunc(pThis, riid, ppv, pEntries[n].dw);这个接口映射的最后一个入口将使用pFunc值,指示映射的结束。 如果没有在映射中发现任何接口则InternalQueryInterface 会返回E_NOINTERFACE。 接口映射通常为ATL的接口映射宏。ATL提供17个不同的宏,它们支持大多数用于实现接口的普通技术(多继承,嵌套类,聚合或者tear-offs。)这些宏及其相应的原始代码请参见 附表三 。下面是一个使用CComObjectRootEx和接口映射实现IPager2 和IMessageSource类的例子:
class CPager : public IMessageSource, public IPager2, public CComObjectRootEx<CComMultiThreadModel>{ public: CPager(void) {} virtual ~CPager(void) {} BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(IMessageSource) COM_INTERFACE_ENTRY(IPager2) COM_INTERFACE_ENTRY(IPager) END_COM_MAP() STDMETHODIMP GetNextMessage(OLECHAR **ppwsz); STDMETHODIMP SendMessage(const COLECHAR * pwsz); STDMETHODIMP SendUrgentMessage(void); };前面的代码产生的接口映射如下:
{ &IID_IMessageSource, 0, _ATL_SIMPLEMAPENTRY }, { &IID_IPager2, 4, _ATL_SIMPLEMAPENTRY }, { &IID_IPager, 4, _ATL_SIMPLEMAPENTRY}, { 0, 0, 0 }在建立接口映射时,ATL假设映射中第一个入口是个简单映射入口并用它来满足对IID_IUnknown.的请求。 除了支持IUnknown外,ATL提供大量缺省的COM接口实现。ATL用一个简单的命名规范来为这些实现命名,它们大多数都是作为模板类来实现的,带有一个模板参数,而这些模板参数才是是既要实现的类。 一个简单的例子是IObjectWithSite接口,它一般用于为某个对象提供一个指向激活现场的指针。ATL为这个指针提供了一个缺省的实现:IObjectWithSiteImpl。此类提供了一个IObjectWithSite兼容的二进制签名并且实现了所有的IObjectWithSite方法。为了使用ATL内建的实现,你只需要添加基类实现(用适当的模板参数),然后在接口映射中添加一个入口输出经过QueryInterface实现的接口。 例如,为了使用ATL的IObjectWithSite实现,按照如下步骤来做:
class CPager : public CComObjectRootEx, public IPager, public IObjectWithSiteImpl { public: BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(IPager) COM_INTERFACE_ENTRY_IMPL(IObjectWithSite) END_INTERFACE_MAP() STDMETHODIMP SendMessage(const COLECHAR * pwsz); };由于使用了ATL内建的实现类,也就有了COM_INTERFACE_ENTRY_IMPL宏。之所以要用只个宏是因为许多ATL的缺省实现不从它们实现的接口派生。这样的话就导致标准的COM_ INTERFACE_ENTRY宏返回不正确的偏移量。例如,因为CPager不从IObjectWithSite派生,用于计算偏移量的强制类型转换就不会在对象中反映,而是用起始位置代替。 在这个例子中,IObjectWithSiteImpl没有基类。而是按照在IObjectWithSite中一样的顺序声明它的虚函数,产生全兼容的vtable(虚表)结构。ATL使用这个有点不可思议的技术,原因是它允许缺省实现支持接口的引用计数,这一点使用常规多继承技术是很难做到的。 IDispatchImpl也是一个常用的ATL缺省实现。这个类实现用于双接口的四个IDispatch方法,由你的类实现IDispatch::Invoke所不能完成的方法。不像大多数其它的ATL实现,这个类实际上是从一个COM接口派生的,有几个模板参数:
template < class T, // 双接口 const IID* piid, // 双接口IID const GUID* plibid, // 包含类型库TypeLib WORD wMajor = 1, // 类型库的版本 WORD wMinor = 0, //类型库的版本 class tihclass = CComTypeInfoHolder > class IDispatchImpl : public T { ... }; 假设两个接口是DIPager 和 DIMessageSource。这个类的使用如下: class CPager : public CComObjectRootEx<CComMultiThreadModel>, public IDispatchImpl<DIMessageSource, &IID_DIMessageSource, &LIBID_PagerLib>, public IDispatchImpl<DIPager, &IID_DIPager, &LIBID_PagerLib> { public: BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(DIMessageSource) COM_INTERFACE_ENTRY(DIPager) // 下一个接口指定DIPager为缺省 [default] COM_INTERFACE_ENTRY2(IDispatch, DIPager) END_INTERFACE_MAP() STDMETHODIMP SendMessage(BSTR pwsz); STDMETHODIMP GetNextMessage(BSTR *ppwsz); };ATL的第一个版本使用CComDualImpl名字,现在它只是IDispatchImpl预处理的一个别名,以便允许1.x版本和2.x版本的工程能在一起编译。
不要过分抽象
ATL最不直观的一个方面是你所定义和实现的C++类仍然是抽象基类。没错,在ATL的模板类和宏上辛苦了半天,却仍然得不到一个可以实例化的类。即使你从 CComObjectRootEx 派生,其结果同从一个或更多的ATL接口实现继承一样。从技术上讲,你的对象不提供 IUnknown 三个核心方法(QueryInterface,AddRef 和 Release)的实现。如果你检查现有ATL之前的 COM 实现,如果不是全部,那么也是大多数的方法实现并不在乎这个类是不是被用于COM聚合或tear-off,是不是被用于独立的对象或一个包含在内的数据成员,是不是要作为基于堆的对象或作为全局变量,以及是不是对象存在时,一直要保持服务器运行。为了允许最大限度的灵活性,所有这些方面分别通过ATL家族中的十个类属的 CComObject 之一来说明。参见下表:
类名 | 服务器是否加锁 | 是否代理IUnknown | 是否删除对象 | 备注 |
CComObject | Yes | No | Yes | 常规情况 |
CComObjectCached | Yes(在第二次AddRef之后) | No | Yes | 用于通过内部指针控制的对象 |
CComObjectNoLock | No | No | Yes | 用于不控制服务器运行的对象 |
CComObjectGlobal | Yes(在第一次AddRef之后) | No | No | 用于全程变量 |
CComObjectStack | No | No | No | 用于不能被增加引用计数的基于堆栈的变量 |
CComContainedObject | No | Yes | No | 用于MFC风格的嵌套类 |
CComAggObject | Yes | Yes | Yes | 仅用于聚合实现 |
CComPolyObject | Yes | Yes(如果聚合) | Yes | 用于聚合/非聚合实现 |
CComTearOffObject | No | Yes(仅用于QueryInterface) | Yes | 用于每次请求所创建的tear-offs |
CComCachedTearOffObject | No | Yes(通过第二个IUnknown) | Yes | 用于在第一次请求和缓存时所创建的tear-offs |
template class CComObjectNoLock : public Base { public: typedef Base _BaseClass; CComObjectNoLock(void* = NULL){} ~CComObjectNoLock() {m_dwRef = 1L; FinalRelease();} STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();} STDMETHOD_(ULONG, Release)() { ULONG l = InternalRelease(); if (l == 0) delete this; return l; } STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {return _InternalQueryInterface(iid, ppvObject);} }; template class CComObject : public Base { public: typedef Base _BaseClass; CComObject(void* = NULL) { _Module.Lock(); } ~CComObject() {m_dwRef = 1L; FinalRelease(); _Module.Unlock(); } STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();} STDMETHOD_(ULONG, Release)() { ULONG l = InternalRelease(); if (l == 0) delete this; return l; } STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {return _InternalQueryInterface(iid, ppvObject);} static HRESULT WINAPI CreateInstance(CComObject** pp); };它假设你的对象将在堆中分配,也就是说最终调用 Release 时,将触发这个对象的 delete 操作。CComObjectNoLock 假设你的对象不是可聚合的,并且在服务器运行时,对象并不一直存在(因此有后缀 NoLock)。
IPager *p = new CComObjectNoLock();CComObjectNoLock 类从 CPager 派生,并且添加了由 CPager 提供的使用 InternalQueryInterface,InternalAddRef 和 InternalRelease 的 QueryInterface,AddRef 和 Release实现。因为此对象是基于堆的,所以对 delete 的调用将发生在 CComObjectNoLock 类的 Release 实现中,此时InternalRelease 返回零。
输出你的类
实现了 CComObject ,你就有足够的条件用 C++ new 操作符创建 COM 对象。不过这样做没有什么实用价值,因为毕竟外部客户端使用 CoCreateInstance 或 CoGetClassObject 创建类实例。也就是说,你必须为每个外部类输出类对象。幸运的是ATL分别在它的 CComClassFactory 和 CComClassFactory2 类中提供了缺省的 IClassFactory 和 IClassFactory2接口实现。
CComClassFactory 不是模板驱动类,但其中有一个函数指针作为数据成员,使用这个函数可以创建对象。ATL提供了一个类模板家族,它们都有一个单独的静态方法 CreateInstance,由 Creators 调用,Creators 提供正确的语义来从 CComClassFactory 创建基于 CComObjectRoot 的对象。下面的这段代码展示了缺省的创建机制:CComCreator,它产生一个模板化的类实例,并用 ATL 中标准的 FinalConstruct 来顺序初始化对象。
ATL Creator template class CComCreator { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { HRESULT hRes = E_OUTOFMEMORY; T1* p = NULL; ATLTRY(p = new T1(pv)) if (p != NULL) { p->SetVoid(pv); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); p->InternalFinalConstructRelease(); if (hRes == S_OK) hRes = p->QueryInterface(riid, ppv); if (hRes != S_OK) delete p; } return hRes; } }; template class CComFailCreator { public: static HRESULT WINAPI CreateInstance(void*, REFIID, LPVOID*) { return hr; } }; template class CComCreator2 { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { HRESULT hRes = E_OUTOFMEMORY; if (pv == NULL) hRes = T1::CreateInstance(NULL, riid, ppv); else hRes = T2::CreateInstance(pv, riid, ppv); return hRes; } };因为 ATL 利用 Visual C++ 中的__declspec(novtable) 优化,所以在很大程度上依赖两层构造。declspec 取消掉了在抽象基类的构造函数中必须对 vptr 进行的初始化,因为抽象基类中的任何的 vptr 会在派生类中被重写。之所以要进行这种优化,是因为初始化从未被使用过的 vptr 毫无意义。另外,因为不需要为抽象基类分配vtable,从而减少了代码的大小。
DECLARE_PROTECT_FINAL_CONSTRUCT()这一行代码重新定义了类的 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 来增减引用计数,从而安全地传递可能调用 QueryInterface 的对象指针。
struct _ATL_OBJMAP_ENTRY { const CLSID* pclsid; HRESULT (*pfnUpdateRegistry)(BOOL bRegister); HRESULT (*pfnGetClassObject)(void* pv, REFIID riid, LPVOID* ppv); HRESULT (*pfnCreateInstance)(void* pv, REFIID riid, LPVOID* ppv); IUnknown* pCF; DWORD dwRegister; LPCTSTR (* pfnGetObjectDescription)(void); };pfnGetClassObject成员的调用是在第一次需要创建新的类对象时。这个函数被作为 Creator 函数(pfnCreateInstance)的第一个参数传递,并且返回的结果接口指针被缓存在pCF成员中。通过按需要创建类对象,而不是静态地实例化变量,就不再需要使用带虚函数的全局对象,使得基于 ATL 的工程不用C运行库就能进行链接。(在 DllMain / WinMain 以前,C运行时必须用来构造全局和静态变量。)
class CPager : public CComObjectRootEx, public CComCoClass, public IPager { public: BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(IPager) END_INTERFACE_MAP() STDMETHODIMP SendMessage(const OLECHAR * pwsz); };一旦你从CComCoClass派生,你的类就已经被添加到ATL Object Map中。ATL所提供的用来简化建立对象映射的宏很像接口映射宏。下面就是为多CLSID服务器建立的一个对象映射。
BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_Pager, CPager) OBJECT_ENTRY(CLSID_Laptop, CLaptop) END_OBJECT_MAP()这个代码建立了一个叫 ObjectMap 的 _ATL_OBJMAP_ENTRY 数组,初始化如下:
静态成员函数从 CComCoClass 派生,被隐含式定义。以上定义的对象映射一般通过使用 CComModule 的 Init 方法被传递到ATL:
_Module.Init(ObjectMap, hInstance);这个方法根据创建的服务器类型,在 DllMain 或 WinMain 中被调用。
DECLARE_NOT_AGGREGATABLE(CPager) DECLARE_ONLY_AGGREGATABLE(CPager) DECLARE_POLY_AGGREGATABLE(CPager)这些宏只是将 ATL Creator 定义成一个将被用于初始化对象映射的嵌套类型(CreatorClass)。前面两个宏是自解释的(它们禁止或需要聚合)。 第三个宏需要解释一下。缺省情况下,CComCoClass 使用 ATL 类创建机制,根据是否需要使用聚合来创建两个不同的类之一。如果不需要聚合,则创建新的 CComObject 实例。如果需要聚合,则创建新的CComAggObject实例。也就是说两个不同的 vtables 必须在可执行文件中出现。对照之下,DECLARE_POLY_ AGGREGATABLE 总是创建一个 CComPolyObject 实例,并根据对象是否聚合来初始化这个外部控制指针。亦即只要定义一个C++类,只需一个 vtable。这个技术的不足之处是:非聚合对象的每个实例必须为非代理 IUnknown 指针多用4个字节。不论哪种情况,支持聚合都不需要实际的编码,而只是在实例和代码大小之间作出取舍。
ATL和注册表
CComModule 提供了两个方法用于自注册:一个是RegisterServer,另外一个是 UnregisterServer。这两个方法使用传递到 Init 例程的对象映射来完成实际的工作。正像我前面所提到的那样,每一个对象映射入口都包含 pfnUpdateRegistry 函数指针,这个指针必须由类实现者提供。ATL最初的版本所提供的例程为 CLSID 自动添加标准注册入口,从而使缺省行为的实现很容易。可惜这些例程不具备很好的可扩展性,而且如果服务器的需求超过了正常 InprocServer32 入口所包含的内容的话,就必须自己用手工来编写注册代码。
随着组件种类(categories)和 AppIDs 概念的出现,几乎就再没有服务器能认可由ATL1.0提供的标准注册入口。在ATL1.1及以后的版本中,首选的自注册技术是使用注册脚本,它非常灵活。这个技术需要 IRegistrar 接口的COM实现,它既可以静态链接以降低依赖性,也可以用 CoCreateInstance 动态绑定来最小化代码尺寸。
注册脚本只是个文本文件,它列出必须为给定的 CLSID 添加什么入口。注册脚本文件默认的扩展名为RGS,并作为定制的 REGISTRY 类型资源被添加进可执行文件。注册脚本的语法十分简单,归纳起来为:
[NoRemove|ForceRemove|val] Name [ = s|d ''''Value''''] { ... 用于子键的脚本条目 }NoRemove 前缀表示在进行注销时不删除这个键。ForceRemove 前缀表示在写这个键之前删除当前的键和子键。Val 前缀表示这个入口是个命名的值,而不是一个键。s和d值前缀分别表示REG_SZ 或 REG_DWORD。ATL的解析机制既可以识别 HKEY_CLASSES_ROOT 等标准的注册表键,也能识别HKCR之类的缩写表示。
REGEDIT4 [HKEY_CLASSES_ROOT\CLSID\{XXX}] @=My Class [HKEY_CLASSES_ROOT\CLSID\{XXX}\InprocServer32] @=C:\foo\bar.dll ThreadingModel=Free其对应的注册脚本如下:
HKCR { NoRemove CLSID { ForceRemove {XXX} = s ''''My Class'''' { InprocServer32 = s ''''%MODULE%'''' { val ThreadingModel = s ''''Free'''' } } } }在使用资源脚本的时候,你的类 UpdateRegistry 方法可以轻松地通过DECLARE_ REGISTRY_RESOURCEID宏定义,它有一个资源ID(通常在resource.h中定义)作为参数:
class CPager : public CComObjectRoot,public IPager CComCoClass { DECLARE_REGISTRY_RESOURCEID(IDR_PAGER) };这个宏仅仅定义了 UpdateRegistry 方法,它调用内建在 CComModule 中的方法UpdateRegistryFromResource。这个方法有调用资源脚本的解析机制。
DateInstalled = s ''''%CURRENTDATE%''''然后丁一个定制的UpdateRegistry方法代替使用DECLARE_REGISTRY_ RESOURCEID宏,在你的方法中,建立一个名字-值对置换表,提供给模块的注册引擎。
static HRESULT WINAPI CPager::UpdateRegistry(BOOL b) { OLECHAR wsz [1024]; SYSTEMTIME st; GetLocalTime(&st); wsprintfW(wsz, L"%d/%d/%d", st.wMonth, st.wDay, st.wYear); _ATL_REGMAP_ENTRY rm[] = { { OLESTR("CURRENTDATE"), wsz}, { 0, 0 }, }; return _Module.UpdateRegistryFromResource(IDR_PAGER,b, rm); }这个代码和注册脚本最后的运行结果是注册键 DateInstalled 将包含安装时的日期。
连接
COM 编程最单调乏味的一个方面是使用连接点来支持 outbound 接口。IConnectionPoint/IConnectionPointContainer 的设计好像是专门用来解决这个问题的,但是经验证明,不论是在性能方面,还是在易用性方面,它还是存在不足之处。ATL为每一个这样的接口提供了缺省的实现类,多少解决了一些易用性问题。
要理解ATL如何实现连结点,最容易的方式是看例子:假设定义了如下的 outbound 接口:
interface IPageSink : IUnknown { HRESULT OnPageReceived(void); } interface IStopSink : IUnknown { HRESULT OnShutdown(void); }为了支持这两个 outbound 接口,下面的ATL代码已经足够:
class CPager : public CComObjectRoot, public CComCoClass, public IPager, public IConnectionPointContainerImpl, public IConnectionPointImpl, public IConnectionPointImpl { BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(IPager) COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT_MAP(CPager) CONNECTION_POINT_ENTRY(IID_IPageSink) CONNECTION_POINT_ENTRY(IID_IStopSink) END_CONNECTION_POINT_MAP() };大多数有经验的 COM 程序员首先注意到的是 CPager 类从一个接口(IConnectionPoint)派生的,这个接口并不作为 COM 本身的一部分提供。为了实现这种诀窍,ATL 类 IConnectionPointImpl 不从接口 IConnectionPoint 派生,而是象 IConnectionPoint 那样以相同的顺序定义它的虚函数。
typedef IConnectionPointImpl base; for (IUnknown** pp = base::m_vec.begin(); pp < base::m_vec.end(); pp++) if (*pp) ((IPageSink*)(*pp))->OnPageRecieved();编写多点传送例程十分繁琐。所幸的是,ATL提供了 Visual Studio 组件—— ATL 代理产生器(ATL Proxy Generator),(如下图五)它们会读取接口的类型库描述并产生 IConnectionPointImpl 派生类,为每一个 outbound 方法添加适当的 Fire 例程。连结点代理便被产生出来。
class CPager : public CComObjectRoot, public CComCoClass, public IPager, public IConnectionPointContainerImpl, public CProxyIPageSink, public CProxyIStopSink { BEGIN_COM_MAP(CPager) COM_INTERFACE_ENTRY(IPager) COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT_MAP(CPager) CONNECTION_POINT_ENTRY(IID_IPageSink) CONNECTION_POINT_ENTRY(IID_IStopSink) END_CONNECTION_POINT_MAP() };为了发送出境方法通知,你只要调用适当的Fire_XXX方法即可:
STDMETHODIMP CPager::SendMessage(LPCOLESTR pwsz) { // send outbound notifications HRESULT hr = Fire_OnPageRecieved(); // process normally return hr; }机器产生代理的一个限制是必须要 outbound 接口的类型库定义。对于大量的 COM 接口而言,这是不可能的,因为类型库在转换方面与 IDL 特性背道而驰。对于更复杂的接口,你可以从机器所产生的代理开始并修改这些代码来进行尝试。