原文:目录
o 一个COM对象和它的VTable
o GUID
o QueryInterface(),AddRef(),和Release()
o 一个IClassFactory对象
o 打包成dll
o 我们的C++/C包含文件
o 定义文件(DEF)
o 安装DLL,注册对象
o 一个C语言的例子
o 一个C++的例子
o 修改代码
o 下一步?
介绍
有大量例子说明如何使用和创建COM/OLE/ActiveX控件。但是这些例子典型的使用MFC,.NET,C#,WTL,至少是ATL,因为这些框架预制了一些样板文件。不信的是,这些框架也隐藏了许多底层细节,所以你从未真正的学习COM组件,而只是学习了如何使用一个顶层的框架。
如果试图使用C,而抛弃MFC,WTL,.NET,ATL,C#甚至是任何C++,那么你会发现很少有这方面的例子和资料。这个就是本篇文章所要说明的第一个系列。
在使用标准Win32控件如Static,Edit,Listbox,Combobox等时,你得到一个控件句柄(HWND)然后向它传递消息(通过SendMessage)为了操纵它。同样,控件也给你传递消息(比如把消息放到你的消息对列,你通过GetMessage去获得)当它想通知你一些事件或者给你一些数据。
这与OLE/COM对象不同。你不必传递消息。取而代之的是,COM对象给你一些函数指针你可以调用它们去操纵对象。例如,一个IE浏览器对象会给你一个函数指针使得你可以调用它来加载网页。一个Office对象会你一个函数指针你可以调用它来加载文档。如果一个COM对象需要通知你一些事件或向你传递数据,那么你可能需要写一些特定的函数,并向COM对象提供函数指针,使得它们可以调用这些函数当它们需要的时候。换句话来说,你需要创建你自己的COM对象。大多数在C方面的争议在于定义你自己的COM对象。为了完成这个,你需要知道关于COM对象的细节--就是大多数框架隐藏掉的,但是这里我们会阐述。
总结一下,你调用COM对象的函数来操纵它,它调用你的函数来通知你某些事件或者给你传递数据或与你的程序交互。这个机制调用DLL中的函数类似,DLL也可以调用你的函数--callback函数。但是与DLL不同的是,你不能使用LoadLibrary()和GetProcAddress()来获得COM中的函数,我们马上会发现,你会使用不同的操作系统函数得到一个对象的指针,然后使用那个对象来得到函数的指针。
一个COM对象和它的VTable
在我们学习如何使用COM对象,我们首先需要明白什么是COM对象,最好的方法是我们创建一个自己的COM对象。
在我们作之前,让我们看一下C struct结构,作为一个C程序员,你应该很熟悉struct,这里有个例子定义了一个简单的结构(IExample)包含两个成员
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;
现在我们为结构添加函数指针
long SetString(char *str)
{
return (0);
}
long GetString(char *buffer, long length)
{
return (0);
}
typedef long SetStringPtr(char *);
typedef long GetStringPtr(char *, long);
typedef struct {
SetStringPtr *SetString;
GetStringPtr *GetString;
DWORD count;
char buffer[80];
} IExample;
初始化
example->SetString = SetString;
example->GetString = GetString;
现在让我们把上面的指针放在一个数组里面,我们添加一个成员lpVtbl。
typedef struct {
IExampleVtbl *lpVtbl;
DWORD count;
char buffer[80];
} IExample;
这里有个例子初始化IExample,包括IExampleVtbl:
// Since the contents of IExample_Vtbl will never change, we'll
// just declare it static and initialize it that way. It can
// be reused for lots of instances of IExample.
static const IExampleVtbl IExample_Vtbl = {SetString, GetString};
IExample * example;
// Create (allocate) a IExample struct.
example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
// Initialize the IExample (ie, store a pointer to
// IExample_Vtbl in it).
example->lpVtbl = &IExample_Vtbl;
example->count = 1;
example->buffer[0] = 0;
调用我们的函数:
char buffer[80];
example=>lpVtbl->SetString("Some text");
example->lpVtbl->GetString(buffer, sizeof (buffer));
接下来让我们重新完善SetString和GetString
typedef long SetStringPtr(IExample *, char *);
typedef long GetStringPtr(IExample *, char *, long);
long SetString(IExample *this, char * str)
{
DWORD i;
// Let's copy the passed str to IExample's 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;
// Let's copy IExample's buffer to the passed buffer
i = lstrlen(this->buffer);
--length;
if (i > length) i = length;
CopyMemory(buffer, this->buffer, i);
buffer[i] = 0;
return(0);
}
我们会这样子调用:
example->lpVtbl->SetString(example, "Some text");
example->lpVtbl->GetString(example, buffer, sizeof (buffer));
如果你曾经使用过C++,你会发现这个好像很熟悉,是的上面所做的,是我们在使用C创建一个C++的类。IExample结构是一个C++类(不继承任何其他类)。一个C++类就是一个结构第一个元素是一个数组的指针--这个数组指向了所有类内部的函数。每个函数的第一个参数通常是类指针本身。
简单来说,一个COM对象只是一个C++类。到了这里你可能会认为"哇!IExample现在是一个COM对象了?这个也太简单了"。等一下,IExample接近了,但是不止这些。并不是这么容易。
首先,让我们介绍一些COM技术的理论。你看到上面的数组指针了--指向IExampleVtbl结构?COM文档中称它为接口或者VTable。
COM对象的其中一个要求是VTable的前三个成员必须是QueryInterface,AddRef,和Release。我们必须要实现这些函数。微软已经规定要传递给它们的参数,它们的返回值以及调用约定。我们使用#include来包含一些头文件。我们这么定义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 {
QueryInterfactPtr *QueryInterface;
AddRefPtr *AddRef;
ReleasePtr *Release;
SetStringPtr *SetString;
GetStringPtr *GetString;
} IExampleVtbl;
让我们先看看QueryInterface。首先函数必须返回HRESULT类型,其实这就是一个long。然后它规定了STDMETHODCALLTYPE调用规约。这意味着参数通过Stack传递而非寄存器,同样也规定了谁将清理堆栈当函数返回时。实际上,对一个COM对象来说,我们应该保证每个函数被声明为STDMETHODCALLTYPE,并返回long(HRESULT)类型。第一个参数是对象的指针。我们不是要讲IExample转换成COM对象么?是的,所以我们传递此参数(记住传递给函数的第一个参数必须是我们用来调用函数的结构体指针,COM就是基于这种设计的)
后来的内容,我们会看一下什么是REFID,并看看QueryInterface的第三个参数。目前请注意AddRef和Release同样也传递了结构体指针。
好了,现在我们对SetString和GetString添加STDMETHODCALLTYPE类型:
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++类就是一个结构体总是以VTable *作为其第一个参数。VTable的前三个成员总是QueryInterface,AddRef和Release。VTable其他的成员就看自己怎么设计的了。传递给这些函数的第一个参数是这个对象的指针。
o. GUID
让我们继续我们实现IExample为COM对象的旅程。我们还没有真正实现QueryInterface,AddRef和Release。在我们实现之前,我们必须先来说说GUID(Globally Universal Identifier)。它是一个16字节的数组,每个都表示唯一的序列。一个GUID不能与世界上任何其他某个GUID相同。每个GUID被唯一创建。
我们如何创建者16字节的序列呢?可以使用微软的GUIDGEN.exe,它与你的编译器捆绑着,或者你可以通过SDK而得到它。
你一旦运行了GUIDGEN,就会自动产生一个新的GUID。
// {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(<<name>>, 0xb5b3d8e, 0x574c, 0x4fa3,
0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2);
DEFINE_GUID是一个宏,编译器会编译为16字节的数组
但是有一件事情我们必须做,就是把<<name>>替换为我们想使用的C变量名,我们称之为CLSID_IExample。
// {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(CLSID_IExample, 0xb5b3d8e, 0x574c, 0x4fa3,
0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2);
现在我们已经定义了IExample使用的GUID,接着我们需要定义IExample的VTable("interface"),IExampleVtbl的GUID
// {74666CAC-C2B1-4fa8-A049-97F3214802F0}
DEFINE_GUID(IID_IExample, 0x74666cac, 0xc2b1, 0x4fa8,
0xa0, 0x49, 0x97, 0xf3, 0x21, 0x48, 0x2, 0xf0);
总结一下,每个COM对象都有一个GUID,它是一个16字节的数组不同于其他任何GUID。GUID可以通过GUIDGEN.exe生成。一个COM对象的VTable(interface)也有一个GUID。
o. QueryInterface(),AddRef(),和Release()
如果我们希望允许其他程序得到我们创建的IExample结构,为了的是程序可以调用我们的函数。
除了我们自己的COM对象之外,有许多其他的COM组件已经在一个PC上被安装(我们之后会介绍如何安装)。不同的计算机有不同COM组件被安装。程序如何从其他的COM对象中区别出我们的IExample COM对象,从而确定它是否被安装?
还记得每个COM对象都有唯一的GUID么,我们的IExample也一样,IExample的VTable也一样。我们需要作的就是告诉程序开发者IExample和它的VTable的GUID。典型的方式是,通过一个包含了两个GUID的宏的.H文件,其他程序知道了GUID后,作什么呢?
现在该QueryInterface函数登场了。其他程序把IExample的VTable的GUID传递给我们的QueryInterface函数,我们将会检查来确保它是IExample VTable的GUID,如果是的话,我们会返回一些东西让程序知道它真的拥有IExample对象。如果错误的GUID被传递,我们会返回错误并且让它知道它没有IExample对象。所以,所有其他COM对象会返回错误如果他们的QueryInterface被传递了IExample VTable的GUID,只有我们的QueryInterface才会返回正确的结果。
QueryInterface的第二个参数是我们需要检查的GUID,第三个参数是句柄,如果GUID和IExample VTable的GUID相匹配,则会被返回对象指针,否则返回NULL。QueryInterface返回long型的NOERROR(被定义为0)如果GUID匹配,否则返回非0的错误(E_NOINTERFACE)。让我们看一下IExample的QueryInterface:
HRESULT STDMETHODCALLTYPE QueryInterface(IExample *this,
REFIID vTableGuid, void **ppv)
{
// Check if the GUID matches IExample
// VTable's GUID. Remember that we gave the
// C variable name IID_IExample to our
// VTable GUID. We can use an OLE function called
// IsEqualIID to do the comparison for us.
if (!IsEqualIID(riid, &IID_IExample))
{
// We don't recognize the GUID passed
// to us. Let the caller know this,
// by clearing his handle,
// and returning E_NOINTERFACE.
*ppv = 0;
return(E_NOINTERFACE);
}
// It's a match!
// First, we fill in his handle with
// the same object pointer he passed us. That's
// our IExample we created/initialized,
// and he obtained from us.
*ppv = this;
// Now we call our own AddRef function,
// passing the IExample.
this->lpVtbl->AddRef(this);
// Let him know he indeed has a IExample.
return(NOERROR);
}
接下来让我们讨论AddRef和Release函数,你已经注意到了我们在QueryInterface函数内调用了AddRef函数。
我们为别的程序分配了IExample的空间,它只是得到它的地址。所以当其他程序使用完他们的时候,我们应该负责释放空间。我们如何知道什么时候该释放呢?
我们采用引用计数的方法。如果你回过头去看看IExample的定义,你会发现它有一个DWORD类型的名为count的成员变量。我们将要利用此成员。当我们创建一个IExample的时候,我们将其初始化为0,然后每当AddRef被调用的时候,我们就增加1,每当Release被调用的时候,就减少1。
所以,当我们传递IExample到QueryInterface,我们会调用AddRef来增加count的值,当其他程序使用完毕的时候,它们会调用Release来减少count的值,如果为0的话,我们会释放IExample的空间。
关于COM的另一条重要规则:如果你得到了其他人创建的COM对象,你必须调用Release函数当你使用完它的时候。
下面是我们的AddRef和Release函数:
ULONG STDMETHODCALLTYPE AddRef(IExample *this)
{
// Increment the reference count (count member).
++this->count;
// We're supposed to return the updated count.
return(this->count);
}
ULONG STDMETHODCALLTYPE Release(IExample *this)
{
// Decrement the reference count.
--this->count;
// If it's now zero, we can free IExample.
if (this->count == 0)
{
GlobalFree(this);
return(0);
}
// We're supposed to return the updated count.
return(this->count);
}
还有一件我们需要作的事情,微软定义了一个IUnknown的COM对象,一个IUnknown像IExample一样,除了它的VTable只包含QueryInterface,AddRef和Release函数。微软为其创建了一个特殊的GUID,但是你知道是什么么?我们的IExample也可以伪装成一个IUnknown对象,毕竟它也有QueryInterface,AddRef和Release函数。如果只是关心这三个函数,那么根本没有必要知道它是不是一个真正的IExample对象。我们要修改我们的代码为的是当传递给我们一个IExample或IUnknown的GUID都返回成功。微软提供了IID_IUnknown来表示IUnknown的GUID。
// Check if the GUID matches IExample's GUID or IUnknown's GUID.
if (!IsEqualIID(vTableGuid, &IID_IExample) &&
!IsEqualIID(vTableGuid, &IID_IUnknown))
总结一下,我们自己的COM对象来说,我们为其他程序分配空间,我们就要负责释放它。我们在AddRef和Release中使用引用计数来达到这个目的。我们的QueryInterface允许其他程序验证他们想要的对象,也让我们增加了引用计数。
现在IExample是一个真正的COM对象了么?是的,太棒了,不算难,我们做到了!
错,我们仍然需要把这些捆绑而使得其他程序可以使用(例如,一个动态链接库),写一个安装程序并解释其他程序如何得到我们创建的IExample。
o. 一个IClassFactory对象
现在我们需要看一下如何得到IExample对象,最后我们要写一些代码来实现它。微软已经设计一个标准的方法。这需要在我们的DLL文件里包含第二个COM对象--IClassFactory,它有一些特殊的函数,且定义了自己的GUID,通过IID_IClassFactory引用。
IClassFactory的VTable有5个特殊函数,QueryInterface,AddRef,Release,CreateInstance和LockServer。注意到了IClassFactory有它自己的QueryInterface,AddRef和Release函数,就像我们的IExample对象一样。(这里为了避免名字冲突,我们将IClassFactory的函数加上class前缀,classQueryInterface)。
真正重要的函数是CreateInstance。当程序无论何时需要我们创建一个IExample对象,并初始化返回它的时候,就必须调用我们的IClassFactory的CreateInstance函数。实际上,如果需要IExample多个对象,则可以调用CreateInstance多次。这就是程序如何得到我们的IExample对象,但是你可能会问“程序如何得到IClassFactory对象呢?”,我们稍候会解释,现在我们简单的实现IClassFactory的五个函数,生成其虚函数表。
生成需函数表很容易,不同于IExample的虚函数表,我们没有必要定义我们自己的IClassFactory的虚函数表。微软已经为我们定义了IClassFactoryVtbl结构。我们需要作的就是声明我们的VTable并且填充我们自己实现的5个函数。让我们创建一个静态的VTable,并命名为IClassFactory_Vtbl:
static const IClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface,
classAddRef,
classRelease,
classCreateInstance,
classLockServer);
同样的,创建一个IClassFactory对象也很容易,因为微软已经定义了此结构。我们仅仅需要一个对象,让我们声明一个静态的IClassFactory对象并命名为MyClassFactoryObj,并用上面的VTable来初始化:
static IClassFactory MyIClassFactoryObj = {&IClassFactory_Vtbl};
现在我们需要实现这5个函数。我们的classAddRef和ClassRelease是没有意义的,因为我们没有真正分配IClassFactory的空间(我们只是简单的声明为静态对象),也不需要释放空间。所以classAddRef总是返回1,表明总有一个IClassFactory对象。classRelease也一样,我们不需要任何引用计数。
ULONG STDMETHODCALLTYPE classAddRef(IClassFactory *this)
{
return (1);
}
ULONG STDMETHODCALLTYPE classRlease(IClassFactory *this)
{
return (1);
}
现在来看看QueryInterface函数,它需要检查传入的GUID是否是一个IUnknown的GUID或者一个IClassFactory的GUID。我们作同样的事情如同在IExample的QueryInterface函数中一样。
HRESULT STDMETHODCALLTYPE classQueryInterface(IClassFactory *this,
REFIID factoryGuid, void **ppv)
{
// Check if the GUID matches an IClassFactory or IUnknown GUID.
if (!IsEqualIID(factoryGuid, &IID_IUnknown) &&
!IsEqualIID(factoryGuid, &IID_IClassFactory))
{
// It doesn't. Clear his handle, and return E_NOINTERFACE.
*ppv = 0;
return(E_NOINTERFACE);
}
// It's a match!
// First, we fill in his handle with the same object pointer he passed us.
// That's our IClassFactory (MyIClassFactoryObj) he obtained from us.
*ppv = this;
// Call our IClassFactory's AddRef, passing the IClassFactory.
this->lpVtbl->AddRef(this);
// Let him know he indeed has an IClassFactory.
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的VTable的GUID(如果希望我们分配,初始化,返回一个IExample对象)
第四个参数是我们返回我们创建的IExample对象句柄。
让我们来看一下CreateInstance函数(被命名为classCreateInstance):
HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this,
IUnknown *punkOuter, REFIID vTableGuid, void **ppv)
{
HRESULT hr;
struct IExample *thisobj;
// Assume an error by clearing caller's handle.
*ppv = 0;
// We don't support aggregation in IExample.
if (punkOuter)
hr = CLASS_E_NOAGGREGATION;
else
{
// Create our IExample object, and initialize it.
if (!(thisobj = GlobalAlloc(GMEM_FIXED,
sizeof(struct IExample))))
hr = E_OUTOFMEMORY;
else
{
// Store IExample's VTable. We declared it
// as a static variable IExample_Vtbl.
thisobj->lpVtbl = &IExample_Vtbl;
// Increment reference count so we
// can call Release() below and it will
// deallocate only if there
// is an error with QueryInterface().
thisobj->count = 1;
// Fill in the caller's handle
// with a pointer to the IExample we just
// allocated above. We'll let IExample's
// QueryInterface do that, because
// it also checks the GUID the caller
// passed, and also increments the
// reference count (to 2) if all goes well.
hr = IExample_Vtbl.QueryInterface(thisobj, vTableGuid, ppv);
// Decrement reference count.
// NOTE: If there was an error in QueryInterface()
// then Release() will be decrementing
// the count back to 0 and will free the
// IExample for us. One error that may
// occur is that the caller is asking for
// some sort of object that we don't
// support (ie, it's a GUID we don't recognize).
IExample_Vtbl.Release(thisobj);
}
}
return(hr);
}
o. 打包成dll
为了让其他程序可以得到我们的IClassFactory(并且调用CreateInstance函数得到IExample对象),我们会将上面的代码打包成一个DLL文件。这篇文档不想说如何创建一个DLL文件,如果你不熟悉的话,那么你应该首先阅读一下相关方面的资料。
我们已经完成了IExample和IClassFactory的所有代码,我们现在需要做的就是变成DLL代码。
还有许多要做,微软已经规定了我们必须在DLL中添加DllGetClassObject函数,以及它的参数,该怎么做,及返回值。一个程序通过调用DllGetClassObject来得到我们IClassFactory对象的指针(实际上,后面我们会看到,程序会调用一个名为CoGetClassObject的OLE函数,那个函数会调用我们的DllGetClassObject.)所以,程序如何得到IClassFactory对象--通过调用DllGetClassObject。我们的DllGetClassObject函数必须完成这个工作,它这样子被定义:
HRESULT PASCAL DllGetClassObject(REFCLSID objGuid,
REFIID factoryGuid, void **factoryHandle);
第一个参数是IExample的GUID(而不是VTable的GUID)。我们需要检查此来确保调用者明确的调用DLL的DllGetClassObject函数。注意到每个COM的DLL都有一个DllGetClassObject函数,所以我们需要GUID来从其他COM的DLL中区分出DllGetClassObject函数。
第二个参数是IClassFactory的GUID。
第三个参数是调用者希望我们返回一个IClassFactory对象指针(如果传递了正确的IExample的GUID)
HRESULT PASCAL DllGetClassObject(REFCLSID objGuid,
REFIID factoryGuid, void **factoryHandle)
{
HRESULT hr;
// Check that the caller is passing
// our IExample GUID. That's the COM
// object our DLL implements.
if (IsEqualCLSID(objGuid, &CLSID_IExample))
{
// Fill in the caller's handle
// with a pointer to our IClassFactory object.
// We'll let our IClassFactory's
// QueryInterface do that, because it also
// checks the IClassFactory GUID and does other book-keeping.
hr = classQueryInterface(&MyIClassFactoryObj,
factoryGuid, factoryHandle);
}
else
{
// We don't understand this GUID.
// It's obviously not for our DLL.
// Let the caller know this by
// clearing his handle and returning
// CLASS_E_CLASSNOTAVAILABLE.
*factoryHandle = 0;
hr = CLASS_E_CLASSNOTAVAILABLE;
}
return(hr);
}
大多数的工作已经完成了,还有一件事情。程序不会加载我们的DLL,而是操作系统代替程序加载当程序调用CoGetDllClassObject(CoGetClassObject加载我们的DLL,通过LoadLibrary函数加载,GetProcAddress函数得到DllGetClassObject函数的地址,然后调用它)。不幸的是,微软并没有设计出程序如何告知操作系统已经使用完了我们DLL并且可以卸载它(FreeLibrary)的方法。所以我们必须告诉操作系统让它知道什么时候可以安全的卸载DLL。我们必须提供一个名为DllCanUnloadNow函数,当它返回S_OK的时候就可以安全的卸载我们的DLL,否则返回S_FALSE。
我们如何知道什么时候是可以安全卸载的呢?
我们还需要引用计数器。每当我们分配一个对象的空间,我们就要增加引用计数,每次程序调用Release函数,我们释放对象,减少引用计数。只有当计数为0时,我们才告诉OS我们的DLL可以被安全卸载,因为我们可以确信没有其他程序在使用我们的任何对象。所以我们声明一个静态的OutstandingObjects来维护这个引用计数(当DLL被加载的时候,这个值为0)。
我们在哪里增加此引用计数呢?在我们的IClassFactory的CreateInstance函数中,当我们调用GlobalAlloc分配空间并所有事情都顺利地完成后。我们在那个函数中添加一行,在Release的后面:
static DWORD OutstandingObjects = 0;
HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this,
IUnknown *punkOuter, REFIID vTableGuid, void **ppv)
{
...
IExampleVtbl.Release(thisobj);
// Increment our count of outstanding objects if all
// went well.
if (!hr) InterlockedIncrement(&OutstandingObjects);
}
}
return(hr);
}
哪里最合适减少引用计数呢?在我们的IExample的Release函数中,在GlobalFree函数后面:
InterlockedDecrement(&OutstandingObjects);
微软规定应该提供一种方法允许程序在内存中锁住我们的DLL。为了那个目的,我们调用IClassFactory的LockServer函数,如果我们希望增加一个锁,则传递参数1,传递0则意味着减少DLL的锁。我们需要第二个静态的DWORD引用计数变量LockCount(DLL加载时初始化为0):
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)
{
// If someone has retrieved pointers to any of our objects, and
// not yet Release()'ed them, then we return S_FALSE to indicate
// not to unload this DLL. Also, if someone has us locked, return
// S_FALSE
return((OutstandingObjects | LockCount) ? S_FALSE : S_OK);
}
o 我们的C++/C包含文件
早先我们提过,为了让程序使用我们的IExample动态链接库文件,我们需要提供IExample和IExample的VTable的GUID。我们把GUID宏放在.H文件里,我们可以给别人使用,也可以给自己的DLL使用。在这个.H文件里,我们还需要包含IExampleVtbl和IExample的结构,这样程序员可以通过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;
另外,尽管typedef可以使函数定义变得简单,但是如果函数过多的话,会显得有些冗长还可能容易犯错。
最后的问题是上面的定义是C定义,它使得C++程序要使用我们的COM对象变得不容易。毕竟尽管我们用C实现的IExample,但是IExample是一个C++类。对C++程序来说定义一个C++类比C结构要容易得多。
代替上述的定义,微软提供了一个宏使得我们可以定义我们的VTable和对象对C和C++都支持,而且隐藏了其他数据成员。要使用此宏,首先必须要定义对象的符号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;
};
看上去可能有些奇怪。
当定义一个函数,STDMETHOD表示函数返回HRESULT。我们的QueryInterface,SetString和GetString都返回HRESULT。AddRef和Release不是,他们返回ULONG,这就是为什么我们用STDMETHOD_(有个下划线)。接着我们把函数名放在括号中,如果函数不返回HRESULT,我们需要在函数名前指出其返回类型,然后逗号。函数名之后是()内的是参数。THIS表示我们对象的指针(IExample),如果函数只有此一个参数,就只需要在()内加入THIS,如同AddRef和Release函数一样。其他函数有额外的参数,我们必须使用THIS_(有个下划线)。我们列出余下的参数,注意到了,在THIS_和其他参数之间没有逗号,但是其他参数之间有逗号。最后我们加上PURE和分号。
虽然宏很奇怪,但是它是定义COM对象的方法为了让C编译器和C++编译器都可以识别。
“但是IExample结构的定义去哪里了?”。这个宏很奇怪,它可以让C编译器自动产生IExample结构的定义,它仅仅包含一个lpVtbl成员。通过这种方法定义我们的VTable,我们自动的得到合适的IExample结构。
把GUID添加到此.H文件中,我们创建IExample.h文件。
但是你知道IExample还有2个数据成员。所以我们打算定义一个IExample变种在我们的DLL文件中,我们定义其为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函数可以通过将IExample转换为MyRealIExample类型而得到额外的数据成员。
o 定义文件(DEF)
我们需要DEF文件来导出DllCanUnloadNow和DllGetClassObject函数。微软编译器希望他们被定义为PRIVATE:
LIBRARY IExample
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
o 安装DLL,注册对象
我们已经完成了生成IExample.dll的所有事情,我们可以开始编译IExample.dll了。
这不是我们的最后步骤,在其他程序使用我们的IExample之前,我们需要作两件事情:
1. 安装DLL文件使得程序运行时可以被找到
2. 注册我们的DLL为COM对象
我们需要创建一个安装程序使得可以将IExample.dll拷贝至一个合适的地方。例如,我们会在Program Files下创建一个IExample目录,然后拷贝DLL到此目录下(当然了,我们的安装程序应该做版本检查,如果有我们DLL的早期版本已经被安装,我们不会覆盖)
接着我们要注册此DLL,这包括创建几个注册表健值。
首先我们在HKEY_LOCAL_MACHINE\Software\Classes\CLSID下创建一个键值,名称必须为IExample的GUID,且必须为text格式。
在GUID键值下,创建字符串InprocServer32,默认值为DLL的安装全路径。添加ThreadingModel字符串,如果我们没有必要约束程序必须以单线程的方式调用我们的DLL函数,则我们可以指定其值为“both”。在我们的IExample函数中没有使用全局数据结构,所以我们是线程安全的。
在运行安装程序之后,IExample.dll被注册为COM组件,其他程序可以使用它了。