常见问题总结5

yintongshun 2003-12-02 12:04:55
加精
用VC进行COM编程所必须掌握的理论知识


  这篇文章是给初学者看的,尽量写得比较通俗易懂,并且尽量避免编程细节。完全是根据我自己的学习体会写的,其中若有技术上的错误之处,请大家多多指正。
  一、为什么要用COM
  软件工程发展到今天,从一开始的结构化编程,到面向对象编程,再到现在的COM,编程,目标只有一个,就是希望软件能象积方块一样是累起来的,是组装起来的,而不是一点点编出来的。结构化编程是函数块的形式,通过把一个软件划分成许多模块,每个模块完成各自不同的功能,尽量做到高内聚低藕合,这已经是一个很好的开始,我们可以把不同的模块分给不同的人去做,然后合到一块,这已经有了组装的概念了。软件工程的核心就是要模块化,最理想的情况就是100%内聚0%藕合。整个软件的发展也都是朝着这个方向走的。结构化编程方式只是一个开始。
  下一步就出现了面向对象编程,它相对于面向功能的结构化方式是一个巨大的进步。我们知道整个自然界都是由各种各样不同的事物组成的,事物之间存在着复杂的千丝万缕的关系,而正是靠着事物之间的联系、交互作用,我们的世界才是有生命力的才是活动的。我们可以认为在自然界中事物做为一个概念,它是稳定的不变的,而事物之间的联系是多变的、运动的。事物应该是这个世界的本质所在。面向对象的着眼点就是事物,就是这种稳定的概念。
  每个事物都有其固有的属性,都有其固有的行为,这些都是事物本身所固有的东西,而面向对象的方法就是描述出这种稳定的东西。而面向功能的模块化方法它的着眼点是事物之间的联系,它眼中看不到事物的概念它只注重功能,我们平常在划分模块的时侯有没有想过这个函数与哪些对象有关呢?很少有人这么想,一个函数它实现一种功能,这个功能必定与某些事物想联系,我们没有去掌握事物本身而只考虑事物之间是怎么相互作用而完成一个功能的。说白了,这叫本末倒置,也叫急功近利,因为不是我们智慧不够,只是因为我们没有多想一步。
  面向功能的结构化方法因为它注意的只是事物之间的联系,而联系是多变的,事物本身可能不会发生大的变化,而联系则是很有可能发生改变的,联系一变,那就是另一个世界了,那就是另一种功能了。如果我们用面向对象的方法,我们就可以以不变应万变,只要事先把事物用类描述好,我们要改变的只是把这些类联系起来方法只是重新使用我们的类库,而面向过程的方法因为它构造的是一个不稳定的世界,所以一点小小的变化也可能导致整个系统都要改变。然而面向对象方法仍然有问题,问题在于重用的方法。搭积木式的软件构造方法的基础是有许许多多各种各样的可重用的部件、模块。
  我们首先想到的是类库,因为我们用面向对象的方法产生的直接结果就是许多的类。但类库的重用是基于源码的方式,这是它的重大缺陷。首先它限制了编程语言,你的类库总是用一种语言写的吧,那你就不能拿到别的语言里用了。其次你每次都必须重新编译,只有编译了才能与你自己的代码结合在一起生成可执行文件。在开发时这倒没什么,关键在于开发完成后,你的EXE都已经生成好了,如果这时侯你的类库提供厂商告诉你他们又做好了一个新的类库,功能更强大速度更快,而你为之心动又想把这新版的类库用到你自己的程序中,那你就必须重新编译、重新调试!这离我们理想的积木式软件构造方法还有一定差距,在我们的设想里希望把一个模块拿出来再换一个新的模块是非常方便的事,可是现在不但要重新编译,还要冒着很大的风险,因为你可能要重新改变你自己的代码。另一种重用方式很自然地就想到了是DLL的方式。Windows里到处是DLL,它是Windows的基础,但DLL也有它自己的缺点。总结一下它至少有四点不足:
  (1)函数重名问题
  DLL里是一个一个的函数,我们通过函数名来调用函数,那如果两个DLL里有重名的函数怎么办?
  (2)各编译器对C++函数的名称修饰不兼容问题
  对于C++函数,编译器要根据函数的参数信息为它生成修饰名,DLL库里存的就是这个修饰名,但是不同的编译器产生修饰的方法不一样,所以你在VC里编写的DLL在BC里就可以用不了。不过也可以用extern "C";来强调使用标准的C函数特性,关闭修饰功能,但这样也丧失了C++的重载多态性功能。
  (3)路径问题
  放在自己的目录下面,别人的程序就找不到,放在系统目录下,就可能有重名的问题。而真正的组件应该可以放在任何地方甚至可以不在本机,用户根本不需考虑这个问题。
  (4)DLL与EXE的依赖问题
  我们一般都是用隐式连接的方式,就是编程的时侯指明用什么DLL,这种方式很简单,它在编译时就把EXE与DLL绑在一起了。如果DLL发行了一个新版本,我们很有必要重新链接一次,因为DLL里面函数的地址可能已经发生了改变。DLL的缺点就是COM的优点。
  首先我们要先把握住一点,COM和DLL一样都是基于二进制的代码重用,所以它不存在类库重用时的问题。另一个关键点是,COM本身也是DLL,既使是ActiveX控件.ocx它实际上也是DLL,所以说DLL在还是有重用上有很大的优势,只不过我们通过制订复杂的COM协议,通COM本身的机制改变了重用的方法,以一种新的方法来利用DLL,来克服DLL本身所固有的缺陷,从而实现更高一级的重用方法。
  COM没有重名问题,因为根本不是通过函数名来调用函数,而是通过虚函数表,自然也不会有函数名修饰的问题。路径问题也不复存在,因为是通过查注册表来找组件的,放在什么地方都可以,即使在别的机器上也可以。也不用考虑和EXE的依赖关系了,它们二者之间是松散的结合在一起,可以轻松的换上组件的一个新版本,而应用程序混然不觉。


  二、用VC进行COM编程,必须要掌握哪些COM理论知识
  我见过很多人学COM,看完一本书后觉得对COM的原理比较了解了,COM也不过如此,可是就是不知道该怎么编程序,我自己也有这种情况,我经历了这样的阶段走过来的。要学COM的基本原理,我推荐的书是《COM技术内幕》。但仅看这样的书是远远不够的,我们最终的目的是要学会怎么用COM去编程序,而不是拼命的研究COM本身的机制。所以我个人觉得对COM的基本原理不需要花大量的时间去追根问底,没有必要,是吃力不讨好的事。其实我们只需要掌握几个关键概念就够了。这里我列出了一些我自己认为是用VC编程所必需掌握的几个关键概念。(这里所说的均是用C语言条件下的COM编程方式)
  (1) COM组件实际上是一个C++类,而接口都是纯虚类。组件从接口派生而来我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:

    class IObject
    {
    public:
        virtual Function1(...) = 0;
        virtual Function2(...) = 0;
        ....
    };
    class MyObject : public IObject
    {
    public:
        virtual Function1(...){...}
        virtual Function2(...){...}
        ....
    };
  看清楚了吗?IObject就是我们常说的接口,MyObject就是所谓的COM组件。切记切记接口都纯虚类,它所包含的函数都是纯虚函数,而且它没有成员变量。而COM组件就是从这些纯虚类继承下来的派生类,它实现了这些虚函数,仅此而已。从上面也可以看出,COM组件是以C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。为了让大家确切了解一下虚函数表是什么样子


...全文
124 10 打赏 收藏 转发到动态 举报
写回复
用AI写文章
10 条回复
切换为时间正序
请发表友善的回复…
发表回复
yintongshun 2003-12-02
  • 打赏
  • 举报
回复

COuter包含了聚合组件CAgg,它包含了几个不同之处:
(1)加入了COM_INTERFACE_ENTRY_AGGREGATE(IID_IAgg, m_pUnkAgg.p)宏。

#define COM_INTERFACE_ENTRY_AGGREGATE(iid, punk)\
{&iid,\
(DWORD)offsetof(_ComMapClass, punk),\
_Delegate},

offsetof我们在上一节中已经见过,可以猜到它求的就是punk在类中的位置。也就是m_pUnkAgg在COuter中的位置。
(2)加入了宏DECLARE_GET_CONTROLLING_UNKNOWN(),其定义为:

#define DECLARE_GET_CONTROLLING_UNKNOWN() public:\
virtual IUnknown* GetControllingUnknown() {return GetUnknown();}

我们也没必要继续深究下去,仅从字面意思就可以看出这个函数将返回组件的IUnknown 指针。
(3)在COuter中加入一个成员变量:CComPtr< IUnknown > m_pUnkAgg; m_pUnkAgg将用于获得被聚集组件的IUnknown指针。

(4)重载了FinalConstruct,FinalRelease

HRESULT COuter::FinalConstruct()
{
IUnknown* pUnkOuter = GetControllingUnknown();
HRESULT hRes = CoCreateInstance(CLSID_CAgg, pUnkOuter, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkAgg);
return hRes;
}

void COuter::FinalRelease()
{
m_pUnkAgg.Release();
.....
}

