http://www.vckbase.com/index.php/wv/60

问题:

我用MFC编写COM程序有一段时间了,知道如何使用宏和嵌套类,以及如何在嵌套类中处理IUnknown接口,但对IUnknown的使用还是不太老练。

假设CMyClass是一个COM服务器,从CCmdTarget派生。它实现了IMyInterface。CMyClass的定义如下:

class CMyClass: public CCmdTarget
{
BEGIN_INTERFACE_PART(...)
STDMETHOD....
END_INTERFACE_PART
DECLARE_INTERFACE_MAP
} ;

因为CCmdTarget没有QueryInterface方法,CMyClass也没有这个方法。我的问题是:这个COM服务器的客户端总是要调用QueryInterface,它不调用ExternalQueryInterface, 也不调用 InternalQueryInterface。因此如果QueryInterface不是一个CMyClass的有效方法。这种情况该怎么处理?

解答:

关于QueryInterface的这个问题,除你之外的许多人都对它感到困惑。但是不用怕,读完本文就可以见分晓。导致困惑的一个潜在的问题是CCmdTarget本身不从CObject派生,并且两者都有不同于QueryInterface的虚函数——例如,CObject中的第一个虚函数是GetRuntimeClass,CCmdTarget中的第一个虚函数是OnCmdMsg。所以从CCmdTarget派生出来的类怎么可能是COM类呢?而且还必须实现头三个虚函数是AddRef, Release, 和 QueryInterface的IUnknown接口:

实际上,解开这个谜团的方法很简单,但要求深入了解MFC类库。为此,我们必须进入MFC考察一番,主要目的是看看在调用CoCreateInstance创建类实例的时候会发生一些什么事情。

首先,CoCreateInstance所做的第一件事情(或多或少)是到注册表中检查哪个DLL实现你的类。为简单起见,假设使用的是进程内服务器。CoCreateInstance加载你的DLL并调用特殊函数DllGetClassObject.。

// DLL 函数创建COM对象类工厂
DllGetClassObject(REFCLSID rclsid, // 类 ID
REFIID riid, // 接口 ID
LPVOID* ppv) // 返回的接口指针

实际上,DllGetClassObject得不到你的类实例,它得到的是类工厂(IClassFactory)实例,通过这个类工厂来创建你得类实例。很怪是不是?MFC提供DllGetClassObject的实现,任务是搜索DLL中的所有类工厂,看看哪个类的ID寓所请求的类ID匹配。MFC是如何知道搜索哪个类工厂呢?当你在编写子记得类代码时,用宏DECLARE_OLECREATE 和 IMPLEMENT_OLECREATE声明并实现你自己这个类的类工厂。尤其是IMPLEMENT_OLECREATE声明一个COleObjectFactory的静态实例,这个对象将自己(通过调用类构造函数    COleObjectFactory::COleObjectFactory)添加到与模块或DLL关联的某个类工厂清单中。结果,给定某个类的ID,DllGetClassObject就知道如何发现与那个类关联的类工厂。

一旦CoCreateInstance有了某个类工厂,它便调用IClassFactory::CreateInstance。在此MFC的COleObjectFactory又提供缺省的实现。

//做了许多精简后
STDMETHODIMP COleObjectFactory::XClassFactory::CreateInstance(...)
{
METHOD_PROLOGUE_EX(COleObjectFactory,
ClassFactory)
……
// 这里省略了许多代码 CCmdTarget* pTarget = pThis->OnCreateObject();
return pTarget->InternalQueryInterface(&riid, ppvObject);
}

这里省略了许多琐碎代码以便突出重点,这些代码包括:创建一个实例和调用InternalQueryInterface。OnCreateObject是个COleObjectFactory的虚拟函数,它通过MFC的运行时类创建你的类实例:

//同样也作了简化
CCmdTarget* COleObjectFactory::OnCreateObject()
{
return (CCmdTarget*)m_pRuntimeClass->CreateObject();
}

一旦COleObjectFactory::XClassFactory::CreateInstance有了你的类实例,便调用InternalQueryInterface获取类的IUnknown接口指针。这里省略了许多细节,例如,在实际代码中MFC要调用IClassFactory2::CreateInstanceLic并检查所有出错条件以及聚合。InternalQueryInterface 和 ExternalQueryInterface之间的差别是外部版本委派外部IUnknown(如果有的话),而内部版本则不然。但即使聚合,最终都要归到InternalQueryInterface。

如果还不明白,不要紧。类工厂的CreateInstance函数创建了一个你的类实例,并调用CCmdTarget::InternalQueryInterface来获取类的IUnknown指针。InternalQueryInterface在哪里得到你的类IUnknown接口指针呢?难以捉摸的QueryInterface又在哪里?

