AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > VC编程

COM编程入门(二)

51自学网 2015-08-30 http://www.51zixue.net

本文为刚刚接触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++类中实现IUnknown。下面是这个类的声明:
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()将摧毁对象。Release()也返回更新的引用计数。注意Release()的实现假设COM对象在堆中创建。如果你在全局粘上创建某个对象,当对象试图删除自己时就会出问题。
现在应该明白了为什么在客户端应用程序中正确调用AddRef()和 Release()是如此重要!如果在这了做得不对,你使用的对象会被很快摧毁,这样的话在整个服务器中内存会很快溢出导致应用程序下次存取服务器代码时崩溃。
如果你编写多线程应用,可能会想到使用++&替代InterlockedIncrement()和InterlockedDecrement()的线程安全问题。++&——用于单线程服务器很保险,因为即使客户端应用是多线程的并从不同的线程中进行方法调用,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()返回一个接口指针到客户端程序。

<

 

 

 
说明
:本教程来源互联网或网友上传或出版商,仅为学习研究或媒体推广,51zixue.net不保证资料的完整性。
上一篇:深度解析VC中的消息传递机制  下一篇:Visual&nbsp;C++开发工具与调试技巧整理