当创建组件COuter后将会调用FinalConstruct,所以会在这里创建聚集组件。原则上聚集组件可以仅在需要的时候才创建,但也可以随着包含它的组件一起创建。聚集组件的创建没什么特别之处,只是要注意它将查询IUnknown指针,并返回给m_pUnkAgg.外部组件将通过m_pUnkAgg操作聚集组件。另外注意到使用pUnkOuter作为CoCreateInstance的参数,这将导致创建CComAggObject< COuter >对象,内部包含一个CComContainedObject的包含对象。与上一节中的CComCachedTearOff<>类似,CComAggObject< COuter >也不是从COuter派生的,所以真正的组件对象不是CComAggObject< COuter >对象,而是它内部包含的CComContainedObject< COuter >对象。同样pUnkOuter得到的将是CComAggObject<>的IUnknown指针,也同样调用它的QueryInterface会转而调用CComContainedObject的_InternalQueryInterface函数(呵呵,现在可都还是我猜的,看我猜的对不对吧)
运行pOuter->QueryInterface(IID_IAgg, (void **)&pAgg1)

函数堆栈一:

9.ATL::AtlInternalQueryInterface(...)
8.ATL::CComObjectRootBase::InternalQueryInterface(...)
7.CAgg::_InternalQueryInterface(...)
6.ATL::CComAggObject< CAgg >::QueryInterface(...)
5.ATL::CComObjectRootBase::_Delegate(...)
4.ATL::AtlInternalQueryInterface(...)
3.ATL::CComObjectRootBase::InternalQueryInterface(...)
2.COuter::_InternalQueryInterface(...)
1.ATL::CComObject< COuter >::QueryInterface(...)
解释:

1-5:这几步函数调用我们已经见了很多次了,因为在这个宏定义使用了_Delegate,所以将调用CComObjectRootBase::_Delegate(...).

static HRESULT _Delegate(void* pv,REFIID iid,void** ppvObject,DWORD dw)
{
HRESULT hRes = E_NOINTERFACE;
IUnknown* p = *(IUnknown**)((DWORD)pv + dw);
if (p != NULL) hRes = p->QueryInterface(iid, ppvObject);
return hRes;
}
第二句话的含义我们在上一节中已经见过了,最后的结果p=COuter::m_pUnkAgg.
6:正如我们刚才所料,现在调用的是CComAggObject< CAgg >::QueryInterface()

STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{
//如果查询的是IUnknown,则....
else
hRes = m_contained._InternalQueryInterface(iid, ppvObject);
return hRes;
}

也正如我们所料,将交给它的包含对象去做.(这段代码在上一节好象也见过是吧,呵呵)
7-9:同上一节一样,将交给CAgg::_InternalQueryInterface(...),剩下的工作将由CAgg完成了。最后返回的指针实际上将是CComContainedObject< CAgg >组件的接口指针。

运行pAgg1->QueryInterface(IID_IAgg, (void **)&pAgg2)

函数堆栈二:

9.CAgg::_InternalQueryInterface(...)
8.ATL::CComAggObject< CAgg >::QueryInterface(...)
7.ATL::CComObjectRootBase::_Delegate(...)
6.ATL::AtlInternalQueryInterface(...)
5.ATL::CComObjectRootBase::InternalQueryInterface(...)
4.COuter::_InternalQueryInterface(...)
3.ATL::CComObject< COuter >::QueryInterface(...)
2.ATL::CComObjectRootBase::OuterQueryInterface(...)
1.ATL::CComContainedObject< CAgg >::QueryInterface(...)

解释:

1-9:浏览整个堆栈,与我们上一节所见的堆栈二太相近了,这是因为都是使用了包含对象。包含对象起了个代理的作用,他先把查询交给外部对象(COuter)去做(第1,2步), 当外部对象发现要查询的是聚集组件的接口时(IAgg),就会再把查询交还给它保留的聚集组件的指针(m_pUnkAgg,第7步中,注意这不是真正的聚集组件),m_pUnkAgg再把查询交给包含对象(第8步中),包含对象再把查询交给真正实现接口的类CAgg(第9步). 若外部对象发现要查询的是外部组件的接口时,那就很简单了,直接查询就行了。这样就防止了外部组件与聚集组件查询操作的不一致性。唉,真个过程真麻烦,不过还好,与上一节的宏很类似。相关的源码可参看上一节。

六、COM_INTERFACE_ENTRY_AGGREGATE_BLIND 参ATL例程COMMAP

上一节我们讲了COM_INTERFACE_ENTRY_AGGREGATE,这节要介绍的宏与它很类似。

#define COM_INTERFACE_ENTRY_AGGREGATE_BLIND(punk)\
{NULL,\
(DWORD)offsetof(_ComMapClass, punk),\
_Delegate},

从定义上就可以看出,它与上一节介绍宏的唯一区别就在于,它没有指明接口ID!!
所以在它的定义中第一项也是NULL。
这个宏的用法与我们COM_INTERFACE_ENTRY_AGGREGATE一模一样。大家可以参考上一节内容以及ATL的例程COMMAP。
我们来看看AtlInternalQueryInterface()中的相关代码。
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
//如果是IUnknown,....
while (pEntries->pFunc != NULL)
{
BOOL bBlind = (pEntries->piid == NULL);
if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid))
{
if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset
{
ATLASSERT(!bBlind);
IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
else
{
HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw);
if (hRes == S_OK || (!bBlind && FAILED(hRes))) return hRes;
}
}
pEntries++;
}
return E_NOINTERFACE;
}


yintongshun 2003-12-02
  • 打赏
  • 举报
回复
这里contained就是CTearOff2,contained::_OwnerClass就是COuter,可见m_contained保存了外部对象的指针。
9:创建完对象后,将查询接口ITearOff2

STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{
//如果是IUnknown,...,返回IUnknwon接口指针
else
hRes = m_contained._InternalQueryInterface(iid, ppvObject);
.....
}

注意,这里把查询工作交给了m_contained,也就是一个CComContainedObject对象。不过现在查询的是IUnknown指针,别忘了,我们在COuter中还定义了一个IUnknown指针呢,现在查询的就是它!!
8:经过一系列退栈,退到_Cache()中,现在还要继续查询ITearOff2接口。是根据我们刚刚查询到的IUnknown指针查询ITearOff2。所以再一次进入 ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...),不过这回将调用的是m_contained._InternalQueryInterface(...)了。

9:因为CComContainedObject m_contained的基类是CTearOff2,所以将调用CTearOff2::_InternalQueryInterface(...)

剩下的操作就没什么特别之处了,仅仅一般的查询操作。

执行pTear1->QueryInterface(ITearOff2, (void **)&pTear2);

函数堆栈二:

12.ATL::AtlInternalQueryInterface(...)
11.ATL::CComObjectRootBase::InternalQueryInterface(...)
10.CTearOff2::_InternalQueryInterface(...)
9.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...)
8.ATL::CComObjectRootBase::_Cache(...)
7.COuter::_Cache(...)
6.ATL::AtlInternalQueryInterface(...)
5.ATL::CComObjectRootBase::InternalQueryInterface(...)
4.COuter::_InternalQueryInterface(...)
3.ATL::CComObject< COuter >::QueryInterface(...)
2.ATL::CComObjectRootBase::OuterQueryInterface(...)
1.ATL::CComContainedObject< CTearOff2 >::QueryInterface(...)

解释:

1:第一步就可能使我们迷惑了,为什么执行的是CComContainedObject::QueryInterface
在上一节中,执行的是ATL::CComTearOffObject< CTearOff1 >::QueryInterface(...),所以我们也自然而然的猜想,这里应该执行的是CComCachedTearOffObject的函数。但是来看看CComCachedTearOffObject的定义:

template < class contained >
class CComCachedTearOffObject :
public IUnknown,
public CComObjectRootEx< contained::_ThreadModel::ThreadModelNoCS >
{ ... };

原来CComCachedTearOffObject没有从contained类(在这里就是CTearOff2)中继承,而 CComTearOffObject却是从CTearOff1继承的!所以我们刚才得到的pTear1就不可能是CComCachedTearOffObject的对象。而实际上,CComContainedObject是从CTearOff2继承的,在上面的函数堆栈中第9步查询ITearOff2接口时,把工作交给了m_contained, 这是个CComContainedObject< CTearOff2 >对象,所以实际上最后查询得到的ITearOff2 指向的是CComContainedObject< CTearOff2 >对象。所以现在执行的会是 CComContainedObject::QueryInterface(...)!!!
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{
HRESULT hr = OuterQueryInterface(iid, ppvObject);
if (FAILED(hr) && _GetRawUnknown() != m_pOuterUnknown)
hr = _InternalQueryInterface(iid, ppvObject);
return hr;
}
//?m_pOuterUnknown


2:

HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject)
{
return m_pOuterUnknown->QueryInterface(iid, ppvObject);
}

把查询工作交给外部对象完成,也就是COuter。
第一、二步的功能与上一节中所讲的一样,都是交给外部对象去处理,不同之处在下面
3-8:COuter中的查询过程与上例中无异,我们可以直接跳到第8步中

static HRESULT WINAPI _Cache(void* pv, REFIID iid, void** ppvObject,DWORD dw)
{
....
if (*pp == NULL) hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp);
if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject);
return hRes;
}

还记得我们在COuter中定义了一个IUnknown指针m_pUnkTearOff2吧,我们在第一次查询 ITearOff2接口时,创建了CTearOff2对象,并查询一个IUnknown指针给了它.现在它就发挥作用了,在_Cache中将判断如果m_pUnkTearOff2不等于空,则表明CTearOff2已经创建就不会再创建它了,而是直接用它去查询接口.
9:所以现在将调用CComCachedTearOffObject< CTearOff2 >::QueryInterface(...),在上一个函数堆栈中的第9步中我们已经看到了这个QueryInterface(...)的代码,它把查询工作交给m_contained._InternalQueryInterface(.),其实因为CComContainedObject中没有定义BEGIN_COM_MAP宏,所以也没有定义_InternalQueryInterface(),所以实际上调用的是它包含的类的函数,即CTearOff2::_InternalQueryInterface(...)