经历了一些步骤和函数调用之后,CCmdTarget::InternalQueryInterface最终要调用一个函数:

LPUNKNOWN CCmdTarget::GetInterface(const void* iid)
{
LPUNKNOWN ptr = NULL;
if (iid == IID_IUnknown) {
ptr = // 接口映射的第一个接口
} else {
ptr = // 在接口映射中查找iid
}
return ptr; // (如果没找到则为NULL)
}

换句话说,如果请求的接口是IUnknown,则GetInterface返回接口映射中的第一个接口指针,否则它查找与请求的接口ID匹配的那一个。这里的诀窍在于:你的类所提供的每一个接口都必须和基类接口一样实现IUnknown,并且还要以相同的方式实现,至于InternalQueryInterface返回哪一个接口指针并不重要。CCmdTarget类自身没有QueryInterface函数,只有嵌套类有,这个嵌套类实现每个接口,每个接口又都实现IUnknown。

图一是一个典型的COM类实现。.CPP文件说明了你必须为你的类所提供的每一个接口编写同样烦人IUnknown实现。每一个IUnknown方法为父类(从CcmdTarget派生)调用相应的ExternalXxx(或者InternalXxx——如果你不想要聚合)方法。这个实现对于你编写的每一个接口都一样。这是没有办法的,因为所有的接口都通过相同的单对象在内存中被实例化。AddRef 和Release必须增加和减少相同的物理指针——与AddRef 或Release实际属于哪个接口(嵌套类)无关。此乃高招所在。

这就是为什么在IUnknown的情况下只有InternalQueryInterface能返回你的接口映射中的第一个接 口指针。由于仅仅实现IUnknown的类没什么用,所以在你的映射中至少还要实现一个接口。如果你不明白,下面是具体步骤的解释:

1、客户端调用CoCreateInstance创建你的类实例。

2、CoCreateInstance查找并加载DLL,调用DllGetClassObject

3、DllGetClassObject搜索DLLs的类工厂清单找出与类工厂匹配的类ID,然后返回这个类工厂的指针。

4、CoCreateInstance调用IClassFactory::CreateInstance创建你的应用实例。

5、COleObjectFactory::XClassFactory::CreateInstance 调用 CRuntimeClass::CreateInstance在内存中创建你的MFC类实例。然后CreateInstance调用InternalQueryInterface获取你的类IUnknown接口指针。InternalQueryInterface依次调用CCmdTarget::GetInterface.。

6、如果所请求的接口是IUnknown以外的其它接口,则GetInterface在你的类接口映射中查找这个接口。否则,GetInterface返回你的类接口映射中的第一个接口指针。这个工作对于实现IUnknown的每一个嵌套类都是一样的。

7、所有的函数返回、返回、返回……..而调用者只管接收某个接口指针。 谁说COM难学?

在结束本文的讨论前,我想指出两个有用的技巧和建议。重载MFC缺省行为的方式有很多。第一,你可以在类工厂中,有时你可能想从COleObjectFactory类派生自己专用的类工厂。只要你愿意,可以这么做,诀窍是将类工厂挂钩在对象上。不要使用DECLARE_OLECREATE 和 IMPLEMENT_OLECREATE,因为这些宏已经将ColeObjectFactory写死在里面了。但你可以拷贝这些宏、改名以及江类工厂的名字改为自己的名字。尤其是你可能要重载COleObjectFactory::OnCreateObject方法。例如,如果你的COM对象是单实例的,就要重载OnCreateObject返回一个且是唯一的一个对象实例(不要使用静态对象,要不然可能遇到引用计数问题,因为静态实例引用计数是1并且最终得不到Release释放。应该取而代之用new在堆中分配单实例)。

最后,一个非常有用的重载是GetInterfaceHook。记得GetInterface吗?这个函数在你的接口映射中查找接口,或如果请求的是IUnknown,则就返回第一个接口。下面是一段参考代码:

LPUNKNOWN CCmdTarget::GetInterface(const void* iid)
{
// 允许常规构子首先起来
LPUNKNOWN lpUnk;
if ((lpUnk = GetInterfaceHook(iid)) != NULL)
return lpUnk;
……
// 如前所述
}

在做其它事情之前,GetInterface调用虚函数CCmdTarget::GetInterfaceHook,缺省CCmdTarget实现返回NULL,但如果你想以某种特别方式实现QueryInterface接口的话,只要重载GetInterfaceHook并返回别的东西就行了。一旦GetInterfaceHook的返回值为非空接口指针,则它首先调用QueryInterface,MFC将用到它。在我的另一篇文章中,曾讨论过如何重载GetInterfaceHook来完成一个完全不同的COM实现,其中用了ATL风格的多继承代替了MFC的嵌套类。 谁说COM复杂难懂?

