荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: DuMiYue (TH@Figo), 信区: Visual
标  题: 第二部分 深入COM服务器
发信站: 荔园晨风BBS站 (Tue Mar 19 14:59:33 2002), 转信

COM编程入门
第二部分 深入COM服务器
文/赵湘宁
下在例子代码:src demo
本文为刚刚接触COM的程序员提供编程指南,解释COM服务器内幕以及如何用C++编写自己
的接口。
继上一篇COM编程入门之后,本文将讨论有关COM服务器的内容,解释编写自己的COM接口
和COM服务器所需要的步骤和知识,以及详细讨论当COM库对COM服务器进行调用时,COM
服务器运行的内部机制。
如果你读过上一篇文章。应该很熟悉COM客户端是怎么会事了。本文将讨论COM的另一端
——COM服务器。内容包括如何用C++编写一个简单的不涉及类库的COM服务器。深入到创
建COM服务器的内部过程,毫无遮掩地研究那些库代码是充分理解COM服务器内部机制的
最好方法。
本文假设你精通C++并掌握了上一篇文章所讨论的概念和术语。在这一部分将包括如下内
容:
走马观花看COM服务器——描述COM服务器的基本要求。
服务器生命其管理——描述COM服务器如何控制加载时间。
实现接口,从IUnknown开始——展示如何用C++类编写一个接口实现并描述IUnknown之方
法的目的。
深入CoCreateInstance()——探究CoCreateInstance()的调用机理。
COM服务器的注册——描述完成服务器注册所需要的注册表入口。
创建COM对象——类工厂——描述创建客户端要使用的COM对象的过程。
一个定制接口的例子——例子代码示范了上述概念。
一个使用服务器的客户端——举例说明一个简单的客户端应用程序,用它来测试COM服务
器。
其它内容——有关源代码和调试的注释。
走马观花看COM服务器
本文我们将讨论最简单的一种COM服务器,进程内服务器(in-process)。“进程内”意
思是服务器被加载到客户端程序的进程空间。进程内服务器都是DLLs,并且与客户端程
序同在一台计算机上。
进程内服务器在被COM库使用之前必须满足两个条件或标准:
1、 必须正确在注册表的HKEY_CLASSES_ROOT\CLSID 键值下注册。
2、 必须输出DllGetClassObject()函数。
这是进程内服务器运行的最小需求。在注册表的HKEY_CLASSES_ROOT\CLSID 键值下必须
创建一个键值,用服务器的GUID作为键名字,这个键值必须包含两个键值清单,一是服
务器的位置,而是服务器的线程模型。 COM库对DllGetClassObject()函数进行调用是在
CoCreateInstance() API中完成的。
还有三个函数通常也要输出:
o DllCanUnloadNow():由COM库调用来检查是否服务器被从内存中卸载。
o DllRegisterServer():由类似RegSvr32的安装实用程序调用来注册服务器。
o DllUnregisterServer():由卸载实用程序调用来删除由DllRegisterServer()创建的注
册表入口。
另外,只输出正确的函数是不够的——还必须遵循COM规范,这样COM库和客户端程序才
能使用服务器。
服务器生命其管理
DLL服务器的一个与众不同的方面是控制它们被加载的时间。“标准的”DLLs被动的并且
是在应用程序使用它们时被随机加载/或卸载。从技术上讲,DLL服务器也是被动的,因
为不管怎样它们毕尽还是DLL,但COM库提供了一种机制,它允许某个服务器命令COM卸载
它。这是通过输出函数DllCanUnloadNow()实现的。这个函数的原型如下:
         HRESULT DllCanUnloadNow();
当客户应用程序调用COM API CoFreeUnusedLibraries()时,通常出于其空闲处理期间,
COM库遍历这个客户端应用已加载所有的DLL服务器并通过调用它的DllCanUnloadNow()函
数查询每一个服务器。另一方面,如果某个服务器确定它不再需要驻留内存,它可以返
回S_OK让COM将它卸载。
服务器通过简单的引用计数来确定它是否能被卸载。下面是DllCanUnloadNow()的实现:

extern UINT g_uDllRefCount;  // 服务器的引用计数
HRESULT DllCanUnloadNow()
{
    return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
如何处理引用计数将在下一节涉及到具体代码时讨论。
实现接口,从IUnknown开始
有必要回想一下IUnknown派生的每一个接口。因为IUnknown包含了两个COM对象的基本特
性——引用计数和接口查询。当你编写组件对象类时(coclass),还要写一个满足自己
需要的IUnknown实现。以实现IUnknown接口的组件对象类为例——下面这个例子可能是
你编写的最简单的一个组件对象类。我们将在一个叫做CUnknownImpl的C++类中实现IUn
known。下面是这个类的声明:
class CUnknownImpl : public IUnknown
{
public:
    // 构造函数和析构器
    CUnknownImpl();
    virtual ~CUnknownImpl();
    // IUnknown 方法
    ULONG AddRef();
    ULONG Release)();
    HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
    UINT m_uRefCount;  // 对象的引用计数
};
构造器和析构器
构造器和析构器管理服务器的引用计数:
CUnknownImpl::CUnknownImpl()
{
    m_uRefCount = 0;
    g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
    g_uDllRefCount--;
}
当创建新的COM对象时,构造器被调用,它增加服务器的引用计数以保持这个服务器驻留
内存。同时它还将对象的引用计数初始化为零。当这个COM对象被摧毁时,它减少服务器
的引用计数。
AddRef()和Release()
这两个方法控制COM对象的生命期。AddRef()很简单:
ULONG CUnknownImpl::AddRef()
{
    return ++m_uRefCount;
}
AddRef()只增加对象的引用计数并返回更新的计数。
Release()更简单:
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
    if ( 0 == m_uRefCount )  // 是否释放了最后的引用?
        delete this;
    return uRet;
}
除了减少对象的引用计数外,如果没有另外的明确引用,Release()将摧毁对象。Relea
se()也返回更新的引用计数。注意Release()的实现假设COM对象在堆中创建。如果你在
全局粘上创建某个对象,当对象试图删除自己时就会出问题。
现在应该明白了为什么在客户端应用程序中正确调用AddRef()和 Release()是如此重要
!如果在这了做得不对,你使用的对象会被很快摧毁,这样的话在整个服务器中内存会
很快溢出导致应用程序下次存取服务器代码时崩溃。
如果你编写多线程应用,可能会想到使用++&替代InterlockedIncrement()和Interlock
edDecrement()的线程安全问题。++&——用于单线程服务器很保险,因为即使客户端应
用是多线程的并从不同的线程中进行方法调用,COM库都会按顺序进行服务器的方法调用
。也就是说,一旦一个方法调用开始,所有其它试图调用方法的线程都将阻塞,直到第
一个方法返回。COM库本身确保服务器一次不会被一个以上的线程闯入。
QueryInterface()
QueryInterface()简称QI(),由客户端程序调用这个函数从COM对象请求不同的接口。我
们在例子代码中因为只实现一个接口,QI()会很容易使用。QI()有两个参数:一个是所
请求的接口IID,一个是指针的缓冲大小,如果查询成功,QI()将接口指针地址存储在这
个缓冲指针中。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
    // 标准QI()初始化 – 置 *ppv 为 NULL.
    *ppv = NULL;
    // 如果客户端请求提供的接口,给 *ppv.赋值
    if ( IsEqualIID ( riid, IID_IUnknown ))
        {
        *ppv = (IUnknown*) this;
        }
    else
        {
        // 不提供客户端请求的接口
        hrRet = E_NOINTERFACE;
        }
    // 如果返回一个接口指针。 调用AddRef()增加引用计数.
    if ( S_OK == hrRet )
        {
        ((IUnknown*) *ppv)->AddRef();
        }
    return hrRet;
}
在QI()中做了三件不同的事情:
1、初始化传入的指针为NULL[*ppv = NULL;]。
2、检查riid,确定组件对象类(coclass)实现了客户端所请求接口.
[if ( IsEqualIID ( riid, IID_IUnknown ))]
3、如果确实实现勒索请求的接口,则增加COM对象的引用计数。
[((IUnknown*) *ppv)->AddRef();]
AddRef()调用很关键。
    *ppv = (IUnknown*) this;
