Thursday, March 24, 2016

coding 那么IID到底是什么东西呢?说到底,IID就是一个128比特的一个GUID结构;GUID是什么?GUID的英文是Globally Unique Identifier,就是全局唯一标识符的首字母缩写。

COM编程——GUID和注册表

什么是GUID?

做COM开发,就不得不去了解IID了,IID作为每一个接口的唯一标识符;我之前也有像下面这样定义一个IID:
// {2A06BBB3-667C-4D51-A8AD-F3CFDD7EF682}
static const IID IID_IX =
{ 0x2a06bbb3, 0x667c, 0x4d51, { 0xa8, 0xad, 0xf3, 0xcf, 0xdd, 0x7e, 0xf6, 0x82 } };
那么IID到底是什么东西呢?说到底,IID就是一个128比特的一个GUID结构;GUID是什么?GUID的英文是Globally Unique Identifier,就是全局唯一标识符的首字母缩写。

为什么要使用GUID?

为什么要使用GUID来标识接口而不用一个长整数呢?一个长整数可以标识2的32次方个接口的;但是,我们很多时候是不关心能标识多少个接口的,而是需要保证每个接口标识的唯一性,为什么呢?想想我们的QueryInterface的实现:
HRESULT __stdcall CA::QueryInterface(const IID &iid, void **ppv)
{
     if (iid == IID_IUnknown)
     {
          cout<<"Query IID_IUnknown:"<<endl;
          *ppv = static_cast<IX *>(this);
     }
     else if (iid == IID_IX)
     {
          cout<<"Query IID_IX:"<<endl;
          *ppv = static_cast<IX *>(this);
     }
     else if (iid == IID_IY)
     {
          cout<<"Query IID_IY"<<endl;
          *ppv = static_cast<IY *>(this);
     }
     else
     {
          cout<<"Query interface not supported:"<<endl;
          *ppv = NULL;
          return E_NOINTERFACE;
     }
     reinterpret_cast<IUnknown *>(*ppv)->AddRef();
     return S_OK;
}
是的,我们需要根据IID返回对应的接口的指针;全球那么多的程序员在进行着COM开发,我们必须要保证接口标识的唯一性,如果有两个接口存在相同的标识符,那么QueryInterface该返回哪个接口的指针?,如果一旦这种问题出现了,程序肯定会出现不可预期的行为。
GUID则提供了一种更好的解决方案。在不用任何中央授权机构进行协调的情况下,可以用编程的方法来生成具有唯一性的GUID。在Visual Studio的菜单的TOOLS中有一个Create GUID工具,使用这个工具可以生成一个唯一的GUID,工具截图如下:
果冻想 | 一个原创文章分享网站
其实Create GUID这个工具实际上调用了Microsoft COM库中的CoCreateGuid函数。

GUID的声明和定义

我之前的代码中都是这样定义的:
// {2A06BBB3-667C-4D51-A8AD-F3CFDD7EF682}
static const IID IID_IX = 
{ 0x2a06bbb3, 0x667c, 0x4d51, { 0xa8, 0xad, 0xf3, 0xcf, 0xdd, 0x7e, 0xf6, 0x82 } };
当然了,我们也可以使用objbase.h中所定义的DEFINE_GUID宏来定义一个GUID:
// {6BED30F3-8FC7-4E12-8F5E-7E902F917312}
DEFINE_GUID(<<name>>,
0x6bed30f3, 0x8fc7, 0x4e12, 0x8f, 0x5e, 0x7e, 0x90, 0x2f, 0x91, 0x73, 0x12);

GUID的使用

GUID的使用非常简单,很多时候,我们会对GUID进行比较判断,可以直接进行==符号进行判断两个GUID是否相等。同时,我之前都是将GUID作为接口的标识符,其实,我们也是将GUID作为组件的标识符。同接口一样,所有的组件都有一个不同的ID,为了将类标识符同IID区分开来,与类标识符相应的类型为CLSID;两个组件可以实现相同的接口集,但是每一个组件仍然必须具有各不相同的CLSID。在不改变类标识符的情况下,可以向组件中加入新的接口。但是若对某个组件的修改将会妨碍已有应用程序的正常运行,那么必须要为组件分配一个新的CLSID。
在使用GUID是,需要特别注意,由于GUID是128位的,在函数之间传递GUID时,请最好使用引用传参的方式。可以将参数定义为const IID&,也可以使用等价的REFIID。类似的,在传递类标识符时,可以使用REFCLSID;而在传递GUID值时可以使用REFGUID。

Windows注册表

如果对COM编程不熟悉,可能很惊讶,为什么总结COM编程,要总结到Windows的注册表呢?只要是从事Windows平台的开发,就不得不对注册表有所了解。COM组件就是用CLSID作为关键字在注册表中发布包含它们的DLL文件名称,然后CoCreateInstance将用CLSID作为关键字在注册表中查找所需的文件名称。
在Windows中,注册表是Windows操作系统的一个正式的共享系统数据库。注册表中包含关于系统的各种信息,可以说只有你想不到,没有你找不到的一个数据库。所有基于Windows的程序都可以向注册表中加入若干信息,并可以从中读取所需要的信息。客户可以在注册表中搜索它们所需要的组件。
关于注册表的结构,我就不在这里多说什么了。你可以使用regedit命令打开注册表看看就知道了。还是把重点放在COM在注册表中的表现形式吧。