10-12:以下的工作就很简单了,不再赘述。

总结:

COM_INTERFACE_ENTRY_CACHED_TEAR_OFF是个相对比较麻烦的宏,它与上一节介绍的宏相比不同之处就在于创建分割接口对象的过程只用进行一次,如果对象已经创建,则下一次查询该对象的接口时不会再创建一个新的分割对象。为了达到这个目的,它在外部对象中包含了一个IUnknown指针,并在第一次创建分割对象时查询这个IUnknown指针,这样就可以通过判断这个指针是否为空来知道这个分割对象是否已经创建,从而决定是否创建新的分割对象,并通过它去查询分割对象内其它接口。这里特别需要注意的是,实际上有两个对象被创建,一个是CComCachedTearOffObject< CTearOff2 >,另一个是 CComContainedObject< CTearOff2 >。并且第一个对象内部实现了第二个对象,真正的查询工作也是交给第二个对象去做。COuter::m_pUnkTearOff2是前面一个对象的IUnknown指针,当用它去查询ITearOff2时,实际上是交给了其内部对象m_contained去做了,这在第8、9步可以看得很清楚。
终于把这个宏讲完了,我感觉这个宏可能是ATL接口映射宏中实现最复杂的了,其实它并没有利用到CComContainedObject的真正功能。感觉实现这个宏也许不应这么麻烦的。

五.COM_INTERFACE_ENTRY_AGGREGATE(iid, punk) 参ATL例程COMMAP

这一节中将介绍ATL中用于聚集对象的宏。聚集对象的概念请参阅其它参考书。
现在先看一看这个宏的典型用法:


class CAgg :
public IDispatchImpl< IAgg, &IID_IAgg, &LIBID_AGGREGLib >,
public ISupportErrorInfo,
public CComObjectRoot,
public CComCoClass< CAgg,&CLSID_CAgg >
{
.....
};

CAgg是一个聚集类,它的实现与一般的ATL组件没有区别,只是注意在它的类定义中不要加入DECLARE_NO_AGGREGATABLE.
class COuter :
public CChainBase,
public IDispatchImpl< IOuter, &IID_IOuter, &LIBID_COMMAPLib >,
public CComCoClass< COuter,&CLSID_COuter >
{
HRESULT FinalConstruct();
void FinalRelease();

BEGIN_COM_MAP(COuter)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IAgg, m_pUnkAgg.p)
END_COM_MAP()

DECLARE_GET_CONTROLLING_UNKNOWN()

CComPtr< IUnknown > m_pUnkAgg;
};

yintongshun 2003-12-02
  • 打赏
  • 举报
回复
同我们所见到的大多数Creator类一样,它也只有一个静态CreateInstance函数。现在我们终于可以创建我们分割组件了,它不是CTearOff1,它也是经了一层包装的,是 CComTearOffObject! 现在我们再来看看它的构造函数干了些什么事:
CComTearOffObject(void* pv)
{
ATLASSERT(m_pOwner == NULL);
m_pOwner = reinterpret_cast< CComObject< Base::_OwnerClass >* >(pv);
m_pOwner->AddRef();
}

还记得CTearOff1是从CComTearOffObjectBase继承的吗,这个基类包含了一个成员变量m_pOwner,现在它被赋值为指向它的外部对象的指针了。
7.现在终于把这个实现分割接口的组件创建了,剩下的在CTearOff1中查询ITearOff1的工作已经是重复劳动了,不再赘述。

执行pTear1->QueryInterface(ITearOff1, (void **)&pTear2)
一个实现分割接口的组件有可能包含多个分割接口,我们来检测一下它的查询过程。


函数堆栈二:

4..............
3.COuter::_InternalQueryInterface(...)
2.ATL::CComObject< COuter >::QueryInterface(...)
1.ATL::CComTearOffObject< CTearOff1 >::QueryInterface(...)

解释:

1:

STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{
return m_pOwner->QueryInterface(iid, ppvObject);
}

还记得我们创建的分割组件是CComTearOffObject< CTearOff1 >吗?现在执行查询操作的是它的成员函数。它的实现很简单,事实上它什么也没做,仅仅是把它交给它的外部对象(即CComObject< COuter >)去做了。还记得m_pOwner是在构造函数里赋值的吧。现在是否感到有些不妙呢?呵呵
2、3: 果然,现在已经不用再看下去了,剩下的将是重复我们在调用第一条查询操作所做的一切。这个过程很简单,但它也隐含说明了一点:若对一个实现分割接口的组件每查询一次它的接口,就会调用一个新的实例!!!在上例中,最后的结果pTear2和pTear1 是不一样的!!这显然是浪费!
在下一节中,我们将介绍一个可以解决这个问题的宏!

四.COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk) 参ATL例程COMMAP

这个宏与上一节所讲的COM_INTERFACE_ENTRY_TEAR_OFF宏最主要的不同就在于,当查询分割对象中其他接口时,不会再新建新的对象。下面还是先看看它的典型用法:


class CTearOff2:
public IDispatchImpl< ITearOff2, &IID_ITearOff2, &LIBID_COMMAPLib >,
public CComTearOffObjectBase< COuter >
{
public:
CTearOff2(){}
~CTearOff2(){}

BEGIN_COM_MAP(CTearOff2)
COM_INTERFACE_ENTRY(ITearOff2)
END_COM_MAP()

HRESULT STDMETHODCALLTYPE get_Name(BSTR* pbstrName)
{
*pbstrName = ::SysAllocString(L"ITearOff2");
return S_OK;
}
};

class COuter : public ....
{
public:
BEGIN_COM_MAP(COuter)
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ITearOff2, CTearOff2, m_pUnkTearOff2.p)
......
END_COM_MAP()

CComPtr< IUnknown > m_pUnkTearOff2;
.....
};

CTearOff2实现了分割接口ITearOff2,它的类定义与上一节所看见的CTearOff1一模一样可见不管是哪种分割接口,实现都是一样的,不同的地方在于COuter。在COuter中增加了一个成员变量m_pUnkTearOff2作为宏的一个参数。
我们继续用老办法跟踪它的内部执行过程,假设pOuter是已经获得的组件COuter有接口IOuter指针。

执行pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);

函数堆栈一:

9.CTearOff2::_InternalQueryInterface(...)
8.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...)(第二次调用)
9.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...)
8.ATL::CComCreator< ATL::CComCachedTearOffObject< CTearOff2 > >::CreateInstance()
7.ATL::CComObjectRootBase::_Cache(...)
6.COuter::_Cache(...)
5.ATL::AtlInternalQueryInterface(...)
4.ATL::CComObjectRootBase::InternalQueryInterface(...)
3,COuter::_InternalQueryInterface(...)
2.ATL::CComObject< COuter >::QueryInterface(...)
1.CTestDlg::OnButton1() line 187 + 22 bytes

解释:

1:pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);

2-5:这段代码见到很多次了,不用再讲了,现在程序执行到

HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw);
看来我们得看看这个宏的定义才能知道pFunc是执行的什么功能了。
#define COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk)\
{&iid,\
(DWORD)&_CComCacheData<\
CComCreator< CComCachedTearOffObject< x > >,\
(DWORD)offsetof(_ComMapClass, punk)\
>::data,\
_Cache},

与我们上一节见的宏的定义不太一样,还是先跟踪下去再说。
6:原来在BEGIN_COM_MAP中也定义了_Cache函数:

static HRESULT WINAPI _Cache(void* pv,REFIID iid,void** ppvObject,DWORD dw )\
{\
......
HRESULT hRes = CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);\
......
}\


7:看看CComObjectRootBase::_Cache的源码:

static HRESULT WINAPI _Cache(void* pv,REFIID iid,void** ppvObject,DWORD dw)
{
HRESULT hRes = E_NOINTERFACE;
_ATL_CACHEDATA* pcd = (_ATL_CACHEDATA*)dw;
IUnknown** pp = (IUnknown**)((DWORD)pv + pcd->dwOffsetVar);
if (*pp == NULL) hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp);
if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject);
return hRes;
}

现在问题的关键是dw了,dw是从pEntries->dw传过来的。我们得看一下宏定义中的
(DWORD)&_CComCacheData<\
CComCreator< CComCachedTearOffObject< x> >,\
(DWORD)offsetof(_ComMapClass, punk)\
>::data,\

是什么意思。
template < class Creator, DWORD dwVar >
_ATL_CACHEDATA _CComCacheData< Creator, dwVar >::data = {dwVar, Creator::CreateInstance};

CComCreator我们在前面已经见过它的定义了,它只有一个成员函数CreateInstance.
template < class contained >
class CComCachedTearOffObject :
public IUnknown,
public CComObjectRootEx< contained::_ThreadModel::ThreadModelNoCS >
{
public:
typedef contained _BaseClass;
CComCachedTearOffObject(void* pv)
: m_contained(((contained::_OwnerClass*)pv)->GetControllingUnknown())
{
m_contained.m_pOwner = reinterpret_cast< CComObject< contained::_OwnerClass >* >(pv);
}

CComContainedObject< contained > m_contained;
};