要创建新的COM对象引用,就必须调用这个函数通知COM对象这个新引用成立。在AddRef
()调用中的强制转换IUnknown*看起来好像多余,但是在QI()中初始化的*ppv有可能不是
IUnknown*类型,所以最好是养成习惯对之进行强行转换。。
上面我们已经讨论了一些DLL服务器的内部细节,接下来让我们回头看一看当客户端调用
CoCreateInstance()时是如何处理服务器的。
深入CoCreateInstance()
在本文的第一部分中,我们见过CoCreateInstance()API,其作用是当客户端请求对象时
,用它来创建对象。从客户端的立场看,它是一个黑盒子。只要用正确的参数调用它即
可得到一个COM对象。它并没有什么魔法,只是在一个定义良好的过程中加载COM服务器
,创建请求的COM对象并返回所要的指针。就这些。
下面让我们来浏览一下这个过程。这里要涉及到几个不太熟悉的术语,但不用着急,后
面会对它们作详细讨论。
1、客户端程序调用CoCreateInstance(),传递组件对象类的CLSID以及所要接口的IID。

2、COM库在HKEY_CLASSES_ROOT\CLSID.键值下查找服务器的CLSID键值,这个键值包含服
务器的注册信息。
3、COM库读取服务器DLL的全路径并将DLL加载到客户端的进程空间。
4、COM库调用在服务器中DllGetClassObject()函数为所请求的组件对象类请求类工厂。

5、服务器创建一个类工厂并将它从DllGetClassObject()返回。
6、COM库在类工厂中调用CreateInstance()方法创建客户端程序请求的COM对象。
7、CreateInstance()返回一个接口指针到客户端程序。
COM服务器注册
COM服务器必须在Windows注册表中正确注册以后才能正常工作。如果你看一下注册表中
的HKEY_CLASSES_ROOT\CLSID键,就会发现大把大把子键,它们就是在这个计算机上注册
的COM服务器。当某个COM服务器注册后(通常是用DllRegisterServer()进行注册),就
会以标准的注册表格式在CLSID键下创建一个键,它名字为服务器的GUID。下面是一个这
样的例子:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
大括弧和连字符是必不可少的,字母大小写均可。
这个键的默认值是人可值别的组件对象类名,使用VC所带的OLE/COM对象浏览器可以察看
到它们。
在GUID键的子键中还可以存储其它信息。需要创建什么子键依赖于COM服务器的类型以及
COM服务器的使用方法。对于本文例子中这个简单的进程内服务器,我们值需要一个子键
:InProcServer32。
InProcServer32键包含两个串:这两个串的缺省值是服务器DLL的全路径和线程模型值(
ThreadingModel)。线程模型超出了本文所涉及的范围,我们先接受这个概念,这里我
们指的是单线程服务器,用的模式为Apartment(即单线程公寓)。
创建COM对象——类工厂
回首看一看客户端的COM,它是如何以自己独立于语言的方式创建和销毁COM对象。客户
端调用CoCreateInstance()创建新的COM对象。现在我们来看看它在服务器端是如何工作
的。
你每次实现组件对象类的时候,都要写一个旁类负责创建第一个组件对象类的实例。这
个旁类就叫这个组件对象类的类工厂(class factory),其唯一目的是创建COM对象。
之所以要一个类工厂,是因为语言无关的缘故。COM本身并不创建对象,因为它不是独立
于语言的也不是独立于实现的。
当某个客户端想要创建一个COM对象时,COM库就从COM服务器请求类工厂。然后类工厂创
建COM对象并将它返回客户端。它们的通讯机制由函数DllGetClassObject()来提供。
术语 “类工厂”和“类对象”实际上是一回事。没有那个单词能精确描述类工厂的作用
和义,但正是这个工厂创建了COM对象,而不是COM类所为。将“类工厂”理解成“对象
工厂”可能会更有助于理解(实际上MFC就是这样理解的——它的类工厂实现就叫做COl
eObjectFactory)。但“类工厂”是正式术语,所以本文也这样用。
当COM库调用DllGetClassObject()时,它传递客户端请求的CLSID。服务器负责为所请求
的CLSID创建者各类工厂并将它返回。类工厂本身就是一个组件对象类,并且实现IClas
sFactory接口。如果DllGetClassObject()调用成功,它返回一个IClassFactory指针给
COM库,然后COM库用IClassFactory接口方法创建客户端所请求的COM对象实例。
一下是IClassFactory接口:
struct IClassFactory : public IUnknown
{
    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObje
ct );
    HRESULT LockServer( BOOL fLock );
};
其中,CreateInstance()是创建COM对象的方法。LockServer()在必要时让COM库增加或
减少服务器的引用计数。
一个定制接口的例子
这个工程是一个能运行的DLL服务器例子,对象由类工厂创建,此DLL服务器在 CSimple
MsgBoxImpl组件对象类中实现了一个接口:ISimpleMsgBox。
接口定义
我们的新接口是ISimpleMsgBox。所有的接口多必须从IUnknown派生。这个接口只有一个
方法:DoSimpleMsgBox()。注意它返回标准类型HRESULT。所有的方法都应该返回HRESU
LT类型,并且所有返回到调用者的其它数据都应该通过指针参数操作。
struct ISimpleMsgBox : public IUnknown
{
    // IUnknown 方法
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );
    // ISimpleMsgBox方法
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsg
Box;
有__declspec的一行将一个GUID赋值给ISimpleMsgBox,并且以后可以用__uuidof操作符
来获取GUID。这两个东西都是微软的C++的扩展。
DoSimpleMsgBox()的第二个参数是BSTR类型。意思是二进制串——即定长序列位的COM表
示。BSTRs主要用于Visual Basic 和 Windows Scripting Host之类的脚本客户端。
接下来这个接口由CSimpleMsgBoxImpl C++类来实现。其定义如下:
class CSimpleMsgBoxImpl : public ISimpleMsgBox
{
public:
        CSimpleMsgBoxImpl();
        virtual ~CSimpleMsgBoxImpl();
    // IUnknown 方法
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );
    // ISimpleMsgBox 方法
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
protected:
    ULONG m_uRefCount;
};
class  __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsg
BoxImpl;
当某一客户端想要创建一个SimpleMsgBox COM对象时,它应该用下面这样的代码:
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
// 组件对象类的CLSID
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl),
    NULL,                         // 非聚合
       CLSCTX_INPROC_SERVER, // 进程内服务器
     __uuidof(ISimpleMsgBox), // 所请求接口的IID
    (void**) &pIMsgBox );         // 返回的接口指针的地址
