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

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

3、COM运行库读取Server的DLL的全路径并将其加载入Client的进程空间;

4、COM运行库调用Server的DllGetClassObject()函数,向类工厂请求要获取CoClass;

5、Server建立所需要的类工厂,并从DllGetClassObject()返回;

6、COM运行库调用类工厂的CreateInstance()方法来创建Client要获取的COM对象。

7、CoCreateInstance()返回接口指针到Client。

 

COM Server的注册

 

在正常工作前,一个COM Server必须正确地在Windows注册表中注册,如果你打开注册表的HKEY_CLASSES_ROOT/CLSID,你就会发现巨量的子键,HKCR/CLSID保存着本机所有可用的COM Server的列表,当一个COM Server被注册(通常是使用DllRegisterServer()),它就建立一个CLSID下的键,键名就是Server的GUID,例如:

 

{067DF822-EAB6-11cf-B56E-00A0244D5087}

 

花括号和连字号都是必须的,字母可以大写也可以小写。

 

默认键值是一个人工可读的CoClass的名字,以便在一些OLE/COM对象查看工具中显示出来。

 

在GUID键的子键下面有更多的信息,你要创建的子键很大程度上取决于你有怎样的COM Server,还有怎样使用它,对于简单的进程内Server来说,我们只需要一个子键:InProcServer32。

 

这个InProcServer32的键包括了两个字符串:(默认)--这个Server DLL的全路径;ThreadingModel--线程模式,线程模式超出了本文范围,但对于单线程的Server来说,这个模型是Apartment

 

建立COM对象 - 类工厂

 

我们回过头来看COM的Client,我讲过关于COM如何具有其语言无关性来创建和销毁COM对象的,Client调用CoCreateInstance()来创建一个COM对象,现在我们来看它如何在Server端工作。

 

每次你实现一个CoClass,你也得写一个对应的用来创建前面这个CoClass实例的CoClass,这个对应的CoClass就叫类工厂,使用类工厂的理由就是语言无关性,COM本身并不创建COM对象,因为这样就不是语言无关了。

 

当Client要创建一个COM对象,COM运行库就从COM Server请求类工厂,类工厂建立起COM对象并返回给Client,这种通信机制通过导出函数DllGetClassObject()实现。

 

这里岔开讲一下,类工厂和类对象这两个术语通常指的都是同样的东西,但其实它们并不准确描述了它们的作用,因为工厂建立的是对象而不是类,所以将它称为对象工厂更为合理,(事实上MFC就这么干了,它的类工厂实现称作COleObjectFactory)但官方术语就叫作类工厂,那我们就依照它吧。

 

当COM运行库调用DllGetClassObject(),它传递CLSID进去,Server根据这个CLSID创建类工厂并返回,类工厂自己就是个CoClass并实现了IClassFactory接口,如果DllGetClassObject()成功了,它就返回IClassFactory指针给COM运行库,COM运行库使用IClassFactory的方法来创建COM对象实例,最后返回到Client。

 

IClassFactory接口如下:

 

struct IClassFactory : public IUnknown

{

    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid,

                            void** ppvObject );

    HRESULT LockServer( BOOL fLock );

};

 

CreateInstance()是创建新COM对象的方法,LockServer()让COM运行库增加或减少Server的引用计数。

 

例子

 

这有个类工厂的例子,我们来看这篇文章的范例工程,它是个在CSimpleMsgBoxImpl CoClass中实现了一个ISimpleMsgBox接口的DLL Server。

 

接口定义

 

我们新的接口叫作ISimpleMsgBox,和其它接口一样,它必须继承于IUnknown,这里只有一个方法DoSimpleMsgBox(),注意它返回的是标准类型HRESULT,所有你写的方法都应该返回这个标准类型HRESULT,其它你需要返回的数据都应该通过指针参数。

 

struct ISimpleMsgBox : public IUnknown

{

    // IUnknown methods

    ULONG AddRef();

    ULONG Release();

    HRESULT QueryInterface( REFIID riid, void** ppv );

 

    // ISimpleMsgBox methods

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

};

 

struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}"))

                  ISimpleMsgBox;

 

(__declspec这行将一个GUID分配给ISimpleMsgBox符号,之后这个GUID就能被__uuidof运算符获取,__declspec和__uuidof都是Microsoft C++扩展)

 

第二个参数DoSimpleMsgBox()是BSTR类型,BSTR代表Binary String--COM中一个定长直接串,BSTR主要是给Visual Basic和Windows Scripting Host使用的。