CComCachedTearOffObject是这个宏与上一节所讲宏不同的关键所在,因为它包含了一个CComContainedObject的对象。这个对象的作用在查询的时候再讲。
我们再来看看offsetof的定义:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
对(DWORD)offsetof(_ComMapClass, punk)来说,就是punk在_ComMapClass类中的偏移值。
现在来看看_ATL_CACHEDDATA是什么东西。
struct _ATL_CACHEDATA
{
DWORD dwOffsetVar;
_ATL_CREATORFUNC* pFunc;
};

typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv);

要注意的是从_Cached()函数传进来的参数dw就是_ATL_CACHEDATA结构的变量,所以可知道
(DWORD)pv + pcd->dwOffsetVar)得到的就是在类COuter中定义的m_pUnkTearOff2的偏移值,所以IUnknown** pp就是指向m_pUnkTearOff2的一个指向指针的指针。
而pdc->pFunc()则会调用CComCreator< CComCachedTearOffObject < x > >::CreateInstance
8:下面将调用CComCreator::CreateInstance,将创建一个CComCachedTearOffObject<>的对象实例。其构造函数定义如下:

CComCachedTearOffObject(void* pv)
: m_contained(((contained::_OwnerClass*)pv)->GetControllingUnknown())
{
ATLASSERT(m_contained.m_pOwner == NULL);
m_contained.m_pOwner = reinterpret_cast< CComObject< contained::_OwnerClass >* >(pv);
}


yintongshun 2003-12-02
  • 打赏
  • 举报
回复
关键的一句话就是IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw);
通过观察变量,正如我们所料pEntries->dw=4。(int)pThis+pEntries->dw)保证了我们可以得到IOuter2分支的虚函数表,又因为IDispatch也是从IUnknown继承,在虚函数表的最顶端放的是IUnknown的虚函数指针,所以进行(IUnknown *)强制转换,可以获得这个虚函数表的顶端地址,这正是我们所需要的。或许会问为什么得到的是虚函数表的地址,而不是一个类实例的地址呢?别忘了,接口是没有数据的,它只有纯虚函数。对于客户来说,它只能通过接口定义的虚函数来访问它,而不可能访问实现接口的类的成员变量,组件的数据对客户来说是不可见的,所以只用得到虚函数表的地址就行了。
三、COM_INTERFACE_ENTRY_TEAR_OFF(iid, x) 参考ATL例程Beeper、COMMAP

使用这个宏的目的就是为了把一些很少用到的接口放在一个单独的组件中实现,仅当查询到这个接口时,才创建这个组件,并且当它的引用计数减为0时就会被释放掉。我们知道ATL中组件是通过多重继承实现的,每继承一个接口,在为它分配的内存块中就会多一个虚函数表指针,用这个宏就可以为每个组件的实例节省下这一个虚函数表指针来(一个指针4个字节,好象也不多啊,呵呵)
下面我们来看它的典型用法:


class CTearOff1: //该类是专门用来实现分割接口ITearOff1的
public IDispatchImpl< ITearOff1, &IID_ITearOff1, &LIBID_COMMAPLib >,
public CComTearOffObjectBase //外部对象
{
public:
CTearOff1(){}
~CTearOff1(){}

BEGIN_COM_MAP(CTearOff1)
COM_INTERFACE_ENTRY(ITearOff1)
END_COM_MAP()

HRESULT STDMETHODCALLTYPE get_Name(BSTR* pbstrName)
{
*pbstrName = ::SysAllocString(L"ITearOff1");
return S_OK;
}
};

class COuter : public ..... //我们真正要实现的组件
{
public:
...........
BEGIN_COM_MAP(COuter)
...........
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ITearOff1, CTearOff1)
END_COM_MAP()
...........
};

CTearOff1实现了Tear-off接口ITearOff1,实现方法与其他组件并无不同。唯一不同的是它从CComTearOffObjectBase继承,CComTearOffObjectBase定义如下:

template < class Owner, class ThreadModel = CComObjectThreadModel >
class CComTearOffObjectBase : public CComObjectRootEx
{
public:
typedef Owner _OwnerClass;
CComObject* m_pOwner;
CComTearOffObjectBase() {m_pOwner = NULL;}
};

我们又看到了我们熟悉的一个类CComObject,它是组件的真正生成类。从上面的定义中可知道CComTearOffObjectBase主要功能就是包含了一个指向外部对象(在这里就是我们的组件类CComObject)的指针。它的功能将在后面看到。
我们继续用我们的老办法来跟踪一下看看它的执行过程。假设pOuter是我们已经获得的组件的IOuter接口指针。
执行pOuter->QueryInterface(IID_ITearOff1, (void **)&pTear1);
函数堆栈一:

7.CTearOff1::_InternalQueryInterface(...)
6.ATL::CComInternalCreator< ATL::CComTearOffObject< CTearOff1 > >::CreateInstance(...)
5.ATL::CComObjectRootBase::_Creator(...)
4.ATL::AtlInternalQueryInterface(...)
3.ATL::CComObjectRootBase::InternalQueryInterface(...)
2.COuter::_InternalQueryInterface(...)
1.ATL::CComObject< COuter >::QueryInterface(...)

解释:

1--4:这些代码已经遇到过很多次了,我们还是集中精力看看核心代码:

ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
//..........
while (pEntries->pFunc != NULL)
{
BOOL bBlind = (pEntries->piid == NULL);
if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid))
{
if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset
{
//若是简单接口,....
}
else //actual function call
{
HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw);
if (hRes == S_OK || (!bBlind && FAILED(hRes)))
return hRes;
}
}
pEntries++;
}
return E_NOINTERFACE;
}

当在COuter的接口映射数组中找到ITearOff1后,因为它不是一个简单接口,所以要执行pEntries->pFunc(....)。
我们先来看看COM_INTERFACE_ENTRY_TEAR_OFF的定义:

#define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)\
{&iid,\
(DWORD)&_CComCreatorData<\
CComInternalCreator< CComTearOffObject< x > >\
>::data,\
_Creator},

看不太明白,还是继续我们路由得了
5:原来_Creator是CComObjectRootBase的静态成员函数,它可是COuter的一个基类啊,所以才可以这样写而不会编译出错。看看它的实现吧:

static HRESULT WINAPI _Creator(void* pv, REFIID iid, void** ppvObject,DWORD)
{
_ATL_CREATORDATA* pcd = (_ATL_CREATORDATA*)dw;
return pcd->pFunc(pv, iid, ppvObject);
}

struct _ATL_CREATORDATA
{
_ATL_CREATORFUNC* pFunc;
};

typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv);

template < class Creator >
_ATL_CREATORDATA _CComCreatorData::data = {Creator::CreateInstance};

源代码都列出来了,不用我多说,大家也都能看懂了。继续路由吧
6:绕了一大圈,现在我们调用的应该是CComInternalCreator<...>::CreateInstance

template < class T1 >
class CComInternalCreator
{
public:
static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
{
ATLASSERT(*ppv == NULL);
HRESULT hRes = E_OUTOFMEMORY;
T1* p = NULL;
ATLTRY(p = new T1(pv))
if (p != NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if (hRes == S_OK) hRes = p->_InternalQueryInterface(riid, ppv);
if (hRes != S_OK) delete p;
}
return hRes;
}
};



yintongshun 2003-12-02
  • 打赏
  • 举报
回复
函数的逻辑很清楚,只有两点可能不太理解,一个是 (IUnknown*)((int)pThis+pEntries->dw)是什么意思,另一个是pEntries->pFunc到底 要干些什么事。前一个问题将在讲述COM_INTERFACE_ENTRY2中讲述,后一个问题将在以后讲述不同类型的接口时分别解释。饭总是要一口一口吃的嘛,呵呵。
现在我们只需关心一下我们的IMyObject是怎么被查找的。看一下它的宏
我们把COM_INTERFACE_ENTRY(IMyObject)解开以后形式为:

{&_ATL_IIDOF(IMyObject), //得到IMyObject的IID值
offsetofclass(IMyObject, CMyObject), //定义偏移量
_ATL_SIMPLEMAPENTRY},//表明是个简单接口

同样对于offsetofclass(IMyObject, CMyObject)我们也将留到下一次再讲。根据这个结构,我们很容易就能获得IMyObject接口指针。
0:OK,it is over.依次退栈返回。
其实这次查询发生的过程在刚才的调用序列中也发生了,当查询IClassFactory接口时就有类似的过程,但还是把它单独提了出来,只为了看看典型的情形,呵呵。

二、COM_INTERFACE_ENTRY2(x, x2) 参ATL例程:COMMAP

ATL中是以多重继承的方式来实现组件的,但在继承树中如果有多个分支实现了同一个接口,当查询这个接口时就需要知道把哪个分支返回给它。这个宏就是干这个工作的通常这个宏是用于IDispatch接口。我们先来看看它的典型用法:

class COuter :
public IDispatchImpl< IOuter1, &IID_IOuter1, &LIBID_COMMAPLib>,//IOuter1是一个双接口
public IDispatchImpl< IOuter2, &IID_IOuter2, &LIBID_COMMAPLib>,//IOuter2也是一个双接口
public ...
{
public:
COuter(){}
...
BEGIN_COM_MAP(COuter)
COM_INTERFACE_ENTRY2(IDispatch, IOuter2) ,//将暴露IOuter2所继承的路线 ,
COM_INTERFACE_ENTRY(IOuter1)
COM_INTERFACE_ENTRY(IOuter2)
...
END_COM_MAP
};