类工厂实现
我们的类工厂SimpleMsgBox是在一个叫做CSimpleMsgBoxClassFactory的C++类中实现的

class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
    CSimpleMsgBoxClassFactory();
    virtual ~CSimpleMsgBoxClassFactory();
    // IUnknown方法
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );
    // IClassFactory方法
    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv );
    HRESULT LockServer( BOOL fLock );
protected:
    ULONG m_uRefCount;
};
构造函数、析构函数和IUnknown方法都和前面例子中的一样,不同的只有IClassFactor
y的方法,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()中的用法一样——它们是客户端所请求的接口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是客户端所请求的组件对象类的CLSID。这个函数必须返回指定组件对象类的类工
厂。
这里的两个参数: riid 和 ppv类似QI()的参数。不过在这个函数中,riid指的是COM库
所请求的类工厂接口的IID。通常就是IID_IClassFactory。
因为DllGetClassObject()也创建一个新的COM对象(类工厂),所以代码与IClassFact
ory::CreateInstance()十分相似。开始也是进行一些有效性检查以及初始化。
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv )
{
    // 检查客户端所要的CSimpleMsgBoxImpl类工厂
    if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) ))
        return CLASS_E_CLASSNOTAVAILABLE;
    //检查指针ppv是不是void*类型
    if ( IsBadWritePtr ( ppv, sizeof(void*) ))
        return E_POINTER;
    *ppv = NULL;
第一个if语句检查rclsid参数。我们的服务器只有一个组件对象类,所以rclsid必须是
CSimpleMsgBoxImpl类的CLSID。__uuidof操作符获取先前在__declspec(uuid())声明中
指定的CsimpleMsgBoxImpl类的GUID。
下一步是创建一个类工厂对象。
CSimpleMsgBoxClassFactory* pFactory;
    // 构造一个新的类工厂对象
    pFactory = new CSimpleMsgBoxClassFactory;
    if ( NULL == pFactory )
        return E_OUTOFMEMORY;