这个接口是通过一个叫CSimpleMsgBoxImpl的C++类来实现的,它定义是:

 

class CSimpleMsgBoxImpl : public ISimpleMsgBox 

{

public:

    CSimpleMsgBoxImpl();

    virtual ~CSimpleMsgBoxImpl();

 

    // IUnknown methods

    ULONG AddRef();

    ULONG Release();

    HRESULT QueryInterface( REFIID riid, void** ppv );

 

    // ISimpleMsgBox methods

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

 

protected:

    ULONG m_uRefCount;

};

 

class  __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}"))

                  CSimpleMsgBoxImpl;

 

当一个Client要建立一个SimpleMsgBox COM对象,它应该这样来做:

ISimpleMsgBox* pIMsgBox;

HRESULT hr;

hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl), // CLSID of the coclass

                      NULL,                         // no aggregation

                      CLSCTX_INPROC_SERVER,         // the server is in-proc

                      __uuidof(ISimpleMsgBox),      // IID of the interface

                                                    // we want

                      (void**) &pIMsgBox );         // address of our

                                                    // interface pointer

 

类工厂

我们的类工厂实现

我们的SimpleMsgBox类工厂是用C++类来实现的:

class CSimpleMsgBoxClassFactory : public IClassFactory

{

public:

    CSimpleMsgBoxClassFactory();

    virtual ~CSimpleMsgBoxClassFactory();

 

    // IUnknown methods

    ULONG AddRef();

    ULONG Release();

    HRESULT QueryInterface( REFIID riid, void** ppv );

 

    // IClassFactory methods

    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv );

    HRESULT LockServer( BOOL fLock );

 

protected:

    ULONG m_uRefCount;

};

 

构造函数,析构函数,IUnknown方法都像前面的例子那样实现,唯一新鲜的玩意儿就是IClassFactory方法,LockServer()大致和你想的那样:

HRESULT CSimpleMsgBoxClassFactory::LockServer ( BOOL fLock )

{

    fLock ? g_uDllLockCount++ : g_uDllLockCount--;

    return S_OK;

}

 

现在我们来看有趣的部分,调用CreateInstance()这个方法就能返回新建的CSimpleMsgBoxImpl对象,我们来看它的原形和参数:

HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,

                                                    REFIID    riid,

                                                    void**    ppv );

 

pUnkOuter是为聚合新对象准备的,指向一个外部COM对象,聚合超出了本文讨论范围,我们的例子不支持聚合。

riid和ppv跟使用QueryInterface()中的一样,它们是Client请求的接口的IID和要返回的接口指针。

这里有个CreateInstance()的实现,它从一些数据校验和初始化开始。

 

HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,

                                                    REFIID    riid,

                                                    void**    ppv )

{

    // 我们不支持聚合,因此pUnkOuter必须是NULL

    if ( NULL != pUnkOuter )

        return CLASS_E_NOAGGREGATION;

 

    // 检查ppv是否真的指向一个void*

    if ( IsBadWritePtr ( ppv, sizeof(void*) ))

        return E_POINTER;

 

    *ppv = NULL;

    // 我们检查了这些参数是合法的,所以我们能创建新对象

 

    CSimpleMsgBoxImpl* pMsgbox;

    // 创建一个新的COM对象

    pMsgbox = new CSimpleMsgBoxImpl;

    if ( NULL == pMsgbox )

        return E_OUTOFMEMORY;

 

    // 最后,我们QI()新的对象的接口,如果QI()失败,那么对象就没有用途了,我们删除它

    HRESULT hrRet;

 

    // 用QI()取得客户端请求的接口

    hrRet = pMsgbox->QueryInterface ( riid, ppv );

 

    // 如果QI()失败,将COM对象删除,因为客户端不再需要它

    if ( FAILED(hrRet) )

        delete pMsgbox;

 

    return hrRet;

}

 

DllGetClassObject()

 

让我们仔细看看DllGetClassObject(),它的原形是:

 

HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );

 

rclsid是Client要求的CoClass的CLSID,这个函数必须返回这个CoClass的类工厂。

riid和ppv也和QI()函数的参数类似,在这里,riid是COM运行库要获取的接口的IID,通常是IID_IClassFactory。

因为DllGetClassObject()建立一个新的COM对象(类工厂),代码看起来非常像IClassFactory::CreateInstance(),开头是一些校验和初始化:

 

HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv )

