COM入门第二部分 - 编写COM Server
作者:Michael Dunn(codeproject.com)
译者:蒋国纲
本文目的
同我写的上一篇《COM入门第一部分》一样,本文也是为初学COM的程序员准备的,帮助他们来弄懂一些COM基础。本文覆盖了COM Server部分,解释如何逐步编写COM接口和COM Server,也提及到当被COM运行库调用的时候COM Server内部的详细过程。
本文假定你精通C++,并懂得《COM入门第一部分》所提及到的术语,章节安排如下:
COM Server快览 - 讲述COM Server的要素;
COM Server生存时间管理 - 讲述COM Server如何来控制它自己被加载多长时间;
接口实现,从IUnknown开始 - 向你展示如何用C++来实现一个接口,并解释IUnknown方法;
CoCreateInstance()内幕 - 关于调用CoCreateInstance()的概览;
COM Server注册 - 讲述完整地注册一个COM Server需要哪些要素;
建立COM对象 - 类工厂 - 讲述为你的Client程序建立COM对象的过程;
例子 - 举例说明上面所讲的内容;
Client - 简单的可以用来测试Server的Client;
其它 - 源码及调试中的注意事项。
COM Server快览
本文中,我们先来学习一种最简单的COM Server,进程内Server(in-process server),“进程内”意味着Server被加载到Client程序的进程空间,进程内Server总是DLL的形式,并且要和Client在同一电脑上。
进程内Server在它能正常使用前必须符合两种规范:
1、它必须已经被注册到注册表的“HKEY_CLASSES_ROOT/CLSID”键下;
2、它必须导出了一个叫“DllGetClassObject()”的函数。
这是你想要一个进程内Server正常工作的最低要求,在注册表的“HKEY_CLASSES_ROOT/CLSID”下面必须有个这个Server的GUID键值,这个键还需要包括这个Server(DLL文件的形式)的具体位置和它的线程模式;DllGetClassObject()函数是被COM运行库的CoCreateInstance()这个API来调用的。
通常也会同时导出以下三个函数:
DllCanUnloadNow(): 让COM运行库检查Server是否已经从内存中移除;
DllRegisterServer(): 被类似RegSvr32这种工具调用,来让Server注册自己;
DllUnregisterServer(): 反注册自己,DllRegisterServer()的逆操作。
当然,光导出正确的函数还是不够的,我们得遵循COM的规格来编写COM,这样COM运行库和Client才能正常使用它们。
Server生存时间管理
DLL Server有点不寻常的特征是它们控制他们驻留多长时间,而普通的DLL则是被动地被它的调用者加载到内存和从内存中卸载,其实,纯技术角度来说,DLL Server也是被动的,本质上它们也是DLL,但COM运行库给它们提供了“自动”的机制,来自动加载和卸载它们,这点通过导出函数DllCanUnloadNow()来实现,这个函数原形如下:
HRESULT DllCanUnloadNow();
当Client调用COM API CoFreeUnusedLibraries()的时候(通常在系统空闲的时候调用),COM运行库通过调用COM Server的DllCanUnloadNow()来检查所有这个COM Server的引用,如果这个Server需要保持加载,函数就返回S_FALSE,否则说明这个Server不需要再加载,它返回S_OK,COM运行库就从内存中卸载它。
一个Server判定是否可以被卸载,用种简单的引用计数就可以了,DllCanUnloadNow()的实现通常如下:
extern UINT g_uDllRefCount; // server's reference count
HRESULT DllCanUnloadNow()
{
return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
下章节我将来讲述如何维护这个引用计数,同时我们也会有些范例。
接口实现,从IUnknown开始
所有接口继承于IUnknown,因为IUnknown包括两个COM对象引用计数的基本功能,当你写一个CoClass,你就得实现IUnknown,我们来看例子,可能是你能写得出的最简单的CoClass例子,我们在一个叫CUnknownImpl的类中实现了IUnknown,类声明如下:
class CUnknownImpl : public IUnknown
{
public:
// Construction and destruction
CUnknownImpl();
virtual ~CUnknownImpl();
// IUnknown methods
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
UINT m_uRefCount; // object's reference count
};
构造与析构函数
构造与析构函数管理着Server的引用计数:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
当COM对象创建的时候,构造函数被调用,Server的引用计数增加,以此保证Server驻留内存,它同时也将COM对象的引用计数置为1,反之当COM对象销毁的时候,析构函数减少Server的引用计数。
AddRef()与Release()
这两个方法控制COM对象的生存时间,AddRef()很简单:
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
它简单地增加了对象的引用计数,并返回更新了的引用计数值。
Release()就相对有点琐碎:
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
if ( 0 == m_uRefCount ) // releasing last reference
delete this;
return uRet;
}
它除了减少对象的引用计数,还在引用计数为0的时候销毁这个对象,Release()也返回更新了的引用计数,注意Release()的实现假定了COM对象被建立在堆中(译者:局部变量),如果你是在栈中(译者:用new创建)或全局中创建它,那在delete这个对象的时候就会出错。
现在就很清楚了,为什么在你的程序中恰当地调用AddRef()和Release()是很重要的。如果你不正确地调用它们,COM对象就有可能提前被销毁,或者根本不被销毁。如果COM对象被过早销毁,而你的应用程序却企图访问它,就会出现内存非法访问导致程序崩溃。
如果你做过多线程编程,你可能对使用“++”和“--”运算符的“线程安全性”感到担忧,你用InterlockedIncrement()和InterlockedDecrement()来取代它,其实,“++”和“--”运算符在单线程Server中非常安全,因为,尽管我们的Client程序是多线程的,调用COM的方法也是不同的线程来调用,但COM运行库最终会把这些调用串行化,也就是说我们一旦调用了一个方法,在这个调用完成之前,其它线程企图对这个方法的调用就会被堵塞,直到第一个方法调用返回,COM运行库确保了我们的Server不会同时被一个以上的线程进入。
QueryInterface()
QueryInterface(),或者简称为QI(),是给Client用来从COM对象获取不同接口的,因为我们的CoClass例子只实现了一个接口,我们的QI()也将会很简单,QI()带两个参数:接口的IID和要获取的接口的指针。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// Standard QI() initialization - set *ppv to NULL.
*ppv = NULL;
// If the client is requesting an interface we support, set *ppv.
if ( IsEqualIID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else
{
// We don't support the interface the client is asking for.
hrRet = E_NOINTERFACE;
}
// If we're returning an interface pointer, AddRef() it.
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
QI()中做了三件不同的事情:
1、初始化参数传进来的指针为NULL;
2、检查参数riid,看是否我们的CoClass实现了Client所需要的接口;
3、如果我们真的实现了这个所要的接口,就增加COM Object的引用计数。
这里特别注意下AddRef(),很重要:
*ppv = (IUnknown*) this;
这行代码功能是对COM对象建立起了新的引用,所以我们必须调用AddRef()来告诉这个对象有了新的引用,将AddRef()的调用返回转换为IUnknown*看起来有些怪异,其实并不是所有的*ppv都是IUnknown*,所以这个转换还是有必要的,是个好习惯。
现在,我们来讨论DLL Server的一些内部细节,让我们再回头看看我们的Server是怎样处理一个Client的CoCreateInstance()调用的。
CoCreateInstance()内幕
回到我写得前一篇文章《COM介绍第一部分》,我们可以看到CoCreateInstance()这个API,当Client需要的时候,它来建立一个COM对象,对Client而言,它是个黑盒,只要用正确的参数调一下CoCreateInstance(),你就得到个COM对象,当然这里面其实并没有什么魔术,它执行了一个预先定义好的过程,将COM对象建立起来,并返回这个对象的接口。
这里有个这个过程的概览,这里可能有些不熟悉的术语,但不用担心,我会在下面的章节中讲述它们。
1、Client程序调用CoCreateInstance(),传递要取得的CoClass的CLSID和接口的IID;
2、COM运行库在注册表“HKEY_CLASSES_ROOT/CLSID”键下查找要取得的Server的CLSID,这个键保存着这个Server的注册信息;