160 likes | 366 Views
十六 ATL. ATL 对 COM 客户端的支持 使用 ATL 开发 COM 组件. 1 ATL 对 COM 客户端的支持. ATL 对 COM 客户的支持主要体现在智能指针上。 回顾: 当没有对 COM 对象进行引用计数时,对象的每个接口指针必须密切配合以保持内存的正确性。 比如: void f(void){ IFastString *pfs=0; IPersistentObject *ppo=0; pfs=CreateFastString("asdf"); if(pfs) {
E N D
十六 ATL • ATL对COM客户端的支持 • 使用ATL开发COM组件
1 ATL对COM客户端的支持 • ATL对COM客户的支持主要体现在智能指针上。 • 回顾: 当没有对COM对象进行引用计数时,对象的每个接口指针必须密切配合以保持内存的正确性。 比如: void f(void){ IFastString *pfs=0; IPersistentObject *ppo=0; pfs=CreateFastString("asdf"); if(pfs) { ppo=(IPersistentObject*) pfs->Dynamic_Cast("IPersistentObject"); if(!ppo) pfs->Delete();//转换不成功,使用pfs来删除对象。 else{ ppo->Save("c:\\myfile"); ppo->Delete();} //转换成功,使用ppo来删除对象。 } }
实现了引用计数后, 每个对象维护一个引用计数,当接口指针被复制的时候,计数增加;接口指针被销毁的时候,计数减少。 1。当接口指针被复制时,调用DuplicatePointer 即AddRef 2。当接口指针不再有用时调用DestroyPointer 即Release void f(void){ IFastString *pfs=0; IPersistentObject *ppo=0; pfs=CreateFastString("asdf"); if(pfs) { ppo=(IPersistentObject*) pfs->Dynamic_Cast("IPersistentObject"); if(ppo) { ppo->Save("c:\\myfile"); ppo->DestroyPointer();} //每个指针各自负责自己。 pfs->DestroyPointer();//每个指针各自负责自己。 } } 或者使用正式的表达方式:
void GetLottaPointers(LPUNKNOWN pUnk) { HRESULT hr; LPPERSIST pPersist; LPDISPATCH pDispatch; LPDATAOBJECT pDataObject; hr = pUnk->QueryInterface(IID_IPersist, (LPVOID *)&pPersist); //引用计数加1 if(SUCCEEDED(hr)) { hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *) &pDispatch); //引用计数加1 if(SUCCEEDED(hr)) { hr = pUnk->QueryInterface(IID_IDataObject, (LPVOID *) &pDataObject); //引用计数加1 if(SUCCEEDED(hr)) { DoIt(pPersist, pDispatch, pDataObject); //同时使用三个指针 pDataObject->Release(); } //释放引用 pDispatch->Release(); } //释放引用 pPersist->Release(); } //释放引用 pUnk指向的对象的引用计数在退出此函数时保持不变。对象检测到引用为零时,会删除自己。 } 比较没有引用计数的情况,有了很大的进步。但是仍然要小心。
CComPtr • template <class T> class CComPtr { public: typedef T _PtrClass; T* p;//数据成员 CComPtr() {p=NULL;} //缺省构造函数把内部接口指针初始化为NULL CComPtr(T* lp) { if ((p = lp) != NULL) p->AddRef(); }//构造函数 引用计数加1 CComPtr(const CComPtr<T>& lp) { if ((p = lp.p) != NULL) p->AddRef(); } //构造函数 引用计数加1 /* CComPtr对象可以通过一个适当类型的接口指针进行初始化,可以使用IFoo或者另一个CComPtr<IFoo> 对象来初始化。另外两个构造函数分别完成这个任务。如果指定的值不是NULL,则构造函数调用内部接口指针的AddRef 比如CComPtr<IUnkown> punk ; CComPtr<ISomeInterface> psome */ T* operator=(T* lp){return (T*)AtlComPtrAssign( (IUnknown**)&p, lp);} T* operator=(const CComPtr<T>& lp) { return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p); }
/* 重载了=操作符进行赋值操作,在赋值操作时: 1当前接口指针非NULL时,Release 2源接口指针非NULL时,AddRef 3把源接口指针保存为当前接口指针*/ 这里使用了AtlComPtrAssign函数。 IUnkown* AtlComPtrAssign(IUnknown**pp,IUnkown *lp) { if(*pp) (*pp)->Release(); // 当前指针要指向新的对象,原对象引用要减1 if(lp) lp->AddRef(); //源指针要被复制,引用计数要加1。 *pp=lp; return lp;}...... }
在智能指针的支持下,对象的引用计数处理起来更加方便:在智能指针的支持下,对象的引用计数处理起来更加方便: void GetLottaPointers(LPUNKNOWN pUnk){ HRESULT hr; CComPtr<IUnknown> persist; CComPtr<IUnknown> dispatch; CComPtr<IUnknown> dataobject; hr = pUnk->QueryInterface(IID_IPersist, (LPVOID *)&persist); //引用计数不变 if(FAILED(hr)) return; hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *) &dispatch); //引用计数不变 if(FAILED(hr)) return; hr = pUnk->QueryInterface(IID_IDataObject, (LPVOID *) &dataobject); //引用计数不变 if(FAILED(hr)) return; DoIt(pPersist, pDispatch, pDataObject); } 实际上,在整个处理过程中,对象的应用没有任何变化!作为局部变量的persist等指针除了函数体以后也自动被清除。真正做到想用就用,不用就丢。无需任何处理。但是COM对象的引用计数仍然在被正确无误地维持着!
事实上ATL提供了另一个智能指针CComQIPtr对QueryInterface函数进行了封装,使得客户使用COM接口更为方便:事实上ATL提供了另一个智能指针CComQIPtr对QueryInterface函数进行了封装,使得客户使用COM接口更为方便: • template <class T, const IID* piid = &__uuidof(T)> class CComQIPtr { public: T* p; typedef T _PtrClass; CComQIPtr() {p=NULL;} //默认构造函数 CComQIPtr(T* lp) { //类型未定的构造函数 if ((p = lp) != NULL) p->AddRef(); //引用计数加1 } CComQIPtr(const CComQIPtr<T,piid>& lp) {//拷贝构造函数 if ((p = lp.p) != NULL) p->AddRef(); //引用计数加1 } CComQIPtr(IUnknown* lp) { //指向IUnkown接口的指针 p=NULL; if (lp != NULL) lp->QueryInterface(*piid, (void **)&p); //由IUnkown接口进行查询。 } ......
T* operator=(T* lp){ return (T*)AtlComPtrAssign((IUnknown**)&p, lp); } T* operator=(const CComQIPtr<T,piid>& lp) { return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p); } T* operator=(IUnknown* lp) { return (T*)AtlComQIPtrAssign((IUnknown**)&p, lp, *piid); } ...... } CComQIPtr的第二个模板参数是一个指向接口IID的指针。它有四个构造函数,并且对“=”进行了三次重载。对IUnkown指针的赋值会调用 AtlComQIPtrAssign函数: _(IUnknown*) AtlComQIPtrAssign(IUnknown** pp, IUnknown* lp, REFIID riid) { IUnknown* pTemp = *pp; *pp = NULL; if (lp != NULL) lp->QueryInterface(riid, (void**)pp); if (pTemp) pTemp->Release(); //注意这里要释放以前的引用! return *pp; }
在CComQIPtr的支持下,客户的使用变得极为简单:在CComQIPtr的支持下,客户的使用变得极为简单: • void GetLottaPointers(IUnkown* pUnk){ CComQIPtr<IPersist, &IID_IPersist> persist; CComQIPtr<IDispatch, &IID_IDispatch> dispatch; CComPtr<IDataObject, &IID_IDataObject> dataobject; dispatch = pUnk; persist = pUnk; dataobject = pUnk; DoIt(persist, dispatch, dataobject); } 或者, • void GetLottaPointers(IUnkown* pUnk){ CComQIPtr<IPersist, &IID_IPersist> persist(pUnk); CComQIPtr<IDispatch, &IID_IDispatch> dispatch (pUnk); CComPtr<IDataObject, &IID_IDataObject> dataobject (pUnk); DoIt(persist, dispatch, dataobject); } 所有繁琐的操作都不见了。
2 使用ATL开发COM组件 • C语言是产生汇编语言代码的框架。 • ATL是产生C++/COM代码的框架。 使用ATL COM AppWizard进行COM组件开发。 1。 使用ATL COM AppWizard创建一个工程。 可以选择Exe Dll 或 service的方式。这里选择dll, 2。当选择Dll时,还可以选择是否 Allow Merging Of Proxy/Stub Code 当Dll在远程时,客户有两种方式加载它,加载到客户进程或通过代理存根。如果通过代理存根,组件实现者还要提供代理存根dll。(如何开发?)选中以上选项可以把代理存根dll和组件对象连在一起。 另外还可以选择支持MFC和MTS。这里我们都不选。 进行完以上操作后,IDE生成了一个CComModule _Module 的实例。它类似于MFC应用程序的CWinApp类。同时 IDE会给此DLL自动生成 DllGetClassObject, DllCanUnloadNow, DllRegisterServer, 和DllUnregisterServer.引出函数。(它们的作用?) 3。Insert-》New Atl Object-》Simple Object-》myobj 当在short name中输入myobj时,IDE为我们选定了: CoCLass的名字为myobj,它实现的接口为Imyobj,ProgID为 Myatl.myobj。同时IDE为我们生成了类Cmyobj以实现COM对象,用两个文件myobj.h和myobj.cpp 来保存代码。 Attribute页为我们选定了Apartment线程模型,双接口,以及聚合的支持。 以上选项都可以人为地修改。
class ATL_NO_VTABLE Cmyobj : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<Cmyobj, &CLSID_myobj>, public IDispatchImpl<Imyobj, &IID_Imyobj, &LIBID_MYATLLib> {...... 模板类CComObjectRootEx实现了IUnkown的成员方法。QueryInterface、AddRef、和Release。模板类型参数指定了COM对象所运行的线程模型。根据不同的线程模型, CComObjectRootEx模板类对IUnkown成员的实现方法不一样。 CComCoClass模板类为对象定义了缺省的类厂,并且支持聚合模型。 ATL_NO_VTABLE是一个宏 #define ATL_NO_VTABLE __declspec(novtable) 。 ATL使用继承的方式来实现接口。COM组件的内存中包含为每个类所实现的每个接口的虚表。这是采用继承方式(即虚表的方式)必须付出的代价。ATL使用__declspec(novtable)指示编译器不要在构造函数中初始化对象的虚表。(因此不要在它的构造函数中调用虚函数) 问题是,如果没有虚表,那么,客户得到接口的指针如何能调用到它的具体实现呢?答案是客户实际上没有实例化类Cmyobj ,而是实例化了类Cmyobj的一个派生类CComObject<Cmyobj>.CComObject也是一个模板类,它使用Cmyobje作为它的模板参数。而CComObject<Cmyobj>的虚表是被正常创建了的。
ATL支持双重接口的开发。(在MFC开发双重接口很困难) IDispatchImpl模板类用于此目的。其模板参数包括双重接口,接口的IID,接口的类型库的GUID。然后在接口映射表中加上双重接口和IDispatch接口的入口: BEGIN_COM_MAP(Cmyobj) COM_INTERFACE_ENTRY(Imyobj) COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() 这些宏以类似于MFC的方式建立了一张COM接口映射表。表项中记录了每一个接口IID所对应的虚表和this指针的偏移。这张映射表使得接口指针能够找到它所指的对象的this指针,从而访问其数据成员。 在接口上添加方法: 使用classview添加接口方法。 IDE会为我们在IDL文件上添加方法,在cpp文件上添加C++代码。 [id(1), helpstring("method mymethod")] HRESULT mymethod(); STDMETHODIMP Cmyobj::mymethod() { // TODO: Add your implementation code here return S_OK; }
也可以添加属性。属性有设置属性和读取属性两种,也可以同时支持。本质上与方法没有两样。一般我们要在实现类上声明一个成员变量来代表这个属性。属性的读取和设置操作都以这个成员变量为目的。也可以添加属性。属性有设置属性和读取属性两种,也可以同时支持。本质上与方法没有两样。一般我们要在实现类上声明一个成员变量来代表这个属性。属性的读取和设置操作都以这个成员变量为目的。 [propget, id(2), helpstring("property myprop")] HRESULT myprop([out, retval] short *pVal); [propput, id(2), helpstring("property myprop")] HRESULT myprop([in] short newVal); STDMETHODIMP Cmyobj::get_myprop(short *pVal) { return S_OK; } STDMETHODIMP Cmyobj::put_myprop(short newVal) { return S_OK; }
添加新的接口 • 1。在idl文件中添加: [ object, uuid(37F7EFD9-F74C-4c72-BF51-8811E4A9A22D), helpstring("Imyobj2 Interface"), pointer_default(unique) ] interface Imyobj2 : IUnknown {}; coclass myobj { [default] interface Imyobj; interface Imyobj2; }; //uuid 是用guidgen产生的。 2.在类的实现文件中加上: class ATL_NO_VTABLE Cmyobj : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<Cmyobj, &CLSID_myobj>, public IDispatchImpl<Imyobj, &IID_Imyobj, &LIBID_MYATLLib>, public Imyobj2 3.在接口映射表中加上入口: BEGIN_COM_MAP(Cmyobj) COM_INTERFACE_ENTRY(Imyobj) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(Imyobj2) END_COM_MAP() 在IDE中编译后,midl会更新myatl.h myatl_i.cpp等文件。同时classview等也会通步更新。
如果要添加新的双接口: • 1。在idl文件中添加: [ object,dual uuid(F1CC2E60-BF57-4084-9278-03B4C3489649), helpstring("Imyobj2 Interface"), pointer_default(unique) ] interface Imyobj2 : IDiaptch {}; coclass myobj { [default] interface Imyobj; interface Imyobj2; }; //uuid 是用guidgen产生的。 2.在类的实现文件中加上: class ATL_NO_VTABLE Cmyobj : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<Cmyobj, &CLSID_myobj>, public IDispatchImpl<Imyobj, &IID_Imyobj, &LIBID_MYATLLib>, public IDispatchImpl<Imyobj2, &IID_Imyobj2, &LIBID_MYATLLib>, 3.在接口映射表中加上入口: BEGIN_COM_MAP(Cmyobj) COM_INTERFACE_ENTRY(Imyobj) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY2(IDispatch,Imyobj2) END_COM_MAP() //当客户请求Idispatch接口时,此宏使得客户获得正确的版本。