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组件,其他程序可以使用它了。
阅读(1594) | 评论(1) | 转发(0) |