IUnknown—COM和MFC的更多相关文章

  1. 用ATL和MFC来创建ActiveX控件

    摘要:目前MFC和ATL代表了两种框架,分别面向不同类型的基于Windows的开发.MFC代表了创建独立的Windows应用的一种简单.一致的方法:ATL提供了一种框架来实现创建COM客户机和服务器所 ...

  2. 【转载】COM 组件设计与应用(十)——IDispatch 接口 for VC.NET

    原文:http://vckbase.com/index.php/wv/1225.html 一.前言 终于写到了第十回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常用 ...

  3. 【转载】COM 组件设计与应用(九)——IDispatch 接口 for VC6.0

    原文: http://vckbase.com/index.php/wv/1224.html 一.前言 终于写到了第九回,我也一直期盼着写这回的内容耶,为啥呢?因为自动化(automation)是非常常 ...

  4. 魔改——MFC SDI 支持 内嵌 EXCEL OLE

    ==================================声明================================== 本文版权归作者所有 未经作者授权 请勿转载 保留法律追究的 ...

  5. C++(MFC)中WebBrowser去除3D边框的方法(实现IDocHostUIHandler接口)控制 WebBrowser 控件的外观和行为

    在 CSDN 上经常看到以下两个问题:1.在 MFC 应用程序中,如果创建了一个 WebBrowser 控件(包括 CHtmlView 在内),如何可以把该控件的三维边框禁止掉?2.在 MFC 应用程 ...

  6. MFC通过ADO操作Access数据库

    我在<VC知识库在线杂志>第十四期和第十五期上曾发表了两篇文章——“直接通过ODBC读.写Excel表格文件”和“直接通过DAO读.写Access文件”,先后给大家介绍了ODBC和DAO两 ...

  7. 关于 "Context" 模式(基于COM思想IUnknown思想)

    有同事很喜欢用Context模式,觉得是自己"首创", 我有些自己的想法, 或者大家可以发表下自己的观点.   什么是Context模式? 23种设计模式中没有这个模式, 是同事自 ...

  8. MFC连接Access讲解(3合1) .

    方法一: 1.首先,要用#import语句来引用支持ADO的组件类型库(*.tlb),其中类型库可以作为可执行程序(DLL.EXE等)的一部分被定位在其自身程序中的附属资源里,如:被定位在msado1 ...

  9. MFC模块状态(一)

    先看一个例子: 1.创建一个动态链接到MFC DLL的规则DLL,其内部包含一个对话框资源.指定该对话框ID如下:              #define IDD_DLL_DIALOG  2000 ...

随机推荐

  1. RTX2010服务器端的主要通信端口有哪些?

    RTX服务端程序在安装之后,如果安装服务端电脑的操作系统有防火墙(如Windows XP.Windows2003等)或者安装了防火墙(如瑞星.Norton等),那么需要在防火墙上打开RTX所需要使用的 ...

  2. Oracle游标带参数

    Oracle游标是可以带参数的,而SqlServer的游标就不可以了 create or replace procedure a as cursor b(c_id int)is select * fr ...

  3. textfield设置左边距

    CGRect frame = f;//f表示你的textField的frame frame.size.width = ;//设置左边距的大小 UIView *leftview = [[UIView a ...

  4. C# 基础(3)

    跳转语句 Goto(现在一般不使用) 标志位 Flag true false 常量:(不可改变的量) 语法: Const 类型 变量名 = 常量值 在定义时赋值,其他地方不能赋值 枚举 是自己定义了一 ...

  5. Windows中多个python版本共存的问题

    原创文章,未经本人允许进制转载. 在我的Windows中,先安装了python3.4,后来因为需要又安装了python2.7,结果发现: 直接双击1.py和命令行python 1.py使用的pytho ...

  6. asp.net正则表达式学习例子

    asp.net 获取网页Document时常会用到 edited by:曹永思-博客园 1.获取某个class的div内的标签 获取<div class="imgList2" ...

  7. 【03_136】Single Number

    感谢:http://www.cnblogs.com/changchengxiao/p/3413294.html Single Number Total Accepted: 103007 Total S ...

  8. ManyToMany【项目随笔】关于异常object references an unsaved transient instance

    在保存ManyToMany  时出现异常: org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.Tran ...

  9. iOS 开发者必知的 75 个工具(译文)

    原文地址:http://benscheirman.com/2013/08/the-ios-developers-toolbelt (需FQ)   如果你去到一位熟练的木匠的工作室,你总是能发现他/她有 ...

  10. Linux下paste命令

    paste 用于将多个文件按照列队列进行合并. 该命令主要用来将多个文件的内容合并,与cut命令完成的功能刚好相反. 1.原文件: 1>a.txt [root@localhost home]# ...