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的后续学习帮助很大。