现在的位置: 首页 > 综合 > 正文

COM入门第二部分 – 编写COM Server

2013年08月07日 ⁄ 综合 ⁄ 共 4658字 ⁄ 字号 评论关闭

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的注册信息;

抱歉!评论已关闭.