{

    // Check that the client is asking for the CSimpleMsgBoxImpl factory.

    if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) ))

        return CLASS_E_CLASSNOTAVAILABLE;

 

    // Check that ppv really points to a void*.

    if ( IsBadWritePtr ( ppv, sizeof(void*) ))

        return E_POINTER;

 

    *ppv = NULL;

 

    //第一部分是检查参数,我们的Server只包含了一个CoClass,所以rclsid肯定是CSimpleMsgBoxImpl类的CLSID,

    //__uuidof运算符返回已经用__declspec(uuid())分配给CSimpleMsgBoxImpl的GUID,

    //InlineIsEqualGUID是个检查两个GUID是否相等的内联函数。

    //下一步就是建立一个类工厂对象了

 

    CSimpleMsgBoxClassFactory* pFactory;

 

    // Construct a new class factory object.

    pFactory = new CSimpleMsgBoxClassFactory;

 

    if ( NULL == pFactory )

        return E_OUTOFMEMORY;

 

    //这里跟CreateInstance()稍有点不同了,回到CreateInstance(),我们只调用了下QI(),如果它失败,

    //我们删除COM对象,这里则有点不太一样,我们把自己当作刚创建的COM对象的Client,

    //我们调用AddRef()来增加它的引用计数,然后我们调用QI(),如果QI成功,它又调了AddRef()一次,

    //使得引用计数变成了2,如果QI()失败,那么引用计数增加1。QI()调用后,类工厂对象的使用就算结束了,

    //所以我们调用Release(),如果QI()失败,对象将删除它自己(因为引用计数是0),所以结果还是一样的。

 

    // AddRef() the factory since we're using it.

    pFactory->AddRef();

 

    HRESULT hrRet;

 

    // QI() the factory for the interface the client wants.

    hrRet = pFactory->QueryInterface ( riid, ppv );

   

    // We're done with the factory, so Release() it.

    pFactory->Release();

 

    return hrRet;

}

 

QueryInterface()故地重游

 

我展示过一个简单的QI()实现,但研究类工厂的QI()更具价值,因为它是个真实的例子,在这个COM对象实现中除了有IUnknown还有更多的内容。首先我们创建ppv缓存,并初始化它。

 

HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv )

{

    HRESULT hrRet = S_OK;

 

    // Check that ppv really points to a void*.

    if ( IsBadWritePtr ( ppv, sizeof(void*) ))

        return E_POINTER;

 

    // Standard QI initialization - set *ppv to NULL.

    *ppv = NULL;

 

    //接下来我们来看riid,看他是否属于类工厂的实现:IUnknown或者IClassFactory

    if ( InlineIsEqualGUID ( riid, IID_IUnknown ))

    {

        *ppv = (IUnknown*) this;

    }

    else if ( InlineIsEqualGUID ( riid, IID_IClassFactory ))

    {

        *ppv = (IClassFactory*) this;

    }

    else

    {

        hrRet = E_NOINTERFACE;

    }

    //最后,如果riid是个支持的接口,我们通过接口指针调用AddRef()并返回

    // If we're returning an interface pointer, AddRef() it

    if ( S_OK == hrRet )

    {

        ((IUnknown*) *ppv)->AddRef();

    }

 

    return hrRet;

}

 

ISimpleMsgBox的实现

 

最后,但不是最终我们得为ISimpleMsgBox的唯一的方法来写些代码,DoSimpleMsgBox(),我在这里用Microsoft的扩展类_bstr_t来把bsMessageText转换成TCHAR串。

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent,

                                            BSTR bsMessageText )

{

    _bstr_t bsMsg = bsMessageText;

    LPCTSTR szMsg = (TCHAR*) bsMsg;         // Use _bstr_t to convert the

                                            // string to ANSI if necessary.

    //转换后,我们显示消息框,并返回

    MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK );

    return S_OK;

}

 

Client

 

现在我们算完成了这个出色的COM Server,我们如何来使用它?我们的接口是个定制的接口,这意味着它只能被C或者C++Client所使用,(如果我们的CoClass实现了IDispatch,我们就能用各种工具写它的Client了,Visual Basic、Windows Scripting Host、a web page和PerlScript等等,但这些不在本文讨论)我已经写好了一个简单的应用程序来使用ISimpleMsgBox。

  图1

此程序是个应用程序向导创建的Win32 Hello World程序,菜单中包含两个命令来测试这个Server:

Test MsgBox COM Server命令建立一个CSimpleMsgBoxImpl对象,并调用它的DoSimpleMsgBox()方法,因为这是个简单的方法,代码并不很长,我们一开始用CoCreateInstance()创建了个COM对象。

 

