原文:http://www.codeproject.com/Articles/13601/COM-in-plain-C
如何在不使用MFC、ATL、WTL或其他框架下使用标准C来创建和使用COM组件
下载例程-17.6kb
内容:
有大量例子教大家如何使用和创建COM/OLE/ActiveX组件,但这些例子大都使用了微软基础类(MFC)、.NET、C#、WTL,至少会使用了ATL,因为这些框架会提供给你一些已经“封装” 好了的模板代码。不幸的是,这些框架对程序员隐藏了所有底层细节,所以你永远不会真正明白使用COM组件的本质。更好的掌握如何使用一个特定的框架是建立在你熟练掌握COM的基础上。
如果你正尝试不使用MFC、WTL、.NET、ATL、C#、或者甚至一些C++代码,只使用标准的C来处理COM对象,则这方面的例子和资料非常匮乏。本文就是介绍在不使用其他框架,只使用标准C创建COM组件文章系列的第一部分。
对于标准的Win32控件,例如Static、Edit、ListBox、ComboBox等,你可以获得一个控件的句柄(也就是一个HWND)并通过发消息(通过 SendMessage)给它来操纵它。同时当这个控件要通知你一些事情或给你一些数据时,它也通过消息的形式返回给你(也就是通过把它们放入你自己的消息队列中,你再通过GetMessage获取他们)。
对于一个OLE/COM对象而言则完全不是这样。你不能来回发消息,取而代之的是,COM对象给你一些可以调用来操纵这个对象的一些函数指针。例如,一个IE浏览器对象会给你一 个函数指针,通过这个指针你可以调用来引发浏览器在你的一个窗口中去加载并显示Web页面。一个Office的对象会给你一个函数指针,你可以通过它加载一个文档。如果COM对象要通知你一些事情或发给你一些数据,那么你需要在你的程序中写特殊的函数,提供这些函数的指针(给COM对象)以便对象在需要的 时候调用它们。换句话说你需要在你的程序中创建你自己的COM对象。其中在C中真正麻烦是怎么定义你自己的COM对象。为了这样做,你需要知道一个COM对象的每个细节-这些原始的东西在预制的框架中对你而言则是隐藏的,在接下来的一系列文章中我将展示它。
总之,你通过调用COM对象中的函数来操纵它,同时它在你的程序中也是通过函数调用来通知你一些事情或传给你一些数据或通过其他方式与你的程序交互。这个方法类似于DLL中的函 数调用,就像在你的C程序中DLL也要调用函数一样-有几分像“回调”。但是与DLL不同的是,你不能用LoadLibrary()和GetProcAddress()去获得这个COM对象的函数指针。我们马上就会揭示它,你需要使用一个与之不同的操作系统函数来获得一个对象指针,然后 用这个对象去获得指向它的函数的指针。
在学习怎样使用COM对象之前,我们首先需要了解一下COM对象是什么。认识它的最好的方式是创建我们自己的COM对象。但在我们这样做之前,让我们给出一个C结构数据类型。作为一个C程序员,你应该对它相当熟悉。这是一个例子的定义,一个简单的结构(叫“IExample”),它包含两个成员-一个DWORD(通过“count” 成员名来存取)和一个80个字符长度的数组(通过“buffer” 成员名来存取)。
struct IExample { DWORD count; char buffer[80]; };让我们用typedef来使它可以提前使用:
typedef struct { DWORD count; char buffer[80]; }IExample;接下来是一个对于这个结构分配一个实例的例子(忽略了错误检查),同时初始化它的成员:
IExample * example; example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample)); example->count = 1; example->buffer[0] =0;
你知道一个结构可以存储一个指向函数的指针嘛?希望你知道,这是个例子。我们有一个参数是char*的函数,返回值是个long。这是我们的函数:
long SetString(char *str) { return(0); }
现在我们需要把这个指向这个函数的指针存储在IExample中。在这里我们定义IExample,添加一个成员(“SetString”)来存储指向上面的函数的指针(并且我也用了一个typedef来使它提前可用):
typedef long SetStringPtr(char *); typedef struct { SetStringPtr * SetString; DWORD count; char buffer[80]; } IExample;
接下来是我们在分配的IExample中给SetString指针赋值,然后用这个指针调用来调用SetString:
example->SetString =SetString; long value =example->SetString("Some text");
好,可能我们需要存储两个函数指针。这是第二个函数:
long GetString(char *buffer, long length) { return(0); }
让我们重新定义IExample,添加另一个函数成员(“GetString”)来存储指向第二个函数的指针:
typedef long GetStringPtr(char *, long); typedef struct { SetStringPtr * SetString; GetStringPtr * GetString; DWORD count; char buffer[80]; } IExample;
接下来我们初始化这个成员:
example->GetString= GetString;
但你可能会说我不想把函数指针直接存储在IExample中。相反的,我们更愿意使用一个函数指针数组。例如,让我们定义第二个结构来达到存储我们的两个函数指针的目的。我们将叫它IExampleVtbl结构,它的定义是这样的:
typedef struct { SetStringPtr * SetString; GetStringPtr * GetString; } IExampleVtbl;
现在,我们把指向上面的数组的指针存储在IExample中。我们要添加一个叫“lpVtbl”的新成员来达到这个目的(当然,我们得删除SetString和GetString成员,因为他们已经挪到IExampleVtbl结构中了)
typedef struct { IExampleVtbl * lpVtbl; DWORD count; char buffer[80]; } IExample;
所以下面是一个分配内存并初始化IExample的例子(当然,包括IExampleVtbl):
// 由于IExample_Vtbl的内容永远不会改变, // 所以我把它定义为静态的并且用以下方法初始化它。 // 它可以被大量的IExample实例复制。 static const IExampleVtbl IExample_Vtbl = {SetString, GetString}; IExample * example; // 创建 (分配内存) 一个IExample结构. example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample)); // 初始化IExample(也就是把指向IExample_Vtbl赋值给它). example->lpVtbl =&IExample_Vtbl; example->count = 1; example->buffer[0] =0;
接着可以这样调用我们的函数:
char buffer[80]; example->lpVtbl->SetString("Sometext"); example->lpVtbl->GetString(buffer,sizeof(buffer));
此外需要说明的是,在我们的函数中可能需要通过访问结构中的“count”和“buffer”成员来调用他们。所以我们要做的是总要把指向这个结构的指针作为第一个参数传入。让我们重写我们的函数来达到这一点:
typedef long SetStringPtr(IExample *, char *); typedef long GetStringPtr(IExample *, char *, long); long SetString(IExample *this, char * str) { DWORD i; // 把传入的str拷贝到IExample的buffer中 i = lstrlen(str); if (i > 79) i = 79; CopyMemory(this->buffer, str, i); this->buffer[i] = 0; return(0); } long GetString(IExample*this, char *buffer, long length) { DWORD i; // 拷贝IExample的buffer到传入的buffer中 i = lstrlen(this->buffer); --length; if (i > length) i = length; CopyMemory(buffer, this->buffer, i); buffer[i] = 0; return(0); }
当调用IExample结构的函数时把它的结构指针传入:
example->lpVtbl->SetString(example,"Some text"); example->lpVtbl->GetString(example,buffer, sizeof(buffer));
如果你曾经用过C++,你可能认为:等一下,它好像很眼熟啊。是的,我们上边做的就是用标准C来创建一个C++类。IExample结构实际上是一个C++类(一个不继承于 其他任何类的类)。一个C++类实际上除了第一个成员总是一个数组指针,这个数组包含所有类成员函数的指针,与结构没什么差别。并且每个函数的第一个参数总是类(也就是结构)本身的指针。(它也就是隐藏的this指针)
简单说来,一个COM对象实际上就是一个C++类。你现在可能会认为:“哇噻!IExample现在就是一个COM对象嘛?这就是它的全部嘛??它就这么简单!”打住!IExample正在接近这一点,但对于它还有很多,它不会这么容易。如果它是这样,它就不会是微软技术了,现在做什么?
首先,让我先来介绍一下COM术语。你看到上面的指针数组-IExampleVtbl结构了嘛?COM文档中把它定义为接口或虚表。
一个COM对象在虚表(也就是我们的IExampleVtbl结构)中首先需要有三个被命名为QueryInterface、AddRef和Release的函数。 当然,我们也必须写这三个函数。微软已经把这三个函数的调用参数,返回值和调用约定指定好了。我们需要#include一些微软的包含文件(他们在你的C编译器包中,或者你下载的微软的SDK中)。我们这样重新定义我们的IExampleVtbl结构:
#include<windows.h> #include<objbase.h> #include<INITGUID.H> typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef struct { // 前3个成员必须叫是QuryInterface、AddRef和Release QueryInterfacePtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl;
让我们查看typedef过的QueryInterface。首先,这个函数返回一个HRESULT,它被简单定义为LONG。接着,它用了STDMETHODCALLTYPE。这意味参数不通过寄存器传递,而是通过栈。并且也约定了谁来平栈。事实上,对于COM对象,我们应该确保所有我们的函数都被定义为STDMETHODCALLTYPE,并返回一个LONG(HRESULT)。QueryInterface的第一个参数是用于函数调用的对象指针。我们难道不是在把IExample转化为一个COM对象嘛?是 的,这也是我们要传递的参数的原因。(记住确保传递给我们函数的第一个参数是一个用于调用这些函数的结构指针?COM完全强制依赖以上的定义)
稍后,我们展示一个REFIID是什么,并且也提到QueryInterface的第三个参数,注意AddRef和Release也传递同样的我们用于调用他们的结构指针。
好,在我们没有忘记前,让我们添加HRESULTSTDMETHODCALLTYPE到SetString和GetString:
typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long); HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str) { ... return(0); } HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, long value) { ... return(0); }
总之,一个COM对象基本上是一个C++类。这个C++类是一个总是以它的虚表指针(一个函数指针数组)为起点的结构。并且在虚表中最开始的三个函数总是被命名为QueryInterface、AddRef和Release。额外的函数也可以出现在虚表中,它们的名字依赖对象它自身的定义。(你决定要加入你的 COM对象中的函数)。例如,IE的Browser对象勿庸置疑有与播放音乐对象不同的函数。但是所有的COM对象都以它们的虚表指针开始,最开始的三个虚表指针指向对象的QueryInterface、AddRef、和Release函数。一个对象的函数的第一个参数是一个指向对象(结构)自身的指针。 这是一个约定,一定要遵守。
让我们继续我们的构造IExample为一个真正的COM对象之旅。现在要写我们的QueryInterface、AddRef和Release函数。但在我们动手之前,我们必须谈谈一个叫全 局唯一表示符(GUID)的东东。哦,它是什么?它是一个用特殊的一连串字节填充的16字节数组。当我说它是特殊的时候,我的意思是唯一。一个GUID(也就是16字节数组)不能与另一个GUID有同样的字节序列,无论何时何地。每个GUID在任何时候被创建都有唯一的16位序列数。
那么你怎样创建这个唯一的16位序列呢?你可以用一个微软的GUIDGEN.EXE工具。它打包在你的编译器中,或者你也可以在SDK找到它。运行它你会看到这个窗口:
当你一运行GUIDGEN时,它自动生成一个新的GUID给你,显示在Result框中。注意在你的Result框中看到的会与上面的有所不同。毕竟,每个 单一的GUID生成与其他的是不同的。所以你最好看到一些与我看到的不同的东东。继续单击“NewGUID”按钮会看到一些不同的数字出现在Result框中。单击一整天,看看是否会生成同一个序列数超过一次,不会。同时,也没人会生成一些与你生成的序列相同的数。
你可以单击“Copy”按钮来把这个信息传输到剪切板上,然后把它粘贴到其它地方(像你的源代码中)。这是我这样做,粘贴完的东东:
//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(<<name>>,0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce,0x24, 0xc2);
上面是一个宏,一个#define在微软的包含文件中,它会告诉你的编译器把上面的内容编译成一个16位数组。
但是有一个事情我们必须做。我们必须用一些我们要用的这个GUID的C变量名来替换<<name>>。我们叫它CLSID_IExample.
//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2} DEFINE_GUID(CLSID_IExample,0xb5b3d8e, 0x574c, 0x4fa3, 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24,0xc2);
现在我们有了一个可以用于IExample的GUID。
我们还需要一个GUID给IExample的虚表(“接口”),也就是,我们的IExampleVtble结构。所以继续单击GUIDGEN.EXE的“NewGUID”按钮,并拷贝、粘贴到其他地方。这次,我们将用一个命名为IID_IExample的C变量名来替换<<name>>。下面是我粘贴、编辑过的结果:
//{74666CAC-C2B1-4fa8-A049-97F3214802F0} DEFINE_GUID(IID_IExample,0x74666cac, 0xc2b1, 0x4fa8, 0xa0, 0x49, 0x97, 0xf3,0x21, 0x48, 0x2, 0xf0);
总之,每个COM对象有它自己GUID,每个GUID是由不同的16位字节数组组成。一个GUID可以通过GUIDGEN.EXE工具生成。一个COM对象的虚表(也就是接口)也得有一个GUID。
当然我们要允许其他程序来获得我们创建、初始化的IExample结构(也就是一个COM对象),那么这个程序就可以调用我们的函数了。(我们先不给出另一个程序怎样来获得我们的IExample。我们将在后面讨论它)。
除我们自己的COM对象以外,可能有很多其他COM组件安装在一个特定的计算机上。(再次,我们将推后讨论怎样安装我们的COM组件。)不同的计算机可能安装了不同的COM组件。一个程序怎样确定我们的IExampleCOM对象是否已经安装了,怎样来把它与其他所有COM对象区别开来?
记住每个COM对象有一个完全唯一的GUID,我们的IExample对象也是。我们的IExample虚表也有一个GUID。我们需要做的是告诉这个程序的开发者 IExample对象和它的虚表的GUID。通常,你给他一个包含上面你用GUIDGEN.EXE获得的两个GUID的宏的文件(.H)。OK,这样其它程序就知道IExample和它的虚表的GUID。它可以用他们做什么呢?
在这我们的QueryInterface函数派上用场了。记住每个COM对象必须有一个QueryInterface函数(也得有AddRef和Release)。其它程序会传递我们 IExample的虚表的GUID给我们的QueryInterface函数,我们检查它并确认它是IExample虚表的GUID。如果它是,那么我们会返回一些值来让这个程序知道它确实拥有了一个IExample对象。如果传入一个错误的GUID,我们会返回一些错误值让它知道它没有获得这个 IExample对象。所以,一台计算机上的所有COM对象,除了我们自己的QueryInterface,如果传给他们的QueryInterface一个IExample虚表的GUID都会返回一个错误值。
传给QueryInterface的第二个参数是我们要检查的GUID。如果传入的GUID与我们的IExample的虚表GUID匹配,我们会返回给传给我们的同一个对象指针给第三个参 数(一个句柄)。如果不匹配,我们把这个句柄置零。另外,如果这个GUID匹配QueryInterface返回一个NOERROR(被#define为0)的LONG值。如果不匹配返回非零错误值(E_NOINTERFACE)。那么,让我们来看一下IExample的QueryInterface:
HRESULT STDMETHODCALLTYPE QueryInterface(IExample *this, REFIID vTableGuid,void **ppv) { // 检查GUID是否与IExample的虚表的GUID相匹配。 // 记得我们给出了一个C变量的IID_IExample对应与我们的虚表GUID。 // 我们可以用一个OLE的IsEqualIID的函数来比较 if(!IsEqualIID(riid, &IID_IExample)) { //我们不认可传给我们的GUID,通过清除调用这的句柄来让它知道, // 返回E_NOINTERFACE *ppv = 0; return(E_NOINTERFACE); } // 它是匹配的! // 首先我们用同一个它传给我们的对象指针来填充它的句柄。 // 这是我们创建/初始化的我们的IExample,它将从我们这里获的对象指针 *ppv = this; // 现在调用我们的AddRef函数,把this指针传给IExample this->lpVtbl->AddRef(this); // 让他知道他确实拥有了一个IExample return(NOERROR); }
现在让我们来讨论一下AddRef和Release函数。你会注意到如果我们真的拥有了一个IExample,我们会在QueryInterface中调用AddRef.. .
记住我们替其它程序分配IExample的空间。它只是简单获得它的使用权。当其它程序不用它时我们有责任释放它。我们怎样知道这些呢?
我们会使用一个名为“引用计数”的东东。如果你回头看看我们的IExample的定义,你会发现我放了一个DWORD成员(count)在它里面。我们将使用这个成员。当我们创建了一个IExample时,我们把它初始化为0。然 后,当AddRef每被调用一次我们会把这个成员增加1,当Release每被调用一次我们会把这个成员减1。 所以,当把我们的IExample传递给QueryInterface时,我们会调用AddRef来增加它的count成员。当其它程序使用完后,这个程序会把我们的IExample传递 给我们的Release函数,在那我们会对这个成员进行减操作。当它减为0时我们会释放IExample。
COM的另一个重要规则。如果你获得了一个其他人创建的COM对象,在你使用完后你必须调用Release函数。我们当然也认为其他程序在使用完我们的IExample对象后后调用我们的Release函数。
下面是我们的AddRef和Release函数:
ULONG STDMETHODCALLTYPE AddRef(IExample *this) { // 增加引用计数(count成员) ++this->count; // 我们应该返回这个更新后的count。 return(this->count); } ULONG STDMETHODCALLTYPE Release(IExample *this) { // 减少引用计数 --this->count; // 如果它现在为0,我们要释放IExample if (this->count == 0) { GlobalFree(this); return(0); } // 我们应该返回这个更新后的count。 return(this->count); }
我们接下来还要做一些其它事情。微软定义了一个叫IUnknown的COM对象。它是什么呢?一个IUnknown对象就像IExample,它的虚表只包含QueryInterface、AddRef和Release 函数(也就是说,它不包含像我们的IExample虚表中的SetString和GetString一类的附加函数)。换句话说,一个IUnkown是一个空的最小的COM对象。微软给IUnknown对象创建了一个特殊的GUID。但你知道吗?我们的IExample对象也可以装扮成一个IUnkown 对象。毕竟在它里面有QueryInterface、AddRef和Release函数。如果他们关心的仅仅是前三个函数,没人需要知道它其实是一个IExample对象。如果其它程序传递给我们的是IExample的GUID或者一个IUnkown的GUID,我们只要更改一行代码返回成功就可以 了。顺便说一下,微软的头文件中给IUnknown的GUID起了一个IID_IUnkown的C变量名:
//检查这个GUID是否与IExample的GUID或IUnknown的GUID匹配 if(!IsEqualIID(vTableGuid, &IID_IExample) && !IsEqualIID(vTableGuid,&IID_IUnknown))
总之,对于我们自己的COM对象,我们替其它程序(他们获得是对象的使用权,用它来调用我们的函数)分配对象的空间。我们负责释放对象的空间。我们通过AddRef和Release 管理引用计数来达到对象安全。我们的QueryInterface允许其它程序检查对象是否是他们需要的,也允许我们来增加引用计数。(事实上,QueryInterface还提供一个不同的功能,这个我们以后再讨论,但此时,知道这些就够了)那么,IExample现在是一个真正的COM对象了吗?这是肯定的!太棒了! 不太难! 我们已经做到了!
等等!我们还需要把它封装成其它程序可以使用的形式(也就是说一个动态链接库),写一段专用的安装程序代码,让其它程序知道如何获得我们创建的IExmaple(这样我们还要写些代码)。
现在我们需要看一下一个程序如何获得我们的IExample对象,最终我们还必须写一些实现的代码。对于这一点微软已经设计了一个标准的方法,我们加入第二个COM对象(还用它的函数)到我们的DLL中。这个COM对象叫IClassFactory,它包括了一套特殊的已经被定义在微软的包含文件中的函数,它也拥有自己的已定义的GUID,同时给出了一个IID_IClassFactory的C变量名。
我们的IClassFactory的虚表有五个特殊函数,它们是QueryInterface、AddRef、Release、CreateInstance和LockServer。注意IClassFactory有自己的QueryInterface、AddRef和Release函数,就像我们的IExample对象那样。当然,我们的IClassFactory也是一个COM对象,所有的COM对象的虚表都必须以这三个函数开始。(但是为了避免与IExmaple的函数名字冲突,我们在我们IClassFactory函数名前加上“class”前缀,例如classQueryInterface、classAddRef和classRelease。在IClassFactory虚表定义时,它的前三个成员为QueryInterface、AddRef和Release,)
真正重要的函数是CreateInstance。只要程序要我们创建一个IExample对象、初始化对象和返回对象时,它就会调用我们IClassFactory的CreateInstance。事实上,如果程序需要几个IExample对象,它可以调用CreateInstance几次。好,一个程序就可以这样获得我们的IExample对象。你可以要问“但是其它程序怎样获得我们的IClassFactory对象呢?”我们一会再开始。现在,让我们简单写一下我们的IClassFactroy的五个函数,构造它的虚表。构造虚表比较简单。不像我们的IExample对象的IExampleVtbl,我们不必定义我们的IClassFactory的虚表结构。微软已经在包含文件中为我们定义了一个IClassFactoryVtbl结构。我们需要做的是声明我们的虚表和用我们的五个IClassFactory的函数指针来填充它。让我们用IClassFactory_Vtbl变量名来创建一个静态的虚表并填充它:
static const IClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface, classAddRef, classRelease, classCreateInstance, classLockServer};
同样的,创建一个实际的IClassFcactory对象是容易的,因为微软也已经定义了这个结构。我们仅仅需要他们中的一个,所以让我们用一个MyIClassFactoryObj变量名声明一个静态的IClassFactory,初始化它的lpVtble成员指向我们上面的虚表:
static IClassFactoryMyIClassFactoryObj = {&IClassFactory_Vtbl};
现在,我们只需要写上面的五个函数。我们的classAddRef和classRelease函数没什么用。因为实际上我们从不要分配我们的IClassFactory(也就是说,我们简单的把它声明为静态的),我们不需要释放任何东西。所以,classAddRef只是简单的返回一个1(指出总有一个IClassFactory存在)。ClassRelease也同样这么做,因为不需要释放它,对于我们的IclassFactroy也就不需要做引用计数。
ULONG STDMETHODCALLTYPE classAddRef(IClassFactory *this) { return(1); } ULONG STDMETHODCALLTYPE classRelease(IClassFactory *this) { return(1); }
现在,让我们看看我们的QueryInterface。它要检查是否传给它的GUID是IUnkown的GUID(由于我们的IClassFactory有QueryInterface、AddRef和Release函数,它也可以装扮成一个IUknown对象)还是IClassFactory的GUID。另外,我们要做像我们在IExample的QueryInterface中做的同样的事情。
HRESULT STDMETHODCALLTYPE classQueryInterface(IClassFactory *this, REFIID factoryGuid,void **ppv) { // 检查GUID是否与IClassFactory或Iuknown的GUID匹配。 if(!IsEqualIID(factoryGuid, &IID_IUnknown) && !IsEqualIID(factoryGuid,&IID_IClassFactory)) { // 不匹配,清除句柄,返回E_NOINTERFACE。 *ppv = 0; return(E_NOINTERFACE); } // 匹配 // 首先,我们用它传给我们的同一个对象指针填充它的句柄。 // 这是它从我们这里获得的我们的IClassFactory(MyClassFactoryObj) *ppv = this; // 传递IClassFacory,调用我们的IClassFactory的AddRef。 this->lpVtbl->AddRef(this); // 让他知道它确实拥有了一个IClassFactroy return(NOERROR); }
我们的IClassFactory的LockServer现在就是一个摆设:
HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock) { return(NOERROR); }
还有一个函数要写-CreateInstance。下面是它的定义:
HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *, IUnknown *, REFIID,void **);
通常,第一个参数会是一个指向我们的被用来调用CreateInstance的IClassFactory对象(MyIClassFactoryObj)指针。仅当我们实现了聚合,我们才使用第二个参数。我们现在先不理这个。如果它非零,就是有人要我们支持聚合,在这我们不支持,我们会通过返回一个错误来提示。第三个参数是这个IExample虚表的GUID(如果有人确实要我们来分配、初始化和返回一个IExample对象)。第四个参数是一个用于存放我们返回的我们创建的IExample对象的句柄。
现在让我们开始写我们的CreateInstance函数(被名字为classCreateInstance):
HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid,void **ppv) { HRESULT hr; struct IExample *thisobj; // 通过清除调用者的句柄显示错误 *ppv = 0; // 在IExample中我们不支持聚合 if (punkOuter) hr = CLASS_E_NOAGGREGATION; else { //创建我们的IExample对象并初始化。 if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(structIExample)))) hr = E_OUTOFMEMORY; else { // 存储IExample的虚表。我们把它声明为一个静态变量IExample_Vtbl thisobj->lpVtbl =&IExample_Vtbl; // 增加引用计数以便如果在调用QueryInterface()有错误时 // 我们可以在下面调用Release()并且它会销毁 thisobj->count = 1; // 用我们上面分配的IExample指针填充调用者的句柄。 // 我们让IExample的QueryInterface来做它, // 因为它也会检查调用者传入的GUID,如果一切正确它也会增加 // 引用计数(到2)。 hr = IExample_Vtbl.QueryInterface(thisobj,vTableGuid, ppv); // 减小引用计数 // 注意:如果在QueryInterface()中发生了一个错误, // 那么Release会减小计数到0 并替我们释放IExample。 // 当调用者寻找某种我们不支持的对象(也就是说它是一 // 个我们不认可的GUID)时可能发生错误。 IExample_Vtbl.Release(thisobj); } } return(hr); }
这样就实现了我们的IClassFactory对象了。
为了使其它程序更容易获得我们的IClassFactory(调用它的CreateInstance函数来获得IExample对象),我们要把上面的源代码打包成一个动态链接库(DLL)。本文不讨论怎样去创建一个DLL本身,所以如果你不熟悉它,你首先需要阅读一下关于DLL的指南。
上面,我们已经写了IExmaple和IClassFactory对象的所有代码。我们所需要做的就是把它们粘贴到我们的DLL源代码中。
但是还有一些事情要做。微软规定我们必须添加一个叫DllGetClassObject的函数到我们的DLL中。微软已经定义了它的传递参数、它该做什么和应该返回什么。其它程序会调用我们的DllGetClassObject来获得我们的IClassFactory对象指针。(事实上,就像我们以后看到得,程序会调用一个命名为CoGetClassObject的OLE函数,在它内部会调用我们的DllGetClassObject。)所以,这就是一个程序如何获得我们得IClassFactory对象的方法-通过调用我们的DllGetClassObject。我们的GetClassObject函数必须完成它的工作。这是它的定义:
HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, REFIID factoryGuid, void**factoryHandle);
第一个传递参数是IExample对象的GUID(不是它的虚表GUID)。我们需要检查它来做确定调用者是否是明确调用我们DLL的DllGetClassObject。注意每个COM的DLL在它里面都有一个DllGetClassObject函数。所以我们需要用GUID来区分我们的DllGetClassObject与其他COMDLL的DllGetClassObject。
第二个参数是IClassFactory的GUID。
第三个参数是个句柄,程序期望我们通过它返回我们的IClassFactory指针(如果这个程序确实传入入一个IExampleGUID,不是一个其他COM对象的GUID的话)。
HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, REFIIDfactoryGuid, void **factoryHandle) { HRESULT hr; // 检查调用者传入的IExample的GUID。看它是否是我们的DLL实现的COM对象。 if (IsEqualCLSID(objGuid,&CLSID_IExample)) { // 用我们的IClassFactory对象指针填充调用者的句柄。 // 我们让我们的IClassFactory的QueryInterface来做, // 因为它也检查IClassFactory的GUID和做其他事宜 hr =classQueryInterface(&MyIClassFactoryObj, factoryGuid,factoryHandle); } else { // 我们不能解析这个GUID。显然它不在我们的DLL中。 // 通过清除它的句柄和返回CLASS_E_CLASSNOTAVAILABLE来让调用者知道 *factoryHandle = 0; hr = CLASS_E_CLASSNOTAVAILABLE; } return(hr); }
我们几乎做完了我们需要创建DLL的工作。还有一个事要做。其实程序不会真正加载我们的DLL。而是当程序调用CoGetDllClassObject时,操作系统代替它来完成(也就是说,CoGetClassObject定位我们的DLL文件,对它调用LoadLibrary,用GetProcAddress来得到我们上面的DllGetClassObject,代替这个程序调用它)。不幸的是,微软没有设计出一些方法给程序,当程序使用完我们的DLL来告诉操作系统、让操作系统卸载(FreeLibrary)我们的DLL。所以我们必须帮助操作系统让它知道我们的DLL什么时候可以安全的卸载。所以我们必须提供一个叫DllCanUnloadNow的函数,当可以安全删除的时候返回S_OK,否则返回S_FALSE。
那么我们怎样知道它什么时候安全呢?
我们必须做一些引用计数。确切的说,每当我们分配一个对象给其它程序,我们必须对引用计数增一。其它程序每调对象的Release函数一次,我们要释放对象,同时对引用计数减少同样的次数。只有这个计数为零时我们通知操作系统我们的DLL可以安全的卸载了,因为这时我们知道程序不在使用我们的对象了。所以,我们声明一个叫OutstandingObjects的静态DWORD变量来保存这个计数。(当然,我们的DLL第一次被加载时,它需要被初始化为0。)
那么,在哪里增加这个变量最方便呢?在我们IClassFactory的CreateInstance函数中,在我们GlobalAlloc分配这个对象并确认所有的工作都正确后。这样,我们要在这个函数中,在调用Release返回正确后增加一行:
static DWORDOutstandingObjects = 0; HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, IUnknown *punkOuter, REFIID vTableGuid,void **ppv) { ... IExampleVtbl.Release(thisobj); // 如果一切正确增加外部对象的计数 if (!hr)InterlockedIncrement(&OutstandingObjects);; } } return(hr); }
那么,在哪里减小这个变量最方便呢?在我们IExample的Release函数的正确GlobalFree对象后。所以我们在GlobalFree后增加一行:
InterlockedDecrement(&OutstandingObjects);
但还没完。(这些混乱的细节微软永远不会结束?)微软还给出一个允许程序在内存中锁定我们DLL的方法。为此,它可以调用我们IClassFactory的LockServer函数,如果它要我们增加一次我们DLL的锁的次数则传入1,如果它要我们减少一次我们DLL的锁的次数则传入0。这样,我们需要第二个命名为LockCount的静态DWORD引用计数。(当然,当我们的DLL被加载时它也需要初始化为0)我们的LockServer函数现在变成这样:
static DWORD LockCount =0; HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this,BOOL flock) { if (flock)InterlockedIncrement(&LockCount); else InterlockedDecrement(&LockCount); return(NOERROR); }
现在我们准备写我们的DllCanUnloadNow函数:
HRESULT PASCAL DllCanUnloadNow(void) { // 如果有人要重新获得我们对象的指针,并且其他人还没有调用Release()。 // 那么我们返回S_FALSE来提示不能卸载这个DLL。 // 如果有人已经锁定了它,同样返回S_FALSE。 return((OutstandingObjects | LockCount) ?S_FALSE : S_OK); }
如果你下载这个例程,我们DLL的源文件(IExample.c)在IExample目录下。这个源文件也支持通过微软的VisualC++工程文件来创建一个DLL(IExample.dll)
像早些时候提到的,为了让C/C++写的程序使用我们的IExmaple DLL,我们需要把我们的IExample和它的虚表GUID给其他程序的作者。我们把这些GUID宏放在包含(.H)文件中,它会分发给其他人,它也包含在我们的DLL源代码中。我们也需要把IExmapleVtbl和IExample结构定义放在这个包含文件中,这样其它程序就可以通过我们给他的IExample来调用我们的函数了。
到目前为止,我们定义IExampleVtble和IExample结构如下:
typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **); typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *); typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *); typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *); typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long); typedef struct { QueryInterfacePtr *QueryInterface; AddRefPtr *AddRef; ReleasePtr *Release; SetStringPtr *SetString; GetStringPtr *GetString; } IExampleVtbl; typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } IExample;
上面代码有一个问题。我们不想让其它程序知道我们的“count”和“buffer”成员。我们要对其它程序隐藏它们。我们决不允许程序直接访问我们对象的数据成员。它应该只知道“lpVtbl”成员,通过它来调用我们的函数。所以,就程序相关性而言,我们要这样定义IExample:
typedef struct { IExampleVtbl *lpVtbl; } IExample;
最后,上面的C定义有一个问题。对于一个要使用我们的COM对象的C++程序员来说,它真不是容易的事。毕竟,即使我们用C写了IExample,我们的IExample结构是一个真正的C++类。但对于一个使用它的C++程序而言,把它定义成C++类比C结构要容易的多。
为了替换上面的定义,微软提供了一个我们可以使用它在某种程度上使C和C++都可以工作且隐藏额外数据成员的宏。为了使用这个宏,我们必须首先把我们的对象名(就是IExample)定义为INTERFACE。在这之前,我们必须undef这个符号来避免编译器的警告。然后我们使用DECLARE_INTERFACE_宏。在这个宏里面,我们列出我们的IExample函数。它看起来是这个样子的:
#undef INTERFACE #define INTERFACE IExample DECLARE_INTERFACE_(INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID,void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (SetString) (THIS_ char *)PURE; STDMETHOD (GetString) (THIS_ char *,DWORD) PURE; };
它可能看起来有点怪怪的。
当在定义一个函数时,只要函数返回HRESULT就要使用STDMETHOD。我们的QueryInterface、SetString和GetString函数返回的是HRESULT。AddRef和Release不是。后面的两个返回ULONG。这就是我们用STDMETHOD_(以下滑线结尾)替换它们两个的原因。然后我们把函数名字放入括号中。如果函数不返回HRESULT,我们需要放入它的返回值的类型,然后在这个函数名前加一个逗号。在函数名后,在括号中列出函数的参数。THIS是指向我们对象(也就是IExample)的一个指针。如果传给函数的仅仅是这个指针,那么你只需简单的把THIS放在括号中。AddRef和Release函数就是这种情况。但其它函数有额外的参数。所以我们必须用THIS_(以下划线结尾)。然后我们列出剩余参数。注意在THIS_和剩余参数之间没有逗号。但是每个剩余参数之间有逗号。最后,我们放一个PURE字和分号。
当然,这是一个不可思议的宏,通过这种方法定义COM对象,普通的C编译器和C++编译器都可以正常工作。
“但我们的IExample结构定义在哪里?”。你可能这么问。这个宏确实非常不可思议。它使C编译器自动生成只包含“lpVtbl”成员的IExample结构定义。所以仅通过这个方法定义我们的虚表,我们自动获得适合其它程序员的IExample定义。
粘贴我们的两个GUID宏到包含文件中,一切准备就绪。我这样创建了IExample.h文件。
但你知道,我们的IExample事实上有两个数据成员。所以我们必须做的是在我们的DLL源文件中定义一个IExample的“异型”。我们叫它“MyRealIExample”,它是我们的IExample的真正的定义。
typedef struct { IExampleVtbl *lpVtbl; DWORD count; char buffer[80]; } MyRealIExample;
我们在IClassFactory的CreateInstance中改变一行以便分配MyRealIExample结构:
if (!(thisobj =GlobalAlloc(GMEM_FIXED, sizeof(struct MyRealIExample))))
其它程序不需要知道我们给它的对象内部的额外数据成员(对其他程序隐藏有实际的目的)。毕竟,这两个结构拥有指向同一函数指针数组的同一“lpVtbl”。但现在我们的DLL函数可以通过铸造一个指向MyRealIExample指针的IExample指针来访问这些“隐藏的”成员。
我们也需要一个DEF文件来暴露DllCanUnloadNow和DllGetClassObject这两个函数。微软的编译器要求把它们定义为PRIVATE。这是我们的DEF文件,链接器依赖它:
LIBRARY IExample EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE
现在我们完成了构造IExample.dll所需的每件事。我们继续来编译IExample.dll。
但我们的工作还没完。在其它程序可以使用IExample对象(也就是这个DLL)前,我们需要做两件事情:
我们需要创建一个拷贝IExample.DLL到一个适当位置的安装程序。例如,或许我们在ProgramFiles目录下创建一个“IExample”的目录,把DLL拷贝到那。(当然我们的安装程序要做版本检查,这样如果我们的DLL的一个后续版本在那已经安装过了,我们就不用旧版本来覆盖它。)
我们接着需要注册这个DLL。这包括创建几个注册表键值。
我们首先需要在HKEY_LOCAL_MACHINE\Software\Classes\CLSID下创建一个键值。我们必须用我们的IExample对象的GUID作为这个新键值的名字,,而且它必须是一个特殊格式的文本字符串。
如果你下载了例程,在RegIExample目录下包含了一个IExample.dll安装程序。StringFromCLSID函数给出怎样把我们的IExample的GUID格式化成一个适合于用它来创建注册表键值名的文本字符串。
注意:这个安装例程在注册DLL前不拷贝DLL到适当的位置。这比较合理,它允许你选择任何地方来编译你的IExample.dll和注册它。这对于开发、测试来说更方便。一个优秀的安装程序产品应该把DLL拷贝到合适的位置,并且做版本检查。这些需要增加部分留给你来做你自己的安装程序。
在我们的“GUID键”下,我们必须创建一个名叫的InprocServer32子键。这个子键的缺省值要设置成我们的DLL安装的全路径。
如果我们不需要限制调用我们DLL函数的程序必须是单线程,我们还必须设置一个命名为ThreadingModel的值为“both”字符串值。由于我们在IExample函数没有用到全局数据,我们是线程安全的。
运行完我们的安装程序后,IExample.dll现在已经作为一个COM组件注册在我们的计算机上了,现在其它程序可以使用它了。
注意:UnregIExample目录里包含一个反安装IExample.dll的例子。它只删除RegIExample创建的注册表键值项。一个优秀的反安装产品应该删除IExample.dll和安装程序创建的目录。
现在我们准备写一个使用我们IExmapleCOM对象的C程序。如果你下载了例程,IexampleApp目录包含一个C例子程序。
首先,C程序要#include我们的IExample.h包含文件,这样它可以查询到IExample对象和它的虚表的GUID。
一个程序在使用COM对象前,它必须通过调用CoInitialize函数来初始化COM。它只需要初始化一次,所以在程序一开始的地方做比较好。
接下来,程序通过调用CoGetClassObject来获得IExample.dll的IClassFactory对象指针。注意我们传递IExample对象的GUID作为第一个参数。我们也传递一个我们的classFactory变量指针,如果一切正确,IClassFactory指针会通过classFactroy返回给我们。
一旦我们拥有了一个IClassFactory对象,我们可以调用它的CreateInstance函数来获得一个IExample对象。注意我们用IClassFactory调用它的CreateInstance函数的方法。我们通过IClassFactory的虚表(也就是它的lpVtbl成员)来得到这个函数。同时注意我们传IClassFactory指针作为第一个参数。记住这是一个标准的COM。
注意我们传递IExample的虚表的GUID作为第三个参数。对于第四个参数,我们传入一个我们的exampleObj的变量指针,如果一切正确,IExample对象指针会通过exampleObj返回给我们。
一旦我们拥有了一个IExample对象,我们可以释放IClassFactory对象。记住当程序使用完对象时必须调用对象的Release函数。IClassFactory是一个对象,就像IExample是一个对象一样。每个对象有自己的Release函数,当我们使用完对象时必须调用它。我们不再需要IClassFactory了。我们不再需要获得IExample对象了,也不需要调用IClassFactory的其它函数了,所以现在我们可以释放它了。注意它根本不影响我们的IExample对象。
所以接下来我们调用IClassFactory的Release函数。一旦我们这样做了,我们的classFactory变量不再包含一个有效的指针。它现在是一个垃圾了。
但我们还拥有IExmaple指针。我们还没有释放它。所以接下来我们决定调用IExample的函数。我们调用SetString。然后我们接着调用GetString。注意我们使用IExmaple指针调用它的SetString函数的方法。我们通过IExmaple的虚表获得这个函数。也要注意我们传递IExample指针作为第一个参数。因为它是一个标准的COM。
当我们最后使用完IExample后,我们要释放它。一旦我们这样做了,我们的exampleObj变量不再包含一个有效的指针。
最后,我们必须调用CoUninitialize让COM来清除内部的垃圾。它只需要做一次,所以它最好放在我们程序的结尾(但只有CoInitialize调用成功才需要这么做)。
也可以用CoCreateInstance函数替换CoGetClassObject(来获得DLL的IClassFactory),然后调用IClassFactory的CreateInstance。CoCreateInstance本身会调用CoGetClassObject,然后调用IClassFactory的CrateInstance。CoCreateInstance直接返回IExample,绕过获得 IClassFactory。这是使用的一个例子。
if((hr = CoCreateInstance(&CLSID_IExample, 0, CLSCTX_INPROC_SERVER,&IID_IExample, &exampleObj))) MessageBox(0, "Can't create IExampleobject", "CoCreateInstanceerror", MB_OK|MB_ICONEXCLAMATION);
IExampleCplusApp 目录下包含一个C++例程。它是像C例子一样正确。但你要注意有些重要的不同之处。首先,因为在IExample.h中宏把IExmaple定义为一个C ++类(而不是一个结构),并且因为C++用特殊的方式处理类,C++程序以不同的格式调用我们的IExample函数。
在C中我们直接通过访问虚表(通过lpVtble成员)来获得IExample函数,我们总是传入IExample作为第一个参数。
C++编译器知道把一个类的虚表作为它的第一个成员,自动访问它的lpVtbl成员来获得它里面的函数。所以我们不必指定lpVtbl部分。C++编译器也会自动传递这个对象作为第一个参数。
那么尽管在C中,我们的代码:
classFactory->lpVtbl->CreateInstance(classFactory,0, &IID_IExample,&exampleObj);
而在C++中,我们代码改为:
classFactory->CreateInstance(0,IID_IExample, &exampleObj);
注意:我们也省略IID_IExample的GUID的&。C++的GUID宏不需要指定它。
创建你自己的对象,给IExample目录作个拷贝。删除Debug和Release子目录,还有下面的文件也得删除:
IExample.dsp IExample.dsw IExample.ncb IExample.opt IExample.plg
在剩下的文件(IExample.c、IExample.h、IExample.def)中搜索IExample字符串并用你自己的对象名替换它(例如,IMyObject.c等)。在这个目录下以你的新对象名创建一个新的VisulaC++工程,工程的类型要选“Win32 Dynamic-Link Library”。创建一个空的工程,然后把上面的三个文件加到工程中。你一定要用GUIDGEN.EXE给你的对象和它的虚表生成你自己的GUID。不要用我生成的GUID。替换.H文件中的GUID宏(同时记住也要替换GUID宏的<<name>>部分)。删除.C和.H文件中的SetString和GetString函数,添加你自己的函数。修改.H文件你添加的函数定义的INTERFACE宏。修改MyRealIExample的数据成员为你需要的。修改安装程序源文件中的前三个字符串。在这个例子程序中,搜索并替换IExample为你的对象名。
虽然一个C或者C++程序,或者一个用大部分编译语言编写的程序,可以使用我们的COM对象,我们必须添加一些东东来支持大多数解释性语言来使用我们的对象,例如VisualBasic、VBScript、Jscript、Python等。这会是这个系列第二部分的主题。