这里的处理与CreateInstance()中所做的有所不同。在CreateInstance()中是调用了QI
(),并且如果调用失败,则删除COM对象。
我们可以把自己假设成一个所创建的COM对象的客户端,调用AddRef()进行一次引用计数
(COUNT = 1)。然后调用QI()。如果QI()调用成功,它将再一次用AddRef()进行引用计
数(COUNT = 2)。如果QI()调用失败。引用计数将保持为原来的值(COUNT = 1)。
在QI()调用之后,类工厂对象就使用完了,因此要调用Release()来释放它。如果QI()调
用失败,这个对象将自我删除(因为引用计数将为零),所以最终结果是一样的。
// 调用AddRef()增加一个类工厂引用计数,因为我们正在使用它
pFactory->AddRef();
HRESULT hrRet;
    // 调用QI()查询客户端所要的类工厂接口
    hrRet = pFactory->QueryInterface ( riid, ppv );
    // 使用完类工厂后调用Release()释放它
    pFactory->Release();
    return hrRet;
}
再谈QueryInterface()
前面讨论过QI()的实现,但还是有必要再看一看类工厂的QI(),因为它是一个很现实的
例子,其中COM对象实现的不光是IUnknown。首先进行的是对ppv缓冲的有效性检查以及
初始化。
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv )

{
HRESULT hrRet = S_OK;
    //检查指针ppv是不是void*类型
    if ( IsBadWritePtr ( ppv, sizeof(void*) ))
        return E_POINTER;
    //标准的QI初始化,将赋值为NULL.
    *ppv = NULL;
接下来检查riid,看看它是不是类工厂实现的接口之一:IUnknown 或 IclassFactory。

    // 如果客户端请求一个有效接口,则扶植给 *ppv.
    if ( InlineIsEqualGUID ( riid, IID_IUnknown ))
        {
        *ppv = (IUnknown*) this;
        }
    else if ( InlineIsEqualGUID ( riid, IID_IClassFactory ))
        {
        *ppv = (IClassFactory*) this;
        }
    else
        {
        hrRet = E_NOINTERFACE;
        }
最后,如果riid是有效接口,则调用接口的AddRef(),然后返回。
    //如果返回有效接口指针,则调用AddRef()
    if ( S_OK == hrRet )
        {
        ((IUnknown*) *ppv)->AddRef();
        }
    return hrRet;
}
ISimpleMsgBox实现
最后的也是必不可少的一关是ISimpleMsgBox实现,我们的代码只实现ISimpleMsgBox的
方法DoSimpleMsgBox()。首先用微软的扩展类_bstr_t将bsMessageText转换成TCHAR串。

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageT
ext )
{
_bstr_t bsMsg = bsMessageText;
LPCTSTR szMsg = (TCHAR*) bsMsg;   // 如果需要的话,用_bstr_t将串转换为ANSI
做完转换的工作后,显示信息框,然后返回。
    MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK );
    return S_OK;
}
使用服务器的客户端
我们已经完成了一个超级棒的COM服务器,如何使用它呢? 我们的接口一个定制接口,也
就是说它只能被C或C++客户端使用。(如果在组件对象类中同时实现IDispatch接口,那
我们几乎就可以在任何客户端环境中——Visual Basic,Windows Scripting Host,We
b页面,PerlScript等使用COM对象。有关这方面的内容我们留待另外的文章讨论)。本
文提供了一个使用ISimpleMsgBox的例子程序。这个程序基于用Win32应用程序向导建立
的Hello World例子。文件菜单包含两个测试服务器的命令:
如图所示:
Test MsgBox COM Server菜单命令创建CSimpleMsgBoxImpl对象并调用DoSimpleMsgBox(
)。因为这
是个简单的方法,要写的代码不长。
我们先用CoCreateInstance()创建一个COM对象。
void DoMsgBoxTest(HWND hMainWnd)
{
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl),  // 组件对象类的CLSID
                            NULL,                // 非聚合
                            CLSCTX_INPROC_SERVER,  // 只使用进程内服务器
                            __uuidof(ISimpleMsgBox), // 所请求接口的IID
                           (void**) &pIMsgBox );   // 容纳接口指针的缓冲
    if ( FAILED(hr) )
        return;
然后调用DoSimpleMsgBox()方法并释放接口。
    pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );
    pIMsgBox->Release();
}
就这么简单。代码中从头到尾都有TRACE语句,这样在调试器中运行测试程序就可以看到
服务器的每一个方法
是如何被调用的。
另外一个菜单命令是调用CoFreeUnusedLibraries()函数,从中你能看到服务器DllCanU
nloadNow()函数的运行。
其它细节-COM宏
COM代码中有些宏隐藏了实现细节,并允许在C和C++客户端使用相同的声明。本文中没有
使用宏,但在例子代
码中用到了这些宏,所以必须掌握它们的用法。下面是ISimpleMsgBox的声明
struct ISimpleMsgBox : public IUnknown
{
    // IUnknown 方法
    STDMETHOD_(ULONG, AddRef)() PURE;
    STDMETHOD_(ULONG, Release)() PURE;
    STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
    // ISimpleMsgBox 方法
    STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};