void DoMsgBoxTest(HWND hMainWnd)

{

    ISimpleMsgBox* pIMsgBox;

    HRESULT hr;

 

    hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), // CLSID of coclass

                            NULL,                        // no aggregation

                            CLSCTX_INPROC_SERVER,        // use only in-proc

                                                         // servers

                            __uuidof(ISimpleMsgBox),     // IID of the interface

                                                         // we want

                            (void**) &pIMsgBox );        // buffer to hold the

                                                         // interface pointer

 

    if ( FAILED(hr) )

        return;

    //接下来我们调用DoSimpleMsgBox()并释放我们的接口

    pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );

    pIMsgBox->Release();

}

 

这就是全部代码。

 

另一个菜单命令CoFreeUnusedLibraries则调用CoFreeUnusedLibraries() API,所以你能看到Server的DllCanUnloadNow()函数会使用到。

 

其它

 

COM宏

 

在COM代码中有好几个宏,它们隐藏了一些实现上的细节,这些宏能在C和C++中使用,我在本文中没有使用这些宏,但范例工程中有使用,所以你得明白它们的含义,这里是ISimpleMsgBox的声明:

struct ISimpleMsgBox : public IUnknown

{

    // IUnknown methods

    STDMETHOD_(ULONG, AddRef)() PURE;

    STDMETHOD_(ULONG, Release)() PURE;

    STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;

 

    // ISimpleMsgBox methods

    STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;

};

 

STDMETHOD() includes the virtual keyword, a return type of HRESULT, and the __stdcall calling convention. STDMETHOD_() is the same, except you can specify a different return type. PURE expands to "=0" in C++ to make the function a pure virtual function.

STDMETHOD()包含了virtual关键字,返回HRESULT类型,使用__stdcall调用协定,STDMETHOD_()也是一样的,除非你指定了一个不同的返回值,PURE则扩展成=0来表示C++中的存虚函数。

 

 

STDMETHOD()和STDMETHOD_()在实现中也有对应的宏定义--STDMETHODIMP和STDMETHODIMP_(),例如,这里有个DoSimpleMsgBox()的实现:

 

STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent,

                                                 BSTR bsMessageText )

{

    //...

}

 

最后,标准导出函数用STDAPI宏来声明,如:

STDAPI DllRegisterServer();

 

STDAPI包括了返回类型和调用协定,你使用STDAPI不利的一面就是你不能再使用__declspec(dllexport),你不得不用.DEF文件来替代它。

 

Server的注册和反注册

 

Server实现了我前面提及的DllRegisterServer()和DllUnregisterServer(),它们的功能就是建立和删除注册表相关项目来告诉COM我们的Server的信息,其中代码尽是烦人的注册表操作,我就不在这里重复了,但这里有个用DllRegisterServer()创建的注册表项目列表:

 

HKEY_CLASSES_ROOT/CLSID/{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}

    Default="SimpleMsgBox class"

HKEY_CLASSES_ROOT/CLSID/{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}/InProcServer32

    Default=[path to DLL]; ThreadingModel="Apartment"

 

(译者注:由于本译文没有附带源代码,下面关于源代码的说明也略作改动)

 

代码范例的注意事项

 

如前面所提及,你需要一个.def文件来从Server导出你的标准导出函数,.def通常如下:

EXPORTS

    DllRegisterServer   PRIVATE

    DllUnregisterServer PRIVATE

    DllGetClassObject   PRIVATE

    DllCanUnloadNow     PRIVATE

每一行包含函数名和PRIVATE关键字,这个关键字意思是函数将导出,但不包含在导入库,这意味着Client不能直接从代码上调用这些函数,即使Clientlink了导入库,这是个必须的步骤,如果你漏了PRIVATE这个关键字,编译器就会报错。

 

在Server端设置断点

 

如果你需要在Server端(DLL)设置断点以便调试,那你有两种办法:

第一种办法是把Server端所属的工程设置为当前活动工程,然后开始调试,如果VC向你询问可执行文件的位置,你就输入测试Client的全路径,当然之前你先要生成Client;另一种办法是把Client所属工程设置为当前活动工程,并配置工程的依赖关系,使得Client要依赖于Server,这样的话,当你改动Server代码并编译时,Client就会自动重新编译,最后,当你开始调试Client的时候,你得告诉VC加载Server的符号:打开Client的工程设置,在Debug标签下Category中选择Additional DLLs,单击下面的列表框增加一个条目,指明你要调试的Server(DLL文件)的位置。

(第二部分完)

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值