COM+ Programming [Note](1)

  最近要搞一个ATL ActiveX的项目,把COM相关的内容整理一下,推荐一本书【COM+ Programming: A Practical Guide Using Visual C++ and ATL】。

一. COM综述

  COM即Microsoft Component Object Model, 是一个开发框架,具有语言无关性,进程无关性等特点。

  COM+是COM变革过程中的一个新的台阶,有点类似C/C++的关系,后续文章会讲述COM+的核心内容。

  组件模型:(Component Model)

  组件模型不是什么新概念,在硬件和其它领域早已有所体现。举个例子,比如为了给家里置办一套家庭影院,你回去家电城采购电视机,DVD机,影响等配件,然后回来简单组装即可。电视机和其它配件可以根据自己的喜好,选择不同的品牌,比如Sharp的电视机,先锋的DVD,惠威的音响等。他们之间通过统一的接口标准来连接,以给你完美的视听体验。

  那么在软件中,这种理想化的模型该如何实现呢,即如何实现软件复用(Software Reusability)?  

  关于编程语言的选择:这里选择C++。C++是面向对象编程语言,提供语义上的封装性,但没有提供二进制意义上的封装,为了在不同语言之间能够实现复用,需要提供二进制意义上的封装。C++本身是实现COM的比较好的手段。并非只有C++才能实现COM,COM是一种规范,只要符合某种内存布局即可,下面会提到这种内容布局。而C++虚函数的内存布局恰好和COM规范的内存布局吻合,所以C++实现COM更合适而流畅。关于如何实现二进制封装,主要通过接口和实现的分离来实现,接口不变即可。

  为了使得C++在各编译器中的行为统一,需要规范一下相关实现:

  Implementation of _stdcall Compiler Directive

Element

Implementation

Argument-passing order

Right to left

Argument-passing convention

By value, unless a pointer or reference type is passed

Stack-maintenance responsibility

Called function pops its own arguments from the stack

Case-translation convention

None

 

二. C++虚函数和内存布局:

  网上有很多好文章,大家可以参阅一下,如果我有时间,会另写一篇文章关于内存布局的:

 

三. 模拟组件模型实现机制

  首先,有人可能会考虑,将某个类的定义以DLL的形式导出,觉得在将来更新C++类定义,比如添加私有成员,对外部调用类成员函数没有影响,真是如此吗?再次强调C++提供的是语义上的封装性,并且添加成员直接导致对象内存布局的改变。对客户端的调用有不可预见的影响。

  为了实现二进制的封装,一种方法是将接口定义和实现分开。可以将原有类,分为两个:一个接口类,一个实现类。这样的优点是,实现类可以添加删除变量或函数,对接口没有影响。客户只需要知道接口类提供的功能,而不需要知道实现类的具体细节。只要接口类不变,客户端也不需要变化。如何将接口类和实现类联系起来?如下是一种解决方法:

class CVcr;

class IVideo
{
public:
  IVideo();
  ~IVideo();
  long GetSignalValue();

private:
  CVcr* m_pActualVideo;   //在接口类中提供一个固定的实现类指针。
};

// Video.cpp—Implementation of interface IVideo
IVideo::IVideo()
{
  m_pActualVideo = new CVcr;
}

IVideo::~IVideo()
{
  delete m_pActualVideo;

}

long IVideo::GetSignalValue()
{
  return m_pActualVideo->GetSignalValue();   //接口函数,调用实现类的实现细节
}

  该处理方法的缺点显而易见:

  (1). C++特有的名字破坏(引入重载和类的产物),在类函数调用时,不同编译器下并没有规定该如何生成函数名称。

  (2). 性能损失,每次调用接口函数时,实际上调用了实现类的方法,调用了两次。

  (3). 如果一个庞大的类库有上百的方法,那么需要写上百个类似IVideo::GetSignalValue()的函数,Oh,My God!

  

  回忆下C++虚函数的内存布局,它使用vtbl机制来调用成员函数,虚函数可以避开名字破坏。我们可以尝试用虚函数机制来实现接口。另有三个问题需要回避:

  (1). vptr的位置,各编译器实现不同,所以最好在接口定义中能够不带任何成员定义。

  (2).多重继承时会存在多个vptr,各编译器对vptr的放置顺序并不一致,最好不要多重继承。(注意:仅只接口的多继承,实现的多继承是可行的)

  (3). 函数重载后,vtbl中函数的位置可能在各编译器中不同,所以最好不要存在函数重载。

  现在我们可以规范一下接口类的定义:(这样,如此定义的一个接口类称为抽象基类)

  (1). 只包含纯虚函数

  (2). 不包含任何虚函数的重载

  (3). 不包含任何成员变量

  (4). 只继承自一个基类

  现在我们重新定义下,前面的例子:

// Video.h - Definition of interface IVideo
class IVideo
{
public:
  virtual long _stdcall GetSignalValue() = 0;
};

// File vcr.h
#include "Video.h"

class CVcr : public IVideo
{
public:
  CVcr(void);

  long _stdcall GetSignalValue();

private:
  long m_lCurValue;
  int m_nCurCount;
};

  看似离目标越来越近了,遇到了新的问题。C++编译器不允许实例化抽象基类。但如果将实现类的细节暴露给客户,那么又绕开了接口类的二进制封装。一种理想的做法,从DLL导出一个全局函数,返回一个基类的实例。

// Video.h
extern "C" IVideo* _stdcall CreateVcr();

// Vcr.cpp (implementation)
IVideo* _stdcall CreateVcr(void)
{
  return new CVcr;
}

  通过这种factory机制(类厂的引入)创建需要的实例。调用如下:
#include "Video.h"
#include <iostream.h>

int main(int argc, char* argv[])
{
  int i;
  IVideo* pVideo = CreateVcr();
  for(i=0; i<10; i++) {
        long val = pVideo->GetSignalValue();
        cout << "Round: " << i << " - Value: " << val << endl;
  }

  delete pVideo;       // we are done with it

  return 0;
}
  这里有个小问题,大家可能注意到了,delete和new在客户和服务端是分开做的。在不同的编译器间,可能引发不可预见的错误。一种方法是在接口中添加一个Delete虚函数。

// Video.h - Definition of interface IVideo

class IVideo
{
public:
  virtual long _stdcall GetSignalValue() = 0;
  virtual void _stdcall Delete() = 0;
};

// File vcr.h

#include "Video.h"

class CVcr : public IVideo
{
public:
  CVcr(void);

  long _stdcall GetSignalValue();
  void _stdcall Delete();
private:
  long m_lCurValue;
  int m_nCurCount;
};

// File vcr.cpp
void CVcr::Delete()
{
  delete this;
}

  通过这种机制,调用如下:

int main(int argc, char* argv[])
{
  int i;
  IVideo* pVideo = CreateVcr();

  for(i=0; i<10; i++) {
    long val = pVideo->GetSignalValue();
    cout << "Round: " << i << " - Value: " << val << endl;
  }

  pVideo->Delete();

  return 0;
}

  为了增强通用性,我们能够针对任意厂商的VCR进行选择。大家很快就能想到,应该用DLL实现。直接上代码:

IVideo* CreateInstance(char* pszDll)
{
  // Define a pointer to the prototype of CreateVcr function
  typedef IVideo* (_stdcall *CREATEVCRPROC)(void);

  // Load the specified library
  HINSTANCE h = LoadLibrary(pszDll);

  // Obtain the procedure entry point for CreateVcr
  CREATEVCRPROC proc =
    reinterpret_cast<CREATEVCRPROC> (GetProcAddress(h, "CreateVcr"));

  // Execute "CreateVcr" indirectly
  return (*proc)();
}

int main(int argc, char* argv[])
{
  int i;
  IVideo* pVideo = CreateInstance("vcr.dll");

  for(i=0; i<10; i++) {
    long val = pVideo->GetSignalValue();
    cout << "Round: " << i << " - Value: " << val << endl;
  }

  pVideo->Delete();

  return 0;
}

  代码很简单,这里引入的是一种思想,CreateInstance指定需要哪个"厂商",该厂商提供了"CreateVcr",返回的实例提供了接口的方法实现。

 

  下面我们来扩展接口。既然是扩展,就不能把现有功能破坏了,现有功能继续可用,新功能提供给新客户。老客户不受影响。组件是透明的,客户需要某种手段来查询是否支持某些功能。

  首先我们先定义新的功能接口:
// SVideo.h - Definition of interface ISVideo
class ISVideo
{
public:
  virtual long _stdcall GetSVideoSignalValue() = 0;   //具体功能函数
  virtual void _stdcall Delete() = 0;   //接口提供delete方法,删除相关内容
};

  新实现,同时继承ISVideo,IVideo (大家回忆下多继承时的内存布局,vtbl的分布)

class CVcr : public IVideo, public ISVideo
{
  ...
}

  为了找到指定的vtbl,我们可以添加函数Probe来查询是否支持某些接口:

IVideo* pVideo = CreateInstance("vcr.dll");

ISVideo* pSVideo = pVideo->Probe("svideo");
if (pSVideo != NULL) {
  // use S-Video
}else {
  // use Video signal
}

  这里有个问题,如果dll的实现,只继承了ISVideo,那么结果可想而知,我们可以提供另一个接口,IGeneral。将查询函数和删除函数抽出。