IDispatchImpl<...>这个类中实现了IDispatch接口,所以现在组件中有两个IDispatch 的实现。那查询IDispatch接口时,返回哪个实现呢?
我们再来看看COM_INTERFACE_ENTRY2(x, x2)的定义

#define BEGIN_COM_MAP(x) public: \
typedef x _ComMapClass; \
....................
#define COM_INTERFACE_ENTRY2(x, x2)\
{&_ATL_IIDOF(x),\ //得到接口的IID值
(DWORD)((x*)(x2*)((_ComMapClass*)8))-8,\
_ATL_SIMPLEMAPENTRY}, //表明是一个简单接口

现在问题就在于(DWORD)((x*)(x2*)((_ComMapClass*)8))-8是个什么意思?
我们先来考察一下下面一段代码:

class A1
{
public:
virtual void Test(){}
};


class A2 : public A1
{
public:
virtual void Test(){}
};

class A3 : public A1
{
public:
virtual void Test(){}
};

class A : public A2, public A3
{
};

{
DWORD dw; dw = (DWORD)((A *)8); //dw = 0x08
dw = (DWORD)((A3 *)(A *)8); //dw = 0x0c
dw = (DWORD)((A1 *)(A3 *)(A *)8); //dw = 0x0c
dw = (DWORD)((A1 *)(A3 *)(A *)8) - 8;//dw = 4
}

这个继承图是个典型的菱形结构,在类A中保存有两个虚函数表指针,分别代表着它的两个分支。当为类A申明一个对象并实例化时,系统会为其分配内存。在这块内存的最顶端保留着它的两个虚函数表指针。分析程序运行的结果,可以看出,最后的结果4代表了指向接口A3的虚函数表指针与类A对象的内存块顶端之间的偏移量。
下面我们再看一个更为复杂点的继承关系:

class B1
{
public:
virtual void Test(){}
};

class B2
{
public:
virtual void Test(){}
};

class B3
{
public:
public:
virtual void Test(){}
};

class B4 : public B1, public B2
{
public:
virtual void Test(){}
};

class B5 : public B2, public B3
{
public:
virtual void Test(){}
};

class B : public B4, public B5
{
};

{
DWORD dw; dw = (DWORD)((B *)8); //dw = 0x08
dw = (DWORD)((B5 *)(B *)8); //dw = 0x10
dw = (DWORD)((B2 *)(B5 *)(B *)8); //dw = 0x10
dw = (DWORD)((B2 *)(B5 *)(B *)8) - 8;//dw = 8
}

类B将保留四个虚函数表指针,因为它共有四个分支。我们的目的是想获得B::B5::B2这个分支中的B2接口,最后的结果8正是我们所需要的,它表示在类B内存块的偏移量。
从上面两个例子中,我们已经明白了(DWORD)((x*)(x2*)((_ComMapClass*)8))-8的作用通过这个值我们能获得我们所需要的接口。
下面我们针对我们的实际情况COM_INTERFACE_ENTRY2(IDispatch, IOuter2)来分析一下
IDispatchImpl< class T,... >模板类从类T中派生,所以COuter要从两个它的模板类中继承, IOuter1、IOuter2都是双接口,即都是从IDispatch派生的类,所以可得COuter有两条分支,也是个菱形结构,所以按照我们的示例,这个偏移值也应该是4。为了证明我们的设想,我们再来通过函数堆栈来验证我们的结果。

函数堆栈:

5.ATL::AtlInternalQueryInterface(...)
4.ATL::CComObjectRootBase::InternalQueryInterface(...)
3.CMyObject::_InternalQueryInterface(...)
2.ATL::CComObject< CMyObject >::QueryInterface(...)
1.pUnk->QueryInterface(IID_IDispatch, (void **)&pDispatch)

解释:

1:这是我们的验证代码,pUnk是组件的IUnknown指针

2--5:这些代码我们现在都已经很熟悉了,我们只需再看看AtlInternalQueryInterface 的具体实现。

ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
...........
while (pEntries->pFunc != NULL)
{
BOOL bBlind = (pEntries->piid == NULL);
if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid))
{
if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset
{
ATLASSERT(!bBlind);
IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
.....//如果是非简单接口的话...
}
pEntries++;
}
return E_NOINTERFACE;
}


yintongshun 2003-12-02
  • 打赏
  • 举报
回复
5:

CComCreator::CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
{
T1* p = NULL;
ATLTRY(p = new T1(pv))//创建类厂对象
if (p != NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if (hRes == S_OK)
hRes = p->QueryInterface(riid, ppv);
if (hRes != S_OK)
delete p;
}
}

注意这里的T1是CComObjectCached< ATL::CComClassFactory >,这是我们给CComCreator 的模板参数。我们又一次看到了我们熟悉的操作符'new'!直到现在我们终于创建了组件的类厂。但还没完,继续往下走,看看SetVoid(pv)里干了些什么?
void CComClassFactory::SetVoid(void* pv)
{
m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv;
}

大家还记得我们曾经把CMyObject::_CreatorClass::CreateInstance作为参数传给 pEntry->pfnGetClassObject(...)吧,当时我们不明白是怎么回事,现在已经豁然开朗!原来是类厂需要它来创建组件对象!虽然我们只是从字面意思猜出这一点,但实际上也正如我们所预料的那样,在CComClassFactory::CreateInstance(...)中,我们看到了m_pfnCreateInstance(pUnkOuter, riid, ppvObj);现在一切都已经明白了, ATL为我们创建类厂而作的层层包装我们都已经打开,剩下的创建组件的过程已经是我们很熟悉的过程了!
但是现在还没有完,我们还需要为类厂对象查询一个IUnknown指针,这个指针就存在我们在前面所看到的pEntry->pCF中。

6:

STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return _InternalQueryInterface(iid, ppvObject);}

现在调用的是CComObjectCached::QueryInterface,至于这个类有何特别之处,我们现在好象还不需要知道,我也很累的说,呵呵。
7:

HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject) \
{ return InternalQueryInterface(this, _GetEntries(), iid, ppvObject); }

所有的类的_InternalQueryInterface(...)都是在BEGIN_COM_MAP中定义的。 CComObjectCached没有BEGIN_COM_MAP宏,所以现在调用的是CComClassFactory的。注意把this指针和接口映射数组_GetEntries()传给了InternalQueryInterface(), 这是InternalQueryInterface(...)实现查询的依据。在BEGIN_COM_MAP(x)中定义了一个静态的接口映射数组:
_ATL_INTMAP_ENTRY _entries[];

每一个接口映射宏实际上都是向这个数组中增加了一项。一个接口映射宏包括三个部分:接口的IID号、偏移值(大部分时候下)、需要执行的函数,对一般接口来说不用执行其他函数。_GetEntries()就是返回这个数组。还有一些细节问题以后再说。
8:

static HRESULT WINAPI InternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
...
HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject);
...
}

现在调用的是CComObjectRootBase::InternalQueryInterface(...)
9:现在我们终于到了QueryInterface的鼻祖了。AtlInternalQueryInterface(...)是整个查询过程的终点,它遍历接口映射表,并根据每一项做出相应的动作。ATL中的消息映射宏有很多种,相应的动作也很多,但现在我们不管那些,现在我们要做的就是查到一个IUnknown接口,这很容易,我们甚至不需要遍历接口映射表。

ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
ATLASSERT(pEntries->pFunc == _ATL_SIMPLEMAPENTRY);
if (ppvObject == NULL)
return E_POINTER;

*ppvObject = NULL;
if (InlineIsEqualUnknown(iid)) // use first interface
{
IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
... //还有一大堆呢,但现在用不上,就节省点空间吧
}

这里有一个规定,接口映射表的第一个接口必须是_ATL_SIMPLEENTRY型的。至于为什么有这个要求,以及pThis+pEntries->dw是什么意思,我们以后再说吧,那也是一堆问题。总之,我们现在如愿以偿轻松的获得了我们所需要的类厂对象以及IUnknown指针。
4:我差一点以为我们可以胜得返回到第一步了,但在ATL::AtlModuleGetClassObject 处却又停了下来,看看它的源码,原来还要再通过我们刚获得的IUnknown指针查询 IClassFactory指针。又是一通相同的调用,从第6步到第9步一模一样,我们将进行相同的调用。但注意在第9步中,我们这回查的不再是IUnknown指针了,所以我们需要看看我刚才还没列出的代码,但这留到下一次函数堆栈再看吧

1:终于终于我们已经完成了创建类厂对象的全部操作,现在我们要做的就是我们熟悉的调用类厂对象的CreateInstance(...)函数创建组件的过程了。正如我们所见到的,现在OLE开始调用CComClassFactory::CreateInstance()了,我们还没忘记,在类厂对象中保留了创建组件用的CreateInstance()函数, 这个过程已经很明朗了。

2.不用再重复了吧,看第4步。

3.不用再重复了吧,看第4步。

4.如果继续路由下去的话,我们的堆栈还可以很长,但这只是重复的枯躁的劳动。我就不继续走下去了,我也很累的说,唉。

函数调用堆栈二:

0:............
5.ATL::AtlInternalQueryInterface(...)
4.ATL::CComObjectRootBase::InternalQueryInterface(...)
3.CMyObject::_InternalQueryInterface(...)
2.ATL::CComObject< CMyObject >::QueryInterface(...)
1.pUnk->QueryInterface(IID_IMyObject, (void **)&pMyObject);(客户端)

