本文假设你熟悉C++和COM。
摘要: ATL——活动模板库(The Active Template Library),其设计旨在让人们用C++方便灵活地开发COM对象。ATL本身相当小巧灵活,这是它最大的优点。用它可以创建轻量级的,自包含的,可复用的二进制代码,不用任何附加的运行时DLLs支持。
由于COM技术良好的口碑,越来越多的程序员已经走进或正在走进COM的编程世界。它就像盛夏里的冰镇啤酒,从来不会让你失望。可惜作为一个C++程序员来说,C++从不与我分享COM的极致以及我对COM的情有独钟。
C++与COM之间若即若离,和平共处,一次又一次在每个对象中用同样简洁的几行代码实现IUnknown。我敢肯定将来C++编译器和链接器会实现C++对象和COM对象之间自然 的无意识的对应和映射,目前这个环境只存在于实验室中,因此它肯定不是一个你我今天可以购买的产品。眼下可得到的最接近这个环境的东西就是活动模板库——ATL。
为什么使用ATL?
ATL是在单层(single-tier)应用逐渐过时,分布式应用逐渐成为主流这样一个环境中诞生的, 它最初的版本是在四个C++头文件中,其中有一个还是空的。它所形成的出色的构架专门用于开发现代分布式应用所需的轻量级COM组件。作为一个模块化的标准组件,ATL不像MFC有厚重的基础结构,省时好用的库使得成百上千的程序员一次又一次轻松实现IUnknown 和IClassFactory。
ATL的构架并不打算包罗万象,无所不能。其第一个版本对实现IUnknown,IClassFactory,IDispatch,IconnectionPointContainer及COM枚举提供非常 到位的支持。第二个版本除了可以编写ActiveX控件外,还对最初的第一个版本中ATL类进行了增强。ATL不提供集合(collections)和串(strings)的处理 ,它假设你用标准的C++库进行这些处理;不支持ODBC——这个世界正在转移到基于COM的不需要包装的数据存取方式;不支持WinSock打包类--sockets本身也是新的东西;ATL也不支持完整的Win32 API打包类——ATL2.0的实现机制提供了对话框和WndProcs支持。此外ATL中没有MFC中的文档/视图模型。取而代之的是ATL那更具伸缩性和灵活 性的通过COM接口(如ActiveX控件)与基于UI的对象之间的沟通模式。
使用正确的工具非常关键。如果你正在编写一个不可见的COM组件,那么ATL与MFC比起来,从开发效率,可伸缩性,运行时性能以及可执行文件大小各方面来看,ATL可能 都是最好的选择。对于现代基于ActiveX控件的用户界面,ATL所产生的代码也比MFC更小更快。另一方面,与MFC的类向导相比,ATL需要更多的COM知识。ATL与STL一样,对于单层应用没什么帮助,而MFC在这方面保持着它的优势。
ATL的设计在很大程度上来自STL的灵感,STL与所有ANSI/ISO兼容的C++编译器一起已经被纳入成为标准C++库的一部分。像STL一样,ATL大胆使用C++模板。模板是C++中众多具有争议的特性之一。每每使用不当都会导致执行混乱,降低性能 和难以理解的代码。明智地使用模板所产生的通用性效果和类型安全特性则是其它方法所望尘莫及的。ATL与STL一样陷入了两个极端。幸运的是 在L大胆使用C++模板的同时,编译器和链接器技术也在以同样的步伐向前发展。为当前和将来的开发进行STL和ATL的合理选择。
尽管模板在内部得到广泛的使用,但是在用ATL技术时,你不用去敲入或关心那些模板中的尖括弧。因为ATL本身带有ATL对象向导(参见图一):
图一 ATL 对象向导
对象向导产生大量基于ATL模板类缺省的对象实现代码(即框架代码)。这些缺省的对象类型如
附表一所列。ATL对象向导允许任何人 快速建立COM对象并且在分分钟之内让它运行起来,不用去考虑COM或ATL的细节问题。当然,为了能充分驾驭ATL,你必须掌握C++,模板和COM编程技术。对于大型的对象类,只要在ATL对象向导所产生的缺省实现(框架代码)中加入方法实现来输出定制接口,这也是大多数开发人员开始实现COM对象时的重点所在。
初次接触ATL时,其体系结构给人的感觉是神秘和不可思议。
HelloATL是一个最简单的基于ATL的进程内服务器源代码 以及用SDK(纯粹用C++编写)实现的同样一个进程内服务器源代码。在真正构建出一个COM组件之前,代码需要经过反反复复多次斟酌和修改。对于想加速开发COM组件速度的主流组件开发人员来说,ATL体系结构并不是什么大问题,因为对象向导产生了所需要的全部框架代码,只 要你加入方法定义即可。对于认真的COM开发人员和系统编程人员来说,ATL提供了一个用C++建立COM组件的高级的,可扩展的体系结构。一旦你理解和掌握了这个体系结构并能驾驭对象向导,你就会看到ATL的表现能力和强大的功能 ,它完全可以和原始的COM编程技术媲美。
另外一个使用ATL开发COM组件的理由是Visual C++ 5.0+集成开发环境(IDE)对ATL的高度支持。 微软在Visual C++ 5.0+中将ATL所要用到的接口定义语言(IDL)集成到了C++编辑器中。
步篇
在本文的
第一部分,我们简要介绍了ATL的一些背景知识以及ATL所面向的开发技术和环境。在这一部分 将开始走进ATL,讲述ATL编程的基本方法、原则和必须要注意的问题。
理解ATL最容易的方法是考察它对客户端编程的支持。对于COM编程新手而言,一个棘手的主要问题之一是正确管理接口指针的引用计数。COM的引用计数法则是没有运行时强制 性的,也就是说每一个客户端必须保证对对象的承诺。
有经验的COM编程者常常习惯于使用文档中(如《Inside OLE》)提出的标准模式。调用某个函数或方法,返回接口指针,在某个时间范围内使用这个接口指针,然后释放它。下面是使用这种模式的代码例子:
void f(void) {
IUnknown *pUnk = 0;
// 调用
HRESULT hr = GetSomeObject(&pUnk);
if (SUCCEEDED(hr)) {
// 使用
UseSomeObject(pUnk);
// 释放
pUnk->Release();
}
}
这个模式在COM程序员心中是如此根深蒂固,以至于他们常常不写实际使用指针的语句,而是先在代码块末尾敲入Release语句。这很像C程序员使用switch语句时的条件反射一样,先敲入break再说。
其实调用Release实在不是什么可怕的负担,但是,客户端程序员面临两个相当严重的问题。第一个问题与获得多接口指针有关。如果某个函数需要在做任何实际工作之前获得三个接口指针,也就是说在第一个使用指针的语句之前必须要由三个调用语句。在书写代码时,这常常意味着程序员需要写许多嵌套条件语句,如:
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();
}
这样的代码虽然不那么专业,但至少减少了屏幕的水平滚动。
使用以上这些代码段潜在着更加棘手的问题,那就是在碰到C++异常时。如果函数UseObjects丢出异常,则释放指针的代码被完全屏蔽掉了。 解决这个问题的一个方法是使用Win32的结构化异常处理(SEH)进行终结操作:
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类实现客户端基本的COM引用计数模型。CComPtr有一个数据成员,它是一个未经过任何加工的COM接口指针。其类型被作为模板参数传递:
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对操作符重载用法的扩展,使得代码的编译和运行无懈可击。
假定模板类知道它所操纵的指针类型,你可能会问:那为什么智能指针不能在它的功能操作符或构造函数中自动调用QueryInterface,从而更有效地包装IUnknown呢?在Visual C++ 5.0出来以前,没有办法将某个接口的GUID与它的本身的C++类型关联起来——Visual C++ 5.0用私有的declspec将某个IID与一个接口定义绑定在一起。因为ATL的设计 考虑到了它要与大量不同的C++编译器一起工作,它需要用与编译器无关的手段提供GUID。下面我们来探讨另一个类——CComQIPtr类。
CComQIPtr与CComPtr关系很密切(实际上,它只增加了两个成员函数)。CComQIPtr必须要两个模板参数:一个是被操纵的指针类型 ,另一个是对应于这个指针类型的GUID。例如,下列代码声明了操纵IDataObject 和IPersist接口的智能指针:
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。
既然有了CComQIPtr,那为什么还要CComPtr呢?由几个理由:首先,ATL最初的发布版本只支持CComPtr,所以它就一直合法地保留下来了。其二(也是最重要的理由),由于重载的构造函数和赋值操作,对IUnknown使用CComQIPtr是非法的。因为所有COM接口的类型定义都必须与IUnknown兼容。
CComPtr<IUnknown> unk;
从功能上将它等同于
CComQIPtr<IUnknown, &IID_IUnknown> unk;
前者正确。后者是错误的用法。如果你这样写了,C++编译器将提醒你改正。
将CComPtr作为首选的另外一个理由可能是一些开发人员相信静悄悄地调用QueryInterface,没有警告,削弱了C++系统的类型。毕竟,C++在没有进行强制类型转换的情况下不允许对类型不一致的原始指针 进行赋值操作,所以为什么要用智能指针的道理也在这,幸运的是开发人员可以选择最能满足需要的指针类型。
许多开发人员将智能指针看成是对过于的复杂编程任务的简化。我最初也是这么认为的。但只要留意它们使用COM智能指针的方法。就会逐渐认识到它们引入的潜在危险与它们解决的问题一样多。
关于这一点,我用一个现成的使用原始指针的函数为例:
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。显然这是错误的,编译器和链接器也欣然接受了这个代码。如果你运气好的话,调试器会很快捕获到这个错误。
使用ATL智能指针的另一个要引起注意的风险是类型强制转换操作符对原始指针提供的访问。如果隐式强制转换操作符的使用存在争议。当 ANSI/ISO C++ 委员会在决定采用某个C++串类时,他们明确禁止隐式类型转换。而是要求必须显式使用c_str函数在需要常量char *(const char *)的地方传递标准C++串。ATL提供了一种隐含式的类型转换操作符顺利地解决了这个问题。通常,这个转换操作符可以根据你的喜好来使用,允许你将智能指针传递到需要用原始指针的函数。
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。
有关使用智能指针的更详细一般信息,请参见Scott Meyer的《More Effective C++》(Addison-Wesley, 1995年出版)。国内目前还没有这本书的中译本或影印本。有关COM智能指针的更多特定信息,请参见Don Box的一篇关于智能指针的专题文章
第一部分:为什么要使用ATL。
第二部分:起步篇。
实现IUnknown 用纯粹的C++实现IUnknown相对来说比较简单。IUnknown实现之间的主要差别重点在于QueryInterface中将给出哪些接口。请看下列接口定义:
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的最后四行代码对所有的对象都一样。其余的部分则根据这个对象类型层次上的类不同而有所不同。
如果IUnknown的实现能形成规律,那么ATL便可以提供一种机制将这些具有共性的程序语句从代码中提取出来。实际上ATL做到了这一点,方法是通过提供灵活和可扩展的类层次,使得开发人员只要正确地说明所使用的类集,就可确定线程,服务器锁定和对象生命期行为。
如果看一看ATL实现的IUnknown类层次,你碰到的第一个参数化行为就是线程。ATL允许你构造被优化的对象和服务器,在相互转换STA和MTA的用法时,不用修改源代码。缺省情况下,ATL工程构造的是安全的MTA(MTA-safe)对象,除了要具备构造单STA对象所需的一切外,还需要附加代码和状态管理。通过定义适当的预处理指令,你能改变缺省的线程行为,从而支持单STA或多个基于STA的工程。
使用如下这个指令:
/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的线程模型概念。ATL定义了三种线程模型类来实现线程安全行为所需的少数内联操作。每一种线程模型都输出两个静态函数,Increment 和Decrement。它们都有一个长整指针参数,并且对指定的线程模型实现最快的,合法的增减操作。每一种线程模型都输出两个嵌套类型定义(typedefs),即AutoCriticalSection 和CriticalSection,它们要么打包Win32的CRITICAL_SECTION,要么就是出于兼容性考虑的空类,没有实际实现。记住,所用临界区实际使用的实际类型依赖于特定的线程模型。
ATL实现的三种线程模型分别是CComMultiThreadModel,CComSingleThreadModel和 CComMultiThreadModelNoCS。CComMultiThreadModel使用InterlockedIncrement/InterlockedDecrement和实的CRITICAL_SECTIONS。CComSingleThreadModel使用更有效的++和-操作符及虚的CRITICAL_SECTIONS。
混合的CComMultiThreadModelNoCS除了使用虚的CRITICAL_SECTIONS外,还有InterlockedIncrement/InterlockedDecrement。第三种模型对于存在于MTAs中,但不需要任何数据成员的锁定的对象很有用。
ATL提供了两个类型定义,CComObjectThreadModel 和 CComGlobalsThreadModel,通过条件编译来保证对象和全局变量各自的效率及行为安全。依据所定义的三种预编译指令之一,每一类型名对应着以上描述的三种线程模型类之一。下表说明了这种对应关系,它依赖于ATL所使用的预处理指令。
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来安全地改变这个对象的引用计数。
如果前面的代码是用_ATL_APARTMENT_THREADED 或者 _ATL_SINGLE_ THREADED选项编译的,则m_critsee数据成员将为空,Lock和Unlock内联例程将变成虚操作,并且AddRef和Release方法将使用++和--操作符。如果这个对象是一个ActiveX控件且其线程模型为Apartment (ThreadingModel=Apartment)的进程内服务器,则这将是小而快的代码。
有时构造以MTA模式(即它的AddRef和Release必须是线程安全的)运行,但不需要附加锁定的对象很有用。对于这种类型的对象,混合型的CComMultiThreadModelNoCS很适合。
通过将类的类型定义从:
typedef CComObjectThreadModel _ThreadModel;
细化到
typedef CComMultiThreadModelNoCS _ThreadModel;
那么针对每一个对象,你不必付出CRITICAL_SECTION的开销(CComAutoCriticalSection 会映射到 CComFakeCriticalSection)就可以得到线程安全的AddRef和Release(将Increment 和 Decrement方法映射到InterlockedIncrement和InterlockedDecrement)。
实现接口
现在你已经积累了一些关于ATL线程模型方面的知识,下面我们来讨论ATL如何实现IUnknown。ATL最不直观的(同时也是最强大的)一个方面就是你要实现的类事实上都是不能被直接实例化的抽象类。实现一个从通用的IUnknown派生的C++类。但是在确定对象的运行环境之前,QueryInterface,AddRef 和 Release是不会有实质性代码的。这种灵活性使开发人员能实现对象的关键功能,如COM的聚合支持,tear-offs,堆和栈分配,服务器锁定等等。下图展示了一个典型的基于ATL的类层次。
图四 典型的基于ATL的类层次
从下面的代码可以看出,ATL中实现IUnknown的关键在于CComObjectRootBase 和 CComObjectRootEx。
CComObjectRoot
class CComObjectRootBase {
public:
// C++ 构造函数
CComObjectRootBase() { m_dwRef = 0L; }
// ATL 伪构造函数和伪析构函数
HRESULT FinalConstruct() { return S_OK; }
void FinalRelease() {}
// 内部Unknown函数(由派生类提供的InternalAddRef/Release)
static HRESULT WINAPI InternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) {
HRESULT hRes = AtlInternalQueryInterface(pThis,pEntries,iid,ppvObject);
return _ATLDUMPIID(iid, pszClassName, hRes);
}
// 外部Unknown函数
ULONG OuterAddRef() { return m_pOuterUnknown->AddRef(); }
ULONG OuterRelease() { return m_pOuterUnknown->Release(); }
HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject)
{ return m_pOuterUnknown->QueryInterface(iid, ppvObject); }
// ATL 创建者的钩子例程
void SetVoid(void*) {}
void InternalFinalConstructAddRef() {}
void InternalFinalConstructRelease() {}
// ATL 接口映射辅助函数
static HRESULT WINAPI _Break( void*, REFIID, void**, DWORD);
static HRESULT WINAPI _NoInterface( void*, REFIID, void**, DWORD);
static HRESULT WINAPI _Creator( void*, REFIID, void**, DWORD);
static HRESULT WINAPI _Delegate( void*, REFIID, void**, DWORD);
static HRESULT WINAPI _Chain( void*, REFIID, void**, DWORD);
static HRESULT WINAPI _Cache( void*, REFIID, void**, DWORD);
// 实际的引用计数或者指针返回到真实的Unknown
union {
long m_dwRef;
IUnknown* m_pOuterUnknown;
};
};
template <class ThreadModel>
class CComObjectRootEx : public CComObjectRootBase {
public:
typedef ThreadModel _ThreadModel;
typedef _ThreadModel::AutoCriticalSection _CritSec;
// 内部 Unknown 函数(InternalQueryInterface 由 CComObjectRootBase提供)
ULONG InternalAddRef() { return _ThreadModel::Increment(&m_dwRef); }
ULONG InternalRelease() { return _ThreadModel::Decrement(&m_dwRef); }
// 对象级的锁定操作
void Lock() {m_critsec.Lock();}
void Unlock() {m_critsec.Unlock();}
private:
_CritSec m_critsec;
};
这两个类提供了三个方法:OuterQueryInterface,OuterAddRef 和 OuterRelease,它们被用来将IUnknown的功能委派给外部实现。当实现COM的聚合和tear-offs时要用到这些方法。其它三个方法--InternalQueryInterface,InternalAddRef和 InternalRelease的作用是实现本身的引用计数以及对象接口的查询或导航。
CComObjectRootEx是个模板类,允许你针对这个类指定使用哪种ATL线程模型。(如果你想要进行条件编译,则使用CComObjectRoot就可以了,它是一个针对CComObjectRootEx<CComObjectThreadModel>的类型定义。)CComObjectRootEx从CComObjectRootBase中派生其大多数功能,它是个相当袖珍的类,只包含一个联合类型的数据成员:
union {
long m_dwRef;
IUnknown *m_pOuterUnknown;
};
根据使用这个类的实际方式,联合中的成员将被用于保存给定类实例的生命周期。大多数情况下要用到m_dwRef,m_pOuterUnknown只有在支持聚合或tear-offs时用到。CComObjectRootBase提供了OuterQueryInterface,OuterAddRef和OuterRelease方法,通过m_pOuterUnknown成员转发IUnknown请求。
反过来,CComObjectRootEx提供InternalAddRef 和InternalRelease方法根据模板参数传递的线程模型来实际增减m_dwRef变量得值。注意这些例程只是增减这个变量,而没有真正删除这个对象。这是因为此对象的分配策略将由派生类中提供,派生类将使用这些例程来调整引用计数。
CComObjectRoot层次最引人注目的是它的QueryInterface实现函数,它被作为CComObjectRootBase的方法(InternalQueryInterface)输出:
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 |
每一个 CComObject 类用派生来提供正确的 QueryInterface,AddRef 和 Release 实现。如果QueryInterface,AddRef 和 Release的语义正确,则所有 CComObject 类用你的类名作为模板参数并创建一个从你的类派生的新类。在这些类中,CComObjectNoLock 是最容易理解的一个类。请看其代码:
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)。
为了在堆中分配基于 ATL 类的 CPager 实例,只要对这个类名进行 CComObject 模板包装即可:
IPager *p = new CComObjectNoLock();
CComObjectNoLock
类从 CPager 派生,并且添加了由 CPager 提供的使用 InternalQueryInterface,InternalAddRef 和 InternalRelease 的 QueryInterface,AddRef 和 Release实现。因为此对象是基于堆的,所以对 delete 的调用将发生在 CComObjectNoLock 类的 Release 实现中,此时InternalRelease 返回零。
CComObject 类通常被用于基于堆的对象,这个对象只要存在,则服务器一直要运行。与许多 CComObject 家族中的其它类一样,CComObject提供了一个全程变量,_Module,它有两个方法,Lock 和 Unlock。这些方法与 MFC 的 AfxOleLockApp 和AfxOleUnLockApp 很相似。CComObject 的构造函数调用_Module 的 Lock 方法,而 CComObject 的析构函数则调用_Module的Unlock 方法。ATL提供了一个 CComModule 类,它以适当的方式为进程内服务器实现了这些方法。当建立进程外服务器时,某个派生类(CExeModule)必须改写默认的 Lock / Unlock方法,以便以某种适当的方式下掉服务器。基于 AppWizard 的 ATL工程自动会有一个类用 PostThreadMessage 终止主线程的消息循环。
输出你的类
实现了 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,从而减少了代码的大小。
使用这种技术的类(包括大多数 ATL 基类)需要当心,不要调用构造器中的虚函数。但是,为了在初始化时允许对虚函数的调用,ATL 的 Creators 调用 FinalConstruct 方法,在这个方法中进行所有重要的初始化工作。在 FinalConstuct 中,从C++的角度看,你的类已经完全构造好了,也就是说你的所有对象的 vptr 完全被派生化。同时,基于 CComObject 的打包器也同时构造好了,允许你存取在 COM 聚合或 tear-off 情况下无法知道的控制。
如果在调试器中单步顺序执行 Creator 调用,你将注意到在缺省情况下对 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 的调用什么也没做,但是,如果你打算在你的 FinalConstruct 实现中创建 COM 聚合,你可能会临时增加一次对象的引用计数,以防止它过早销毁(这发生在某个聚合对象调用 QueryInterface时)。你能通过添加下面的类定义行进行自我保护: DECLARE_PROTECT_FINAL_CONSTRUCT()
这一行代码重新定义了类的 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 来增减引用计数,从而安全地传递可能调用 QueryInterface 的对象指针。
每一个基于ATL的工程都包含着一个 CComModule 派生类的实例。除了实现前面提到过的服务器生命期行为外,CComModule 还维持着一个 CLSID 到 ClassObject 的映射(叫做对象映射 Object Map)向量来提供所有外部可创建类。这个对象映射被用于实现进程内服务器的 DllGetClassObject,并且它为进程外服务器每次调用 CoRegisterClassObject 提供参数。虽然能直接显式地使用 CComClassFactory 和 Creator 类,但通常都是在 ATL 对象映射基础的上下文中使用。 ATL Object Map 是一个_ATL_OBJMAP_ ENTRY结构数组:
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运行时必须用来构造全局和静态变量。)
虽然你可以显式地定义用于对象映射的各种函数,通常的方法是将 CComCoClass 添加到你自己类的基类列表中。CComCoClass 是一个模板类,它有两个模板参数:你自己的类名和对应的 CLSID 指针。它添加适当的类型定义和静态成员函数来提供对象映射必须的功能。下面的代码示范了 CComCoClass 的使用:
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 数组,初始化如下:
static _ATL_OBJMAP_ENTRY ObjectMap[] = {
{ &CLSID_Pager, &CPager::UpdateRegistry,
&CPager::_ClassFactoryCreatorClass::CreateInstance,
&CPager::_CreatorClass::CreateInstance, NULL, 0,
&CPager::GetObjectDescription
},
{ &CLSID_Laptop, &CLaptop::UpdateRegistry,
&CLaptop::_ClassFactoryCreatorClass::CreateInstance,
&CLaptop::_CreatorClass::CreateInstance, NULL, 0,
&CLaptop::GetObjectDescription
},
{ 0, 0, 0, 0 } };
静态成员函数从 CComCoClass 派生,被隐含式定义。以上定义的对象映射一般通过使用 CComModule 的 Init 方法被传递到ATL:
_Module.Init(ObjectMap, hInstance);
这个方法根据创建的服务器类型,在 DllMain 或 WinMain 中被调用。
缺省情况下,CcomCoClass 为你的类提供了一个标准的类工厂,允许客户端聚合你的对象。你可以通过添加下面的类定义代码行来改变缺省的聚合行为:
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 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。这个方法有调用资源脚本的解析机制。
在上面显示的注册脚本中,所有出现 %MODULE% 的地方将被实际的 GetModuleFileName 调用结果所代替。如果你需要根据动态运行值添加额外的注册条目,可以添加其它的在注册之前能被置换的串,而不是用 %MODULE%。为此,首先要选用一个新的置换变量,用百分符号限定变量名。例如: DateInstalled = s ''''%CURRENTDATE%''''
然后丁一个定制的UpdateRegistry方法代替使用DECLARE_REGISTRY_ RESOURCEID宏,在你的方法中,建立一个名字-值对置换表,提供给模块的注册引擎。
下面是用包含当前日期的串来替换%CURRENTDATE%变量的一个例子: 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 那样以相同的顺序定义它的虚函数。
其次,为了防止每一个基类继承主对象的 QueryInterface 实现,IConnectionPointImpl中第一个虚函数不是QueryInterface。而是一个类型兼容的方法,它叫做 LocCPQueryInterface,这个方法只针对 IID_IConnectionPoint 和 IID_IUnknown。它除了涉及允许完全基于多线程实现外,还有许多深奥的窍门在里面。
对象的 FindConnectionPoint 方法实现使用由 ATL中 CONNECTION_POINT 宏定义的连接点映射。此映射是对象中的一个 DWORD表,表示与 IConnectionPoint 实现相对应的偏移量。FindConnectionPoint 遍历此偏移量数组,询问每一个所碰到的连接点,看看此连接是否拥有所请求的接口。
上述例子建立了一个有效的对象实现,它支持作为 outbound 接口的 IStopSink 和 IPageSink 。但是,为了调用outbound 接口,你需要存取由 IConnectionPointImpl 类操纵的接口指针向量,并自己手动模拟多点传送:
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 例程。连结点代理便被产生出来。
图五 ATL 代理产生器
这个类的定义应该像下面这样: 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 特性背道而驰。对于更复杂的接口,你可以从机器所产生的代理开始并修改这些代码来进行尝试。
后记
本文概括地讨论了 ATL 核心体系结构,主要针对 ATL 中使用,同时也是 ATL 用户使用的基本编程风格。实际上本文中所讨论的和涉及的内容特性都是基于 ATL1.1 版本的,某些内容在 ATL2.0 中稍有改动。本文没有包含有关ActiveX 控件接口的缺省实现方面的内容,它们被加到了 ATL2.0 版本中,遵循与 ATL1.1 同样的编程哲学。只要你常常推敲 ATL 源代码,这些接口会很容易理解。
在与同事和朋友讨论 ATL 时,我发现大多数人的感触是针对 ATL 的设计。有一小部分人觉得它非常棒。另外一小部分人觉得它复杂而不可思议。但大多数人(包括我自己)的感觉是双重的,优劣兼而有之。幸运的是 ATL 编程是一个非常"量入为出"的过程。只要你去学总会有收获,所以初学者需要努力而为之。
From:http://www.vckbase.com/
翻译者:赵湘宁
posted on 2007-01-29 09:53
我风 阅读(3331)
评论(0) 编辑 收藏 引用