五 COM对象 接口及其接口方法的实现 注册表 类厂 COM组件与客户程序的交互过程 类厂的由来 类厂的定义与实现 类厂的创建 通过类厂创建对象 类厂对组件生存期的控制 COM组件与客户程序的交互过程
1 接口及其接口方法的实现 COM接口只是描述了它所代表的功能.实现这些功能的是COM对象.COM规范并没有规定对象应该如何实现,只要接口指针能够访问到对象对接口的具体实现即可. 这里我们使用C++语言来实现COM对象. 当然,使用C++语言也有不同的方法来实现COM对象,只要通过接口指针能够访问到对象的的方法和属性(私有数据)即可。 这里首先我们使用从接口类派生实现类的办法来实现COM对象。 实际上,使用C++还有别的办法也能实现COM对象。比如,MFC使用的嵌套类的办法,使用模板类的方法等等.我们将在后续章节接触到这些方法. 继续上一章的例子:我们的字典支持接口IDictionary:
class IDictionary:public IUnknown { virtual BOOL __stdcall Initialize() = 0; virtual BOOL __stdcall LoadLibrary(String) = 0; virtual BOOL __stdcall InsertWord(String, String) = 0; virtual void __stdcall DeleteWord(String) = 0; virtual BOOL __stdcall LookupWord(String, String *) = 0; virtual BOOL __stdcall RestoreLibrary(String) = 0; virtual void __stdcall FreeLibrary() = 0; };//注意都是纯虚函数 假设字典还支持另外一个接口: class ISpellCheck : public IUnknown { public : virtual BOOL __stdcall CheckWord(String, String*) = 0;}; 这个接口支持拼写检查的功能 实现类的定义如下:
class CDictionary : public IDictionary , public ISpellCheck{ CDictionary(); ~CDictionary(); //构造函数,析构函数 // IUnknown 成员函数 (在这里要实现,所以再次申明) virtual HRESULT __stdcall QueryInterface(const IID& iid, void **ppv) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // IDictionary成员函数 virtual BOOL __stdcall Initialize(); ...... // ISpellCheck成员函数 virtual BOOL __stdcall CheckWord (String word, String *); private : struct DictWord *m_pData; char *m_DictFilename[128];……//私有与功能相关的数据. int m_Ref ;//用作引用计数 }; CDictionary类,对象,与接口的内存结构 如下:
CDictionary类,对象,与接口的内存结构 this pVtable for IDictionary QueryInterface AddRef Release Cdictionary 类中虚函数的具体实现 CDictionary的vtable m_pData m_DictFile pVtable for ISpellCheck Initialize InsertWord …… CheckWord IDictionary接口指针 另一个IDictionary接口指针 ISpellCheck接口指针 另一个COM对象 某接口指针 Cdictionary2 类中虚函数的具体实现 假设CDictionary2也实现了这两个接口 CDictionary2的vtable CDictionary类,对象,与接口的内存结构 浅蓝色: 类 绿色: 对象 灰色: 接口指针
COM对象的首要任务是要实现在IUnknown中定义的几个纯虚函数,(否则不能创建实例.) Cdictionary增加了一个成员变量作为引用计数。 CDictionary::CDictionary(){ m_Ref = 0;……} //引用计数赋初值 ULONG CDictionary::AddRef(){ m_Ref ++; //增加计数 return (ULONG) m_Ref;} ULONG CDictionary::Release(){ m_Ref --; //减少计数 if (m_Ref == 0 ) { delete this; //减到0时,删除自身 return 0; } 跟上章IExtensibleObject的 DuplicatePointer函数和DestroyPointer函数的实现方式一样。
接口查询函数为: HRESULT CDictionary::QueryInterface(const IID& iid, void **ppv) { if ( iid == IID_IUnknown ) { *ppv = (IDictionary *) this ; ((IDictionary *)(*ppv))->AddRef() ; } else if ( iid == IID_Dictionary ) } else if ( iid == IID_SpellCheck ) { *ppv = (ISpellCheck *) this ; ((ISpellCheck *)(*ppv))->AddRef() ; } else { *ppv = NULL; return E_NOINTERFACE ; } return S_OK; } 注意这里使用((IDictionary *)(*ppv))->AddRef() ; 而不是直接AddRef() ; 这里调用AddRef的指针可能不一样,当然,它们都是指向同一个对象, 都是使用的同一个AddRef() ; 但是也有例外.在使用其他方式实现接口时(嵌套类方式), 不同的指针调用AddRef版本可能不一致. 为了统一起见,一律使用新指针去调用AddRef,这样也符合直观的逻辑.
或者,显示地使用静态转换: HRESULT CDictionary::QueryInterface(const IID& iid, void **ppv){ if ( iid == IID_IUnknown ) { *ppv = static_cast <IDictionary *> (this) ; ((IDictionary *)(*ppv))->AddRef() ; } else if ( iid == IID_Dictionary ) { *ppv = static_cast <IDictionary *>( this) ; } else if ( iid == IID_SpellCheck ) { *ppv = static_cast <ISpellCheck *>( this) ; ((ISpellCheck *)(*ppv))->AddRef() ; } else {*ppv = NULL; return E_NOINTERFACE ; return S_OK;
注意我们没有使用 if ( iid == IID_IUnknown ) { *ppv = static_cast < IUnknown *> (this) ; ……} 是因为这种转换存在二义性,无法通过编译。当客户请求Iunknown接口时,我们直接返回IDictionary接口,因为IDictionary接口虚表的前三个函数正是IUnknown的前三个函数。而且这样处理也满足接口查询的原则。(当然转换成ISpellCheck接口也是可以的) 然后要实现IDictionary中和ISpellCheck中定义的虚函数.这是COM对象之所以存在的价值. 过程略. 完成以上函数以后,COM对象的实质性的功能已经完成,但是为了能让客户顺利地使用使用. 但是,这其中还有许多的关节……
2 注册表 COM对象往往以一个DLL为载体.当然,有时也以一个exe程序为载体. 客户程序和COM对象可能位于不同的进程,甚至不同的机器上.我们需要一种中介机制在双方打起桥梁. 当COM对象的数目,接口的数目,COM客户的数目很大时,其必要性就尤其突出. 注册表是Windows操作系统的中心数据仓库。当组件程序安装到机器上以后,必须把它的信息注册到注册表中,客户才能从表中找到组件程序并对其进行操作。注册表可以看作是组件与客户的中介。是COM实现位置透明性的关键. COM对象的信息存储在注册表的HK_CLASSES_ROOT键的CLSID子键下。在每个COM对象的CLSID子键下,存储了一些相关的信息,比如组件程序的路径,版本号,类型库、ProgID, COM对象的唯一标识符CLSID,COM接口的唯一标识符IID等。ProgID(Program identifier)是字符串化的组件名字。COM提供了两个API用于CLSID和ProgID的相互转换:CLSIDFromProgID和ProgIDFromCLSID。
组件有两种方式将信息注册到注册表中。 1。非自注册。 非自注册组件本身不编写任何代码来支持注册操作,相反,由程序员手工编写注册文件,手工输入到注册表中。比如:使用文本编辑器将要注册的内容写入文件并以.reg后缀保存。双击之组件信息即由注册表编辑器输入注册表。以下是一个注册文件的例子: REGEDIT HKEY_CLASSES_ROOT\CLSID\{89A48671-20B3-11d0-8B80-EA9EFFE6330C} = MyDictionary HKEY_CLASSES_ROOT\CLSID\{89A48671-20B3-11d0-8B80-EA9EFFE6330C}\InprocServer32 = c:\MyDictDll.dll
2。自注册 (先只讨论进程内组件的情形) Windows系统提供了一个注册进程内组件的工具RegSvr32.exe, 只要进程内组件提供了入口函数,RegSvr32.exe就会调用入口函数完成注册工作或注销工作。 注册:RegSvr32.exe c:\MyDictDll.dll 注销:RegSvr32.exe \u c:\MyDictDll.dll 组件负责提供的入口函数名字分别为DllRegisterServer 和 DllUnRegisterServer 分别完成注册和注销任务。 DllRegisterServer 和 DllUnRegisterServer 要由组件程序实现,其中要使用Windows提供的操作注册表的API如RegCreateKey和RegSetValue等函数。
3 类厂 3.1 类厂的由来 最初使用源代码共享时,客户包含C++类的定义,然后使用诸如 3 类厂 3.1 类厂的由来 最初使用源代码共享时,客户包含C++类的定义,然后使用诸如 FastString *pFS=new FastString; 这样的方式 使用动态链接库作为C++对象的载体时, 仍然使用 FastString *pFS=new FastString; 使用接口类把接口从实现中分离出来: FastStringItf *pFIf=new FastStringItf; 使用抽象基类把接口与对象分开以后,COM组件只需引出一个诸如 extern "C" {__declspec(dllexport) IFastString *CreateFastString(const char*psz);} 的函数。 而客户只需调用此引出函数,就可以得到接口指针。 IFastString *pIF=CreateFastString(“abcdefg”); 但是 COM规范考虑地问题更多。比如我们希望在客户创建对象的时候需要客户提供口令或其他安全信息。我们使用另外一个类来创建对象并返回接口。比如说,用以下的方式:
class CDictionaryFactory { public: HRESULT CreateDictinary(IDictionary **ppv) { …… //在这里可以进行身份认证、安全认证等附加操作。 *ppv=new CDictionary; return NO_ERROR; }; } 组件DLL中再把CDictionaryFactory类暴露出去,客户则可以这样使用: IDictionary * pID=NULL; CDictionaryFactory *pCF=new CDictionaryFactory ; IDictionary * pID=pCF-> CreateDictinary(& pID); pID->……//使用字典接口的功能。 通过以上方式,可以进一步控制COM对象的安全性。 但是,很显然,和以前的FastString类一样,CDictionaryFactory类不能很安全地从DLL中引出。然而这一次,我们已经有了经验. 我们可以完全仿照COM对象的实现方式,对CDictionaryFactory的定义和实现也分离开。这就是类厂对象以及类厂接口的由来。
3.2 类厂的定义与实现 类厂,准确地说应该叫“对象厂”,(因为是用来创建对象的). 有的文献称为“类对象” (class object) . COM库通过类厂来创建COM对象。对应每个COM类,有一个类厂专门用于该COM类的对象的创建工作。而且,类厂本身也是一个COM对象。(当然,类厂不再需要别的类厂来创建了.) 类厂支持一个特殊的接口IClassFactory.(如前所述,它也派生自IUnknown) IClassFactory : public IUnknown { public: virtual HRESULT _stdcall CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) = 0; // pUnkOuter 用于对象被聚合的情形,一般把它设为NULL。 Riid指COM对象创建完后,客户应该得到的初始接口IID,比如 IID_Idictionary,IID_ISpellCheck 等. ppv用来保存接口指针。 virtual HRESULT _stdcall LockServer( BOOL fLock) = 0; }; //LockServer用来控制DLL组件的卸载。 类厂的定义则为:
class CDictionaryFactory : public IClassFactory CDictionaryFactory (); ~CDictionaryFactory (); //IUnknown 成员 HRESULT __stdcall QueryInterface(const IID& iid, void **ppv); //查询接口,它对IUnkown接口和IClassFactory接口提供支持并返回其指针。 ULONG __stdcall AddRef(); ULONG __stdcall Release(); //AddRef,Release成员函数实现引用计数操作,m_Ref是其引用计数变量。以上三个函数的实现方法与一般的COM对象完全类似,略去. //IClassFactory 成员 HRESULT __stdcall CreateInstance(IUnknown *, const IID& iid, void **ppv); // CreateInstance是接口类最重要的成员函数。它的实现见下页: HRESULT __stdcall LockServer(BOOL);//组件生存期控制,见后. private: ULONG m_Ref;//类厂接口的引用计数 }; CreateInstance的实现如下:
HRESULT CDictionaryFactory::CreateInstance(IUnknown HRESULT CDictionaryFactory::CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv) { CDictionary * pObj; HRESULT hr; *ppv=NULL; // 确保pUnknownOuter在这里是空指针 if (NULL != pUnknownOuter) return CLASS_E_NOAGGREGATION; pObj=new CDictionary(); // 创建COM对象 if (NULL==pObj) return hr; hr=pObj->QueryInterface(iid, ppv); //返回COM对象的初始接口 if (hr != S_OK) delete pObj; return hr; }
3.3 类厂的创建 现在的问题是类厂如何创建。COM并不使用如下的方法: 3.3 类厂的创建 现在的问题是类厂如何创建。COM并不使用如下的方法: IClassFactory*pIF=CreateClassFactory(…) 相反,COM规定使用DllGetClassObject函数来完成这一任务: extern "C" HRESULT __stdcall DllGetClassObject(const CLSID& clsid, const IID& iid, void **ppv) { if (clsid == CLSID_Dictionary ) { CDictionaryFactory *pFactory = new CDictionaryFactory; if (pFactory == NULL) { return E_OUTOFMEMORY ;} HRESULT result = pFactory->QueryInterface(iid, ppv); return result; } else return CLASS_E_CLASSNOTAVAILABLE; } 首先确认clsid是我们要创建的字典对象的ID,然后创建类厂对象,调用类厂对象的QueryInterface成员函数返回类厂接口指针。整个过程与类厂创建COM对象并返回COM接口的过程完全一致。 Iid一般为IID_ICLassFactory. ppv用来保存类厂接口指针。 然而客户仍然不直接调用DllGetClassObject引出函数函数来获得类厂接口指针。COM规定,客户使用如下的COM库函数:
Extern “C”__stdcall HRESULT CoGetClassObject( REFCLSID rclsid, //将要创建的COM对象的ID DWORD dwClsContext, //指定组件的类别,进程内或进程外 LPVOID pvReserved, //用于DCOM,指定远程对象的服务器信息,此时为NULL REFIID riid, //类厂接口的ID,一般为IID_ICLassFactory void** ppv //用来保存类厂的接口指针。 ); 我们考虑进程内组件的情形。(即COM对象存在于DLL中) CoGetClassObject从注册表中查找组件clsid程序的路径名,(COM组件注册时最主要的任务之一就是注册路径名)然后加载组件到内存。再调用组件程序的引出函数DllGetClassObject以创建类厂接口对象并返回指针。(MSDN文档中指出,在这个过程中并没有调用CoLoadLibrary,但是是否调用了Win32API LoadLibrary和GetProcAddress,无从得知,也许Microsoft使用了未公开的其他API,但是从原理上,我们可以清晰地了解整个过程。) 在调用DllGetClassObject时,CoGetClassObject直接把clsid,riid和ppv三个参数传进去。
3.4 通过类厂创建对象 一旦客户得到了类厂接口指针,就可以使用该指针调用其CreateInstance成员函数来创建COM对象,并得到该COM对象的接口指针。实际上COM还提供了另一个函数把以上两步操作封装起来: Extern “C”__stdcall HRESULT CoCreateInstance ( REFCLSID rclsid, //将要创建的COM对象的ID LPUNKNOWN pUnkOuter, //用于被聚合的情形 DWORD dwClsContext, //指定组件的类别,进程内或进程外 REFIID riid, //COM接口的ID比如IID_IDictionary void** ppv //用来保存COM接口指针。 ); 它的实现方式可以是这样的:
HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID FAR* ppv) { IClassFactory *pCF; HRESULT hr; Hr=CoGetClassObject (clsid,dwClsContext,NULL,IID_IClassFactory,(void**)&pCF); If(FAILED)(hr)) return hr; Hr=pCF->CreateInstance(pUnkOuter,iid,(void**)&ppv); pCF->Release(); Return hr;} 使用这种方式把类厂屏蔽起来了,使得客户看不到类厂接口指针,方便客户的使用。 当然用户也可以使用CoGetClassObject先得到类厂指针,然后使用类厂指针去创建COM对象. 客户使用类厂创建COM对象的过程如下图:
客户程序 调用CoCreateInstance 调用CoGetClassObject 获得类厂接口指针 创建对象 使用对象 组件程序 DllGetClassObject 创建类厂 返回类厂接口指针 COM对象 类 厂 COM库 CoGetClassObject 根据注册表找到DLL 装入DLL程序 调用DllGetClassObject函数 返回类厂接口 6 1 9 7 10 8 4 5 3 2 1.CoCreateInstance 调用CoGetClassObject 2. CoGetClassObject 根据注册表找到Dll的路径并加载到内存中 3. CoGetClassObject调用组件程序的引出函数DllGetClassObject 4. DllGetClassObject函数创建类厂 5. DllGetClassObject函数把类厂接口指针返回给CoGetClassObject函数 6.CoGetClassObject函数把类厂接口指针返回给CoCreateInstance函数 7.CoCreateInstance函数得到类厂接口指针后,调用类厂的CreateInstance函数 8.类厂创建COM对象 9.类厂把COM对象的接口返回给CoCreateInstance函数,CoCreateInstance函数返回 10.客户可以通过接口使用COM对象提供的服务
3.5 类厂对组件生存期的控制 概念澄清: COM对象的引用计数是对COM对象的生存期的控制。组件指DLL或EXE, 系COM对象的载体. 客户有可能在一个载体内创建同一种COM对象类的多个对象. 每个对象及其接口指针的引用计数机制来对该对象进行生存期的控制. 而组件的生存期,是指组件何时可以从内存中卸载的时期. 当然,组件的生存期要比单个COM对象的生存期要长. 以下的讨论,我们假设组件中只有一种COM对象. 而对于有多种COM对象的情形, 完全可以类似地处理. 一般情况下,客户只是在创建COM对象的时候要用到类厂接口指针,创建完后就把类厂对象丢弃了。为了效率等原因,客户可能需要控制组件程序的生存期。因为如果组件程序被释放后,客户可能在将来还要重新加载,而且此时由于类厂对象也随着组件程序一起被销毁,客户再使用此接口指针会出错。因此,如果客户能控制其生存期,客户可以在将来继续使用类厂接口指针,以便创建新的COM对象,这种情况下可能会提高程序的工作效率。 类厂接口的LockServer函数正是为了这个目的而设置的。 LockServer函数的实现:
在组件程序中定义一个全局变量 ULONG g_LockNumber = 0; HRESULT CDictionaryFactory::LockServer(BOOL bLock) { if (bLock) g_LockNumber ++; else g_LockNumber --; return NOERROR; } 为了准确地判断组件程序能否卸载,我们还需要引入一个全局变量以记录COM对象的个数。 ULONG g_DictionaryNumber = 0; 在Cdictionary的构造函数和析构函数中分别进行增1和减1操作。
这样当锁计数器和组件对象个数计数器都为零是组件程序就可以安全卸载了。 extern "C" HRESULT __stdcall DllCanUnloadNow(void) { if ((g_DictionaryNumber == 0) && (g_LockNumber == 0)) return S_OK; else return S_FALSE; } 而这个引出函数是当客户执行CoFreeUnusedLibraries时,由COM库调用的函数。 通过COM对象引用计数器、组件对象个数计数器、锁计数器的引入,客户可以真正安全、方便且灵活地控制COM逻辑对象以及其运行载体。
至此,COM组件的四个引出函数已经全部介绍完毕: DllGetClassObject DllRegisterServer DllUnRegisterServer DllCanUnloadNow 我们也接触到了COM库中最为常用的几个函数CoGetClassObject CoCreateInstance CoFreeUnusedLibrary 我们所说的COM库是MS提供的在Windows平台上的COM的实现。主要的可执行程序为rpcss.exe ( SCM, Service Control Manager 服务控制管理器), ole32.dll和oleaut32.dll,包括几十个函数,其中最重要的我们已经接触到。在使用COM库提供的函数之前要先调用CoInitialize函数对COM库进行初始化,使用完以后使用CoUnInitialize以释放COM库所维护的资源。
4 COM组件与客户程序的交互过程 以下是一个小结, 整理出客户程序创建COM对象,使用COM接口,以及最后释放资源的全过程.
客户程序 COM库 组件程序 CLSID clsid; IClassFactory *pClf; Iunkown *pUnkown; CoInitialize(NULL); CLSIDFromProgID(“Dictionary”,&clsid); COM在注册表中查找字典的CLSID CoGetClasObject(clsid,CLSCTX_INPROC_SERVER,NULL,IID_IClassFactory,(void**)&pClf; COM库在内存中查找clsid组件,如果没有装入内存,从注册表中获取其路径加载到内存中。然后调用DllGetClassObject 创建类厂对象CDictionaryFactory,并返回IClassFactory接口 COM库返回IClassFactory接口给客户 pClf->CreateInstance(NULL,IID_Iunkown,(void**)&pUnkown); 类厂对象的CreateInstance函数通过组件的vtalbe被客户直接调用。 New Cdictionary; 返回Iunkown指针
客户程序 COM库 组件程序 客户使用字典对象,通过接口指针进行各种操作 。。。。。。 响应客户的操作 pClf->Release(); pUnkown->Release(); 组件对象的Release被调用。如果计数为零,则删除自己。 CoFreeUnusedLibraries(); COM库调用字典组件的引出函数DllCanUnloadNow() DllCanUnloadNow中:如果不存在字典对象,且锁计数器为零则返回真,否则为假 如果DllCanUnloadNow中返回真则CoFreeLibrary();卸载组件程序 CoUninitialize(); COM库释放资源 客户程序退出