解释如下:
1.我们通过刚刚获得的组件对象的IUnknown接口指针来查询IMyObject指针,这才是我们真正需要的指针。

2.还记得我们说过ATL真正创建的组件并不是CMyObject,而是CComObject,CComAggObject 或CComPolyObject,这里我们创建的是CComObject.所以理所当然我们要调用 CComObject::QueryInterface(...),而确实CComObject也实现了这个函数。

STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return _InternalQueryInterface(iid, ppvObject);}
它只是简单地调用_InternalQueryInterface(...),我们也说过,只有类里面申明了BEGIN_COM_MAP宏才会有_InternalQueryInterface(...),所以现在执行转到了它的父类CMyObject中去,所以将调用CMyObject::_InterfaceQueryInterface(...)
3.以后的调用我们已经很熟悉了,还用我再说一遍吗,呵呵

4.这个调用我们也很熟悉了,不用多说了吧

5.现在我们将要查询的是一个非IUnknown接口,所以我们来看看我们以前没列出的代码

ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis,
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
//确保接口映射的第一项是个简单接口
//若是查询IUnknown接口,执行相应的操作
//以下将遍历接口映射表,试图找到相应的接口
while (pEntries->pFunc != NULL)
{
BOOL bBlind = (pEntries->piid == NULL);
if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid))
{
//_ATL_SIMPLEMAPENTRY就表明是个简单接口
if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset
{
ATLASSERT(!bBlind);
IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
else //如果不是一个简单接口,则需要执行相应的函数
{
HRESULT hRes=pEntries->pFunc(pThis,iid,ppvObject,pEntries->dw);
if (hRes == S_OK || (!bBlind && FAILED(hRes)))
return hRes;
}
}
pEntries++;
}
return E_NOINTERFACE;
}
}


yintongshun 2003-12-02
  • 打赏
  • 举报
回复
ATL接口映射宏详解
序言:

这几天看了看ATL的接口映射宏,不知不觉看得比较深入了,突然就萌发了把它写出来的想法。ATL中定义了很多接口映射宏,有几个还是比较重要的,虽然好象没有必要把它所有的细节都弄得很清楚,但深入学习的过程中也可以顺带学一学其他的ATL类,对它的机制也可以更清楚一些,应该还是会有些好处的吧。我按照我学习的过程把它写出来,也 不知道大家能不能看懂。想模仿一下侯老师的手笔力争把其内部细节解释清楚,但也不敢大言不惭的美其名曰“深入浅出”,呵呵,只希望能对大家有所帮助了。

以后将分别介绍ATL中各个形式为COM_INTERFACE_ENTRY_XX的接口映射宏并将按照从易到难的顺序讲解,每一部分都将建立在前一部分的基础上。每一部分都将通过分析实际的调用函数堆栈来进行分析,堆栈的写法是从下向上。文中所涉及的代码都为略写,只列出相关部分。

一、COM_INTERFACE_ENTRY(x)

首先我们从一个最典型的应用开始:

定义一个最简单的ATL DLL:

class ATL_NO_VTABLE CMyObject :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl
{
.....
BEGIN_COM_MAP(CMyObject)
COM_INTERFACE_ENTRY(IMyObject) //一个双接口
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
.....
};

编写一段最简单的查询接口代码:

IUnknown *pUnk;
IMyObject *pMyObject;
CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
pUnk->QueryInterface(IID_IMyObject, (void **)&pMyObject);

执行客户代码,首先我们看看组件对象是如何被创建的。
函数调用堆栈一:

4...........
3.ATL::CComCreator< ATL::CComObject< CMyObject > >::CreateInstance(...)
2.ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< CMyObject > >,
ATL::CComCreator< ATL::CComAggObject< CMyObject > > >::CreateInstance(...)
1.ATL::CComClassFactory::CreateInstance(...)
4.ATL::AtlModuleGetClassObject(...)
9.ATL::AtlInternalQueryInterface(...)
8.ATL::CComObjectRootBase::InternalQueryInterface(...)
7.ATL::CComClassFactory::_InternalQueryInterface(...)
6.ATL::CComObjectCached::QueryInterface(...)
5.ATL::CComCreator >::
CreateInstance(...)
4.ATL::AtlModuleGetClassObject(...)
3.ATL::CComModule::GetClassObject(...)
2.DllGetClassObject(...)
1.CoCreateInstance(...)(客户端)

解释如下:

1:

CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
其内部将调用OLE API函数CoGetClassObject(), 而CoGetClassObject则会通过 LoadLibrary(...)装入DLL,并调用DLL中的DllGetClassObject()函数。

2:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{

return _Module.GetClassObject(rclsid, riid, ppv);
}

其中值得注意的是_Module变量,在DLL中定义了全局变量:
CComModule _Module;
ATL通过一组宏:

BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_MyObject, CMyObject)
END_OBJECT_MAP()

#define BEGIN_OBJECT_MAP(x) static _ATL_OBJMAP_ENTRY x[] = {
#define OBJECT_ENTRY(clsid, class) {&clsid, class::UpdateRegistry, \
class::_ClassFactoryCreatorClass::CreateInstance, \//关键
class::_CreatorClass::CreateInstance, \
NULL, 0, class::GetObjectDescription, \
class::GetCategoryMap, class::ObjectMain },
#define END_OBJECT_MAP() {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}};

生成一个静态全局_ATL_OBJMAP_ENTRY型数组:ObjectMap[];
然后ATL又在


BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/

{
.....
_Module.Init(ObjectMap, hInstance, &LIBID_TEST2Lib);
.....
}

中初始化_Module //注意在有的情况下是在InitInstance()中初始化_Module


那么_Module初始化都做了些什么呢,其实他什么也没做,在CComModule::Init中,它调用AtlModuleInit(_ATL_MODULE* pM, _ATL_OBJMAP_ENTRY* p, HINSTANCE h),在其中关键的只有一句:pM->m_pObjMap = p;可见_Module仅仅是把这个全局对象映射数组 ObjectMap[]给存了起来。那么为什么可以通过_Module.GetClassObject得到类厂呢?其实关键在于我们的组件CMyObject继承的又一个基类CComCoClass! 在CComCoClass中缺省定义了一个宏DECLARE_CLASSFACTORY()而

#define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(CComClassFactory)
#define DECLARE_CLASSFACTORY_EX(cf)
typedef CComCreator< ccomobjectcached< cf > > _ClassFactoryCreatorClass;

CComCreator,CComObjectCached我们暂且不管,但一看到CComClassFactory,顾名思义,我们就知道我们要的类厂终于出现了!每个组件内部原来都有一个类厂对象。绕了一大圈,我们现在已经知道了_Module中包含了我们所要的每个组件的类厂对象,这对目前来说已经足够了,现在继续路由下去!
3:

HRESULT CComModule::GetClassObject(REFCLSID rclsid,REFIID riid,LPVOID* ppv)
{
return AtlModuleGetClassObject(this, rclsid, riid, ppv);
}

CComModule::GetClassObject的实现非常简单,仅仅是调用ATL的API函数。

4:

ATLINLINE ATLAPI AtlModuleGetClassObject(_ATL_MODULE* pM, REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
_ATL_OBJMAP_ENTRY* pEntry = pM->m_pObjMap;//从_Module中取出对象映射数组

while (pEntry->pclsid != NULL)
{
if ((pEntry->pfnGetClassObject != NULL) && InlineIsEqualGUID(rclsid, *pEntry->pclsid))
{
if (pEntry->pCF == NULL)
{
hRes = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance,
IID_IUnknown, (LPVOID*)&pEntry->pCF);
}
if (pEntry->pCF != NULL)
hRes = pEntry->pCF->QueryInterface(riid, ppv);
break;
}
pEntry = _NextObjectMapEntry(pM, pEntry);
}
}

现在好象已经有点看不懂了,看来我们得看看_ATL_OBJMAP_ENTRY的结构了
struct _ATL_OBJMAP_ENTRY
{
const CLSID* pclsid;
HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister);
_ATL_CREATORFUNC* pfnGetClassObject;
_ATL_CREATORFUNC* pfnCreateInstance;
IUnknown* pCF;
DWORD dwRegister;
_ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;
_ATL_CATMAPFUNC* pfnGetCategoryMap;
}

pclsid很清楚就代表着我们组件的CLSID;pfnGetClassObject我们也已经知道了它就是CMyObject::_ClassFactoryCreatorClass::CreateInstance(我们组件所包含的类厂对象的CreateInstance函数);pCF我们也可以猜出它是指向这个类厂的IUnknown指针,代表这个类厂对象是否被创建过,若类厂对象已经存在,就不用再创建新的类厂对象了。现在就剩下pfnCreateInstance我们还不明白怎么回事。其实答案还是在 CComCoClass中!
在CComCoClass中缺省定义了宏DECLARE_AGGREGATABLE(x),这个宏表示这个组件既可以是聚集的也可以是非聚集的,关于聚集的概念我们暂且不理,先看它的定义:

#define DECLARE_AGGREGATABLE(x) public:\
typedef CComCreator2< ccomcreator< CComObject< x > >, \
CComCreator< ccomaggobject< x > > > _CreatorClass;

我们看到了一个熟悉的字符串_CreatorClass, 原来这还有一个组件包含的对象。但还有一个问题我们没有搞清楚,就是为什么_ClassFactoryCreator和_CreatorClass后面都要跟着一个CreateInstance? 看来我们必须先来看看CComCreator是个什么东西了。
template < class T1 >
class CComCreator
{
public:
static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
{.....
}
};