STDMETHOD()包含virtual关键字,返回类型和调用规范。STDMETHOD_()也一样,除非你
指定不
同的返回类型。PURE扩展了C++的“=0”,使此函数成为一个纯虚拟函数。
STDMETHOD()和STDMETHOD_()有对应的宏用于方法实现——STDMETHODIMP和STDMETHODIM
P_()。
例如DoSimpleMsgBox()的实现:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMes
sageText )
{
  ...
}
最后,标准的输出函数用STDAPI宏声明,如:
STDAPI DllRegisterServer()
STDAPI包括返回类型和调用规范。要注意STDAPI不能和__declspec(dllexport)一起使用

因为STDAPI的扩展。输出必须使用.DEF文件。
服务器注册以及反注册
前面讲过服务器实现了DllRegisterServer()和DllUnregisterServer()两个函数。它们
的工作是创建和
删除关于COM服务器的注册表入口。其代码都是对注册表的处理,所以在此不必赘言,只
是列出DllRegisterServer()创建的注册表入口:
键名
 键值
HKEY_CLASSES_ROOT
CLSID
{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}
 Default="SimpleMsgBox class"
InProcServer32
 Default=[path to DLL]; ThreadingModel="Apartment"
关于例子代码的注释
本文的例子代码在一个WORKSPACE(工作间)文件中(SimpleComSvr.dsw)同时包含了服
务器的源代码和测试服
务器所用的客户端源代码。在VC的IDE环境中可以同时加载它们进行处理。在工作间的同
级层次有两个工程都要
用到的头文件,但每个工程都有自己的子目录。
同级的公共头文件是:
ISimpleMsgBox.h——定义ISimpleMsgBox的头文件。
SimpleMsgBoxComDef.h——包含__declspec(uuid())的声明。这些声明都在单独的文件
中,因为客户
端需要CSimpleMsgBoxImpl的GUID,不是它的定义。将GUID移到单独的文件中,使客户端
在存取GUID时不依赖
CSimpleMsgBoxImpl的内部结构。它是接口,ISimpleMsgBox,对客户端很重要。
正如前面所说的,必须用.DEF文件来从服务器输出四个标准的输出函数。下面是例子工
程的.DEF文件:
EXPORTS
    DllRegisterServer   PRIVATE
    DllUnregisterServer PRIVATE
    DllGetClassObject   PRIVATE
    DllCanUnloadNow     PRIVATE
每一行都包含函数名和PRIVATE关键字。这个关键字的意思是:此函数是输出函数,但不
包含在输入库(import lib)中。也就是说客户端不能直接从代码中调用这个函数,即
使是链接了输入库也不行。这个关键字时必须要用的,否则链接器会出错。
在服务器中设置断点链
如果你想在服务器代码中设置断点,有两种方法:第一种是将服务器工程(MsgBoxSvr)设
置为活动工程,然后开始调试。MSVC将问你调试会话要运行的可执行程序。输入客户端
测试程序的全路径,你必须事先建立好。第二种方法是将客户端工程(TestClient)设置
为活动工程,配置工程的从属(dependencies)属性,以便服务器工程从属于客户端工
程。这样如果你改变了服务器的代码,那么在编译客户端工程时会自动重新编译服务器
工程代码。最后还要做的是当你开始调试客户端时必须告诉MSVC加载服务器符号(symb
ols)。
下面是设置工程属性的对话框:Project->Dependencies菜单
为了加载服务器符号,打开TestClient的工程设置(Project->Settings菜单),选择D
ebug标签,并在Category组合框中选择Additional DLLs。在列表框中单击New一个入口
,然后输入服务器DLL的全路径名。如下图所示:
这样设置以后,根据实际源代码的所在位置,DLL的路径将会做自动调整。
(第二部分完)

--
他能够不知不觉令到我倾心
    也能够一句说话而留下痛恨
        感情最光辉一刹热暖我一生
            爱人远走了以后仍然是爱人

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 218.30.30.9]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店