CLSID关键字结构

注册表那么大,那么复杂;对于COM来说,只使用了注册表的一个分支:HKEY_CLASSES_ROOT。在这个关键字下面,可以找到一个CLSID关键字,在CLSID下面就是系统中安装的所有组件的CLSID。就是下面这样的结构:HKEY_CLASSES_ROOT\CLSID\{00000010-0000-0010-8000-00AA006D2EA4}
可以看到,每个CLSID的关键字都是类似00000010-0000-0010-8000-00AA006D2EA4这样的结构,而按照CLSID来查看注册表将是一件极不轻松的事情,这就是为什么每一个CLSID关键字都用组件的有意义的名称来作为其缺省值。
对于每一个CLSID关键字,我们现在关心的只是它的一个子关键字InprocServer32。这个关键字的缺省值就是组件所在的DLL文件名称。CLSID和文件名称是注册表中记录的两种最重要的信息,我们可以在不向注册表中加入任何有关组件信息的情况下编写出大量的组件。
在注册表的HKEY_CLASSES_ROOT下面还有一些与COM编程有关的关键字需要讲解:
  1. AppID;这个关键字下的子关键字的作用是将某个APPID(应用程序ID)映射成某个远程服务器名称。分布式COM将用到此关键字;
  2. Interface;这个关键字用于将IID映射成与某个接口相关的信息,这些信息主要用于在跨越进程边界使用接口的情况;
  3. Licenses;Licenses保存的是授权使用COM组件的一些许可信息;
  4. TypeLib;类型库关键字所保存的是关于接口成员函数所用参数的信息;这个关键字可以将一个LIBID映射成存储类型库的名称;
  5. Component Categories;注册表的这一分支可以将CATID(组件类别ID)映射成某个特定的组件类别。

ProgID

什么又是ProgID啊?其实,在HKEY_CLASSES_ROOT下面,大多数关键字都是ProgID,比如FunctionDiscovery.WCNProvider。所谓的ProgID指的是程序员给某个CLSID指定的一个程序员容易记住的名字。某些编程语言将使用ProgID而不是CLSID来标识组件。但是没有办法来保证ProgID是唯一的,因此名字冲突将是一个潜在的问题;但是对于ProgID处理起来却是容易很多的。
ProgID一般都是按照<Program>.<Component>.<Version>的格式进行命名的;这不是一定要遵守的规则,有的ProgID就没有按照这个进行命名。有的时候,组件经常会有一个与版本号无关的ProgID。这个ProgID被映射成所安装的最新版本的组件。与版本无关的ProgID的命名约定是将上述约定中的版本号去掉。
ProgID的主要作用是获取相应的CLSID,由于在每一个CLSID项中查找某个ProgID将是非常低效的,因此ProgID是在HKEY_CLASSES_ROOT的下面直接列出的。它在注册表中的结构如下:
果冻想 | 一个原创文章分享网站

从ProgID到CLSID的转换

将所需要的信息保存到注册表之后,进行CLSID和ProgID之间的转换将是非常容易的。COM库为此提供了两个函数CLSIDFromProgID和ProgFromCLSID,用来完成所需要的注册表处理。
CLSID clsid;
CLSIDFromProgID(L"Cmiv2.CmiFactory.2", &clsid);

自注册

现在我们都知道注册表中保存了组件的重要信息;但是,这些信息是如何写进注册表的呢?由于DLL知道它所包含的组件,因此DLL可以完成这些信息的注册。由于DLL本身不能运行,因此在DLL中一定要输出以下两个函数,由其它程序调用DLL完成注册。
extern "C" HRESULT __stdcall DllRegisterServer();
extern "C" HRESULT __stdcall DllUnRegisterServer();
DLL导出了这两个函数,然后,我们就可以使用系统提供的regsvr32.exe工具来调用对应的DLL完成注册。
DllRegisterServer用来完成注册,DllUnRegsiterServer用来完成反注册;它们的实现原理就是调用Win32的注册表操作函数完成对注册表的写操作。

组件类别

组件类别就是HKEY_CLASSES_ROOT键下的Component Categories。一个组件类别实际上就是一个接口集合。该集合将被分配给一个GUID,此GUID此时将被称作是CATID。对于某个组件而言,若它实现了某个组件类别的所有接口,那么它就可以将其注册成该组件类别的一个成员。对于将自己注册为某个组件类别的组件,它将保证它可以支持此组件类别的所有成员接口;就好比C++中的抽象类一样,派生类必须实现抽象类中的全部纯虚函数。
对于组件类别,我们不需要自己完成注册表的处理,这些工作可以由Windows系统附带的一个名为Component Category Manager来完成。CLSID_StdComponentCategoriesMgr是一个实现了两个主要接口的COM组件:ICatRegister和ICatInformation。ICatRegister可以完成新组件类别的登记或取消注册;ICatInformation则可用于获取系统中某个组件类别的数据。

OleView工具

果冻想 | 一个原创文章分享网站
当我们注册完我们的组件以后,就可以使用该工具进行查看了;如果没有找到,则说明注册过程中出现问题了。该工具的使用非常简单,这里就不过多的说明了。

总结

好了,COM中的这些零碎知识点又搞定一篇了。下一篇再见。我坚信,分享使我们更加进步。如果你喜欢,请分享这篇文章。
2014年1月13日 于大连,东软。

No comments:

Post a Comment