原来它里面只有一个CreateInstance函数,我们现在终于大体明白_ClassFactoryCreatorClass::CreateInstance 表示什么意思了,它就代表CComClassFactory::CreateInstance(..)吧,差不多就是这样了。那我们再来看看CComCreator2有什么不同:
template < class T1, class T2 >
class CComCreator2
{
public:
static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
{
return (pv == NULL) ?
T1::CreateInstance(NULL, riid, ppv) :
T2::CreateInstance(pv, riid, ppv);
}
};

这个类与CComCreator很类似,都只有一个CreateInstance成员函数,从_CreatorClass 中我们可以知道它实际上包含两个类CComObject,CComAggObject的CreateInstance函数(通过CComCreator),其中CComObject用于非聚集对象,CComAggObject用于聚集对象根据情况它建立相应的对象。(ATL中实际生成的组件对象不是CMyObject,而是 CComObject,CComAggObject或CComPolyObject对象,这个概念很重要,但现在暂且不谈) 现在我们对AtlModuleGetClassObject(...)基本已经知道是怎么回事了,它就是根据存在对象映射数组中的创建类厂的函数的地址来创建类厂。pfnGetClassObject以及 pfnCreateInstance我们基本上都已经知道是怎么回事了,但还有一个问题为什么要把pEntry->pfnCreateInstance作为pEntry->pfnGetClassObject(...)中的一个参数传递?答案在下面呢,让我们继续路由下去!

yintongshun 2003-12-02
  • 打赏
  • 举报
回复
深入MDI客户窗口编程

在使用VC6.0/5.0的AppWizard生成MDI应用的时候,我们发现MDI主窗口的客
户区背景千篇一律的是深灰的。VC6.0/5.0并没有提供修改其背景色的方法。甚
至使用SDK编程也没有好的方法修改背景色。以至于微软的产品如Office也是灰蒙
蒙的背景。那么,有没有办法将背景设置为自己喜欢的颜色呢?
笔者在学习过程中摸索出一套随意改变客户区窗口颜色的方法。利用这套方法,
可以将客户区窗口设为256色背景甚至设为BITMAP位图以至于动画等等。大大地增
强了程序的多媒体效果。

先介绍对MDI客户窗口编程的基本原理。
一、MDI客户窗口
一个MDI应用的主框架窗口包含一个特殊的子窗口称为MDICLIENT窗口。
MDICLIENT窗口负责管理主框架窗口的客户区。MDICLIENT窗口本身有自己的
子窗口即由CMDIChildWnd派生的文档窗口,也就是MDI子窗口。MDI主框架窗口
负责管理MDICLIENT子窗口。当控制条(菜单条,状态条等)发生变化时,MDI主
框架窗口重新配置MDICLIENT窗口。MDICLIENT子窗口负责管理全部的MDI子窗口。
父窗口负责将某些命令传递到子窗口。因此,消息队列发向MDI子窗口的消息
由MDICLIENT窗口负责传递,发向MDICLIENT窗口和MDI子窗口的消息由主框架窗
口负责传递。这样,我们可以在主框架窗口截获关于MDICLIENT窗口的重画消息
然后加入自己设计的代码。
二、MDI客户窗口编程方法
对MDI客户窗口编程有一定的难度。原因是MDIFrameWnd的客户区完全被
MDICLIENT窗口覆盖掉了。这样,MDI主窗口类MDIFrameWnd的背景色和光标都不
起作用。同时,微软并不支持将MDICLIENT窗口作为子类,MDICLIENT窗口只能使
用标准的背景色和光标。所以,对MDI客户窗口编程不能象对普通窗口那样简单
地重载WM_PAINT的消息处理函数。
改变MDI客户窗口背景的方法有两种。
使用CMDIFrameWnd::CreateClient 函数:
CreateClient( LPCREATESTRUCT lpCreateStruct, CMenu* pWindowMenu );
参数lpCreateStruct是指向CREATESTRUCT 结构的指针。在CREATESTRUCT 结构
中lpszClass项指向窗口类WNDCLASS结构。通过改变WNDCLASS结构中的
HbrBackground项和hCursor项可以更改MDICLIENT窗口的背景刷和光标。由于该
函数创建新的MDICLIENT窗口对象,必须在重载的主窗口的OnCreate成员函数中
调用。该方法比较复杂,必须手动创建MDI客户窗口,不能利用AppWizard自动
提供的功能。而且,只能使用Windows95有限的16色背景刷。本文采用第二种方法。
在主框架窗口的消息队列中截获发向MDI客户窗口的WM_PAINT消息并向主框架窗
口发送一条标志消息。在这条标志消息的处理函数中对MDI客户窗口进行操作。
该方法比较简捷,但有几点值得注意的地方。
首先,如何截获MDI客户窗口WM_PAINT消息。MFC提供
了PreTranslateMessage(MSG* pMsg) 函数。它在消息发送到TranslateMessage
和DispatchMessage 函数以前预先解释消息。可以重载该函数截获MDI客户窗
口WM_PAINT消息:
BOOL PreTranslateMessage(MSG* pMsg)
{
if(pMsg->hwnd==m_hWndMDIClient && pMsg->message==WM_PAINT)
PostMessage(WM_PAINT);
return CMDIFrameWnd::PreTranslateMessage(pMsg);
}
其次,为简单起见,这里将标志消息设为MDI主窗口的WM_PAINT。在MDI主窗
口WM_PAINT的消息处理函数中增加重画MDI客户窗口的代码。读者也可以自定义
消息,不过麻烦一点。
最后,由于对MDI客户窗口的操作都是在主窗口完成的。如何在主窗口中获得
MDI客户窗口的设备描述表呢。其实,在MDI主窗口类中有MDI客户窗口成员
m_hWndMDIClient。按如下方法得到客户窗口的设备描述表
dc.m_hDC=::GetDC(this->m_hWndMDIClient);
然后就可以对客户窗口进行操作了。
三、实例
将客户窗口设为256色背景。
使用AppWizard生成MDI应用TEST。
在TEST.CPP中的函数,增加如下代码:
BOOL CTestApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
LoadStdProfileSettings(); // Load standard INI file options (including MRU)
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_TESTTYPE,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CChildFrame), // custom MDI child frame
RUNTIME_CLASS(CTestView));
AddDocTemplate(pDocTemplate);
// create main MDI Frame window
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The main window has been initialized, so show and update it.
pMainFrame->ShowWindow(m_nCmdShow);
//******增加代码头******
AfxGetMainWnd()->PostMessage(WM_PAINT);
//******增加代码尾******
pMainFrame->UpdateWindow();
return TRUE;
}
保证程序一开始就更新客户窗口。
使用ClassWard在CmainFrame类中加入PreTranslateMessage消息处理函数入如下:
BOOL PreTranslateMessage(MSG* pMsg)
{ //******增加代码头******
if(pMsg->hwnd==m_hWndMDIClient && pMsg->message==WM_PAINT)
PostMessage(WM_PAINT);
//******增加代码尾******
return CMDIFrameWnd::PreTranslateMessage(pMsg);
}
在主窗口的WMPAINT消息处理函数中加入:
void CMainFrame::OnPaint()
{
//******增加代码头******
CMDIFrameWnd::OnPaint();
CRect rc;
CDC dc;
dc.m_hDC=::GetDC(this->m_hWndMDIClient);
CBrush br(RGB(120,200,40));//256色刷子
dc.SelectObject(&br);
dc.GetClientRect(&rc);
dc.PatBlt(rc.left,rc.top,rc.Width(),rc.Height(),PATCOPY);
ReleaseDC(&dc);
//******增加代码尾******
}
将客户窗口设为Bitmap位图。
1,2,3同例一。
4.在资源中加入自己喜欢的位图并设为IDB_BITMAP1。在主窗口类加入Cbitmap
类成员m_bmp。并在CMDIFrameWnd::OnCreate()函数末尾初始化:
m_bmp.LoadBitmap(IDB_BITMAP1);
5.在主窗口的WM_PAINT消息处理函数中加入:
void CMainFrame::OnPaint()
{
//******增加代码头******
CMDIFrameWnd::OnPaint();
CRect rc,memrc;
CDC dc,memdc;
dc.m_hDC=::GetDC(this->m_hWndMDIClient);
memdc.CreateCompatibleDC(&dc);
memdc.SelectObject(&m_bmp);
GetClientRect(&rc) ;
dc.BitBlt(rc.top,rc.left,rc.Width(),rc.Height()
,&memdc,rc.top,rc.left,SRCCOPY);
ReleaseDC(&memdc);
ReleaseDC(&dc);
//******增加代码尾******
}

yintongshun 2003-12-02
  • 打赏
  • 举报