class IGeneral
{
public:
  virtual IGeneral* _stdcall Probe(char* pszType) = 0;   //类似QueryInterface
  virtual void _stdcall Delete() = 0;
};
class IVideo : public IGeneral
{
  ...
};
class ISVideo : public IGeneral
{
  ...
};
extern "C" IGeneral* _stdcall CreateVCR();

 

  实现类的定义变更为:

class CVcr : public IVideo, public ISVideo
{
public:
  // IGeneral interface
  IGeneral* _stdcall Probe(char* pszType);
  void _stdcall Delete();

  // IVideo interface
  long _stdcall GetSignalValue();

  // ISVideo interface
  long _stdcall GetSVideoSignalValue();

private:
  // other member variables and methods not shown for brevity
};

 

Probe方法的实现:(QueryInterface)

IGeneral* CVcr::Probe(char* pszType)
{
  IGereral* p = NULL;
  if (!stricmp(pszType, "general")) {
    p = static_cast<IVideo*>(this);
  }else
  if (!stricmp(pszType, "video")) {
    p = static_cast<IVideo*>(this);
  }else
  if (!stricmp(pszType, "svideo")) {
    p = static_cast<ISVideo*>(this);   //注意这里的static_cast转型后,指向ISVideo的Vtbl。
  }

  return p;
}

  CreateVCR的实现,这里转型为IVideo,如果直接返回IGeneral,存在二义性,IVideo和ISVideo都继承自IGeneral。

IGeneral* _stdcall CreateVCR(void)   //类似CreateObject,在CreateInstance中调用
{
  return static_cast<IVideo*>(new CVcr);
}

  下面给出调用过程:

// TV client code
int main(int argc, char* argv[])
{

 //获取类似的IUnknow指针
  IGeneral* pVCR = CreateInstance("vcr.dll");

  // Use S-Video if available

  //查询接口是否支持
  IGeneral* pGeneral = pVCR->Probe("svideo");
  if (NULL != pGeneral) {
    ISVideo* pSVideo = reinterpret_cast<ISVideo*>(pGeneral);
    UseSVideo(pSVideo);
    pSVideo->Delete();
    return 0;
  }

  // S-Video not available. Try old "video" type 

  //查询接口是否支持
  pGeneral = pVCR->Probe("video");
  if (NULL != pGeneral) {
    IVideo* pVideo = reinterpret_cast<IVideo*>(pGeneral);
    UseVideo(pVideo);
    pVideo->Delete();
    return 0;
  }

  // Neither S-Video nor Video

  //未能查询到指定接口
  cout << "This VCR does not have the signals this TV supports" << endl;
  pVCR->Delete();
  return 1;
}

  对于接口生命期的控制,我们考虑在对象级别使用引用计数来处理。

class CVcr : public IVideo, public ISVideo
{
public:
  ...
public:
  // A helper function to increment the reference count
  void AddReference();   
private:
  ...
  long m_lRefCount;      // count of outstanding copies of interfaces
};

  在构造函数中,初始化为0。

CVcr:: CVcr()
{
  ...
  m_lRefCount = 0;
}

  析构时,查看引用计数,为0则自动删除对象。

void CVcr::Delete()
{
  if ( (—m_lRefCount) == 0) {
    delete this;
  }
}

  对于引用计数的增加时机,有两个对象创建和成功的查询。(其实还有一种,接口的拷贝)

IGeneral* CreateVCR(void)
{
  CVcr* p = new CVcr;
  if (NULL == p)
    return p; 
  p->AddReference(); 

  return static_cast<IVideo*>(p);
}

IGeneral* CVcr::Probe(char* pszType)
{
  IGeneral* p = NULL;
  if (!stricmp(pszType, "general")) {
    p = static_cast<IVideo*>(this);
  }else
  if (!stricmp(pszType, "video")) {
    p = static_cast<IVideo*>(this);
  }else
  if (!stricmp(pszType, "svideo")) {
    p = static_cast<ISVideo*>(this);
  }

  if (NULL != p) {
    AddReference();
  }

  return p;
}

  对于接口拷贝的情况,需要客户端来进行引用计数的增加,服务端无法指定何时会拷贝。那么我们必须将AddReference方法放入IGeneral接口中。

class IGeneral
{
public:
  virtual IGeneral* _stdcall Probe(char* pszType) = 0;
  virtual void _stdcall AddReference() = 0;
  virtual void _stdcall Delete() = 0;
};

  从上面看出,客户端对接口生命期的控制总结为如下:

  (1). 如果客户端从服务器获取了一个接口指针,它必须在使用完该指针后,调用Delete.

  (2). 如果客户端生成了接口指针的一个拷贝,需要对拷贝调用AddReference,并在使用完后调用Delete。可以调用多次AddReference,Delete必须匹配。

 

  到此,已经基本模拟了COM的原理。理解了这些对COM的后续学习帮助很大。

posted @ 2010-09-30 11:51  友学友  阅读(581)  评论(0编辑  收藏  举报