回复
DCOM是以RPC为基础的,要在网络间传递数据必须遵守标准的网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调度,这个过程很复杂,不过Windows已经把一切都给我们做好了,一般情况下我们不需要自己来编写调度DLL。
  我们刚说过一个COM组件必须在一个进程内。对于本地模式的组件一般是以EXE的形式出现,所以它本身就已经是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了调度代码以实现基本的调度。这个进程就是dllhost.exe。这是COM默认的DLL代理。实际上在分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着很强大的功能,是专门的用于管理分布式DLL组件的工具。
  调度离我们很近又似乎很远,我们编程时很少关注到它,这也是COM的一个优点之一,既平台无关性,无论你是远程的、本地的还是进程内的,编程是一样的,一切细节都由COM自己处理好了,所以我们也不用深究这个问题,只要有个概念就可以了,当然如果你对调度有自己特殊的要求就需要深入了解调度的整个过程了,这里推荐一本《COM+技术内幕》,这绝对是一本讲调度的好书。
  (5) COM组件的核心是IDL。
  我们希望软件是一块块拼装出来的,但不可能是没有规定的胡乱拼接,总是要遵守一定的标准,各个模块之间如何才能亲密无间的合作,必须要事先共同制订好它们之间交互的规范,这个规范就是接口。我们知道接口实际上都是纯虚类,它里面定义好了很多的纯虚函数,等着某个组件去实现它,这个接口就是两个完全不相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,我们没有时间自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们需要的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂商已经在自己的组件中实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组件,我们不关心组件从哪来,它有什么其它的功能,我们只关心它是否很好的实现了我们制订好的接口。这种接口可能是业界的标准,也可能只是你和几个厂商之间内部制订的协议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,是COM组件通信的标准。
  COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL开始,先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,有的人可能善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你最后把DLL拿给我。这是一种多么好的开发模式,可以用任何语言来开发,也可以用任何语言也欣赏你的开发成果。
  (6) COM组件的运行机制,即COM是怎么跑起来的。
  这部分我们将构造一个创建COM组件的最小框架结构,然后看一看其内部处理流程是怎样的

    IUnknown *pUnk=NULL;
    IObject *pObject=NULL;
    CoInitialize(NULL);
    CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown,
            (void**)&pUnk);
    pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
    pUnk->Release();
    pObject->Func();
    pObject->Release();
    CoUninitialize();
  CoCreateInstance身上,让我们来看看它内部做了一些什么事情。以下是它内部实现的一个伪代码:
    CoCreateInstance(....)
    {
      .......
      IClassFactory *pClassFactory=NULL;
      CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory,
            (void **)&pClassFactory);
      pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
      pClassFactory->Release();
      ........
    }

  这段话的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。
  继续深入一步,看看CoGetClassObject的内部伪码:
    CoGetClassObject(.....)
    {
      //通过查注册表CLSID_Object,得知组件DLL的位置、文件名
      //装入DLL库
      //使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。
      //调用DllGetClassObject
    }
  DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.
  下面是DllGetClassObject的伪码:
   DllGetClassObject(...)
   {
      ......
      CFactory* pFactory= new CFactory; //类厂对象
      pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
      //查询IClassFactory指针
      pFactory->Release();
      ......
   }
CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码:
   CFactory::CreateInstance(.....)
   {
      ...........
      CObject *pObject = new CObject; //组件对象
      pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
      pObject->Release();
      ...........
   }
  上图是从COM+技术内幕中COPY来的一个例图,从图中可以清楚的看到CoCreateInstance的整个流程。

(7) 一个典型的自注册的COM DLL所必有的四个函数
  DllGetClassObject:用于获得类厂指针
  DllRegisterServer:注册一些必要的信息到注册表中
  DllUnregisterServer:卸载注册信息
  DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载DLL
  DLL还有一个函数是DllMain,这个函数在COM中并不要求一定要实现它,但是在VC生成的组件中自动都包含了它,它的作用主要是得到一个全局的实例对象。
(8) 注册表在COM中的重要作用
  首先要知道GUID的概念,COM中所有的类、接口、类型库都用GUID来唯一标识,GUID是一个128位的字串,根据特制算法生成的GUID可以保证是全世界唯一的。
  COM组件的创建,查询接口都是通过注册表进行的。有了注册表,应用程序就不需要知道组件的DLL文件名、位置,只需要根据CLSID查就可以了。当版本升级的时侯,只要改一下注册表信息就可以神不知鬼不觉的转到新版本的DLL。
  本文是本人一时兴起的涂鸭之作,讲得并不是很全面,还有很多有用的体会没写出来,以后如果有时间有兴趣再写出来。希望这篇文章能给大家带来一点用处,那我一晚上的辛苦就没有白费了。-:)

yintongshun 2003-12-02
  • 打赏
  • 举报
回复
用VC进行COM编程所必须掌握的理论知识2

(2) COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatchCOM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。
  另一方面,一个组件也不是只给你一个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。
  引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各种的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。
  IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。
  IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平常是怎么实例化一个类对象的?是用‘new’命令!很简单吧,COM组件也一样如此。但是谁来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果客户知道组件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表着组件的128位的数字串而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一下,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是类厂。
  每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操作都交给类厂对象去做了。
  IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则是太少太少了。
  IDispatch叫做调度接口。它的作用何在呢?这个世上除了C++还有很多别的语言,比如VB、VJ、VBScript、JavaScript等等。可以这么说,如果这世上没有这么多乱七八糟的语言,那就不会有IDispatch。:-) 我们知道COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如VBScript、JavaScript。不行的原因在于它们并不支持指针,连指针都不能用还怎么用多态性啊,还怎么调这些虚函数啊。唉,没办法,也不能置这些脚本语言于不顾吧,现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,我们只能另寻他法了。时势造英雄,IDispatch应运而生。:-)
  调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,当给一个编号就能让别人知道怎么调用一个函数那不是天方夜潭吗,你总得让别人知道你要调用的函数要带什么参数,参数类型什么以及返回什东西吧,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看一看MS的类库里实现Invoke的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事,而且可能永远也没这样的机会。:-)
  (3) dispinterface接口、Dual接口以及Custom接口
  这一小节放在这里似乎不太合适,因为这是在ATL编程时用到的术语。我在这里主要是想谈一下自动化接口的好处及缺点,用这三个术语来解释可能会更好一些,而且以后迟早会遇上它们,我将以一种通俗的方式来解释它们,可能并非那么精确,就好象用伪代码来描述算法一样。-:)
  所谓的自动化接口就是用IDispatch实现的接口。我们已经讲解过IDispatch的作用了,它的好处就是脚本语言象VBScript、 JavaScript也能用COM组件了,从而基本上做到了与语言无关它的缺点主要有两个,第一个就是速度慢效率低。这是显而易见的,通过虚函数表一下子就可以调用函数了,而通过Invoke则等于中间转了道手续,尤其是需要把函数参数转换成一种规范的格式才去调用函数,耽误了很多时间。所以一般若非是迫不得已我们都想用VTable的方式调用函数以获得高效率。
  第二个缺点就是只能使用规定好的所谓的自动化数据类型。如果不用IDispatch我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度代码。而用自动化接口就不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预料到我们要用到的所有类型,它只能根据一些常用的数据类型来写它的处理代码,而且它也要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成的调度代码只适用于它所规定好的那些数据类型,当然这些数据类型已经足够丰富了,但不能满足自定义数据结构的要求。你也可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。
  考虑到IDispatch的种种缺点(它还有一个缺点,就是使用麻烦,现在一般都推荐写双接口组件,称为dual接口,实际上就是从IDispatch继承的接口。我们知道任何接口都必须从IUnknown继承,IDispatch接口也不例外。那从IDispatch继承的接口实际上就等于有两个基类,一个是IUnknown,一个是IDispatch,所以它可以以两种方式来调用组件,可以通过IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用这就有了很大的灵活性,这个组件既可以用于C++的环境也可以用于脚本语言中,同时满足了各方面的需要。
  相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口(虽然它实际上不是的),这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。
  Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了
  (4) COM组件有三种,进程内、本地、远程。对于后两者情况必须调度接口指针及函数参数。
  COM是一个DLL,它有三种运行模式。它可以是进程内的,即和调用者在同一个进程内,也可以和调用者在同一个机器上但在不同的进程内,还可以根本就和调用者在两台机器上。
  这里有一个根本点需要牢记,就是COM组件它只是一个DLL,它自己是运行不起来的,必须有一个进程象父亲般照顾它才行,即COM组件必须在一个进程内.那谁充当看护人的责任呢?
  先说说调度的问题。调度是个复杂的问题,以我的知识还讲不清楚这个问题,我只是一般性的谈谈几个最基本的概念。我们知道对于WIN32程序,每个进程都拥有4GB的虚拟地址空间,每个进程都有其各自的编址,同一个数据块在不同的进程里的编址很可能就是不一样的,所以存在着进程间的地址转换问题。这就是调度问题。对于本地和远程进程来说,DLL和客户程序在不同的编址空间,所以要传递接口指针到客户程序必须要经过调度。Windows经提供了现成的调度函数,就不需要我们自己来做这个复杂的事情了。对远程组件来说函数的参数传递是另外一种调度。

16,472

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC相关问题讨论
社区管理员
  • 基础类社区
  • Web++
  • encoderlee
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

        VC/MFC社区版块或许是CSDN最“古老”的版块了,记忆之中,与CSDN的年龄几乎差不多。随着时间的推移,MFC技术渐渐的偏离了开发主流,若干年之后的今天,当我们面对着微软的这个经典之笔,内心充满着敬意,那些曾经的记忆,可以说代表着二十年前曾经的辉煌……
        向经典致敬,或许是老一代程序员内心里面难以释怀的感受。互联网大行其道的今天,我们期待着MFC技术能够恢复其曾经的辉煌,或许这个期待会永远成为一种“梦想”,或许一切皆有可能……
        我们希望这个版块可以很好的适配Web时代,期待更好的互联网技术能够使得MFC技术框架得以重现活力,……

试试用AI创作助手写篇文章吧