ATL揭秘之“对象创建”篇(转)

2024-04-25 13:38
文章标签 创建 对象 揭秘 atl

本文主要是介绍ATL揭秘之“对象创建”篇(转),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.问题

    当我们用 VC++ ATL 工程创建了一个 COM 工程,实现了一个自己的 COM 对象,又在另一个程序中 CoCreateInstance 这个 COM 对象时,不知你是否想过这样的问题: COM 对象是用 C++ 类对象实现的,但是,我们从来没有在自己的代码中创建这些 C++ 类对象——比如,“ new ”这些对象。那么,实现 COM 对象的 C++ 对象是由谁,何时,以及如何创建的呢?

     当然,简单而且正确的回答是: ATL 在幕后帮助我们完成了这些工作。如果你不想了解 ATL 的工作细节,这样的回答应该是足够了。然而, ATL 本身的思想就是“白盒”操作,想要用好 ATL ,就应该尽量多地了解 ATL 的工作细节。所以,搞清楚这个问题还是很有必要的。

    想到这个问题后,出于懒惰的天性,我首先上网,试图找到别人对于这个问题的讲述,然而,大家要么讨论 C++ 对象,要么讨论 ATL 其他的机制,似乎没有人特别关注 ATL COM 对象的创建过程,更比较少有人留心 ATL 如何将 COM 对象创建过程转换到 C++ 对象创建过程上。

    在研究这个问题的过程中,我逐渐发现这个问题很有意思,对这个问题的完整回答涉及了 ATL 相当多的基础结构。弄清楚了这个问题,对 ATL 的了解也会加深不少。

下面,我们就一起开始 ATL 对象创建揭秘之旅。


2           “对象”探讨

既然谈“对象创建”,则有必要对“对象”这个概念作一点讨论。在实际工作中,我感觉不少人对“对象”这个概念有不少误解;对“ COM 对象”也没有清晰的认识。

2.1       对象性质

    这似乎是老生常谈了。对象性质,不就是“封装、继承、多态”这三个陈词滥调吗?然而,孔老夫子教导我们说:“温故而知新”。真理往往就蕴含在陈词滥调中。经过这些年的软件生涯,我对这句“陈词滥调”似乎有了更多地理解:

    首先,“对象性质”是个独立的概念,也就是说,凡是具备了这个性质的东西就可以被称作对象。因此,一来“对象”不一定要用面向对象的语言编写,二来“对象”也可以具备各种环境下的语义——面向对象语言生成的对象是“编程语言”语义下的对象,如“ C++ 对象”; 面向组件的开发生成的对象是“组件环境”语义下的对象,如“ COM 对象”。

    其次,对象性质中的“继承”、“多态”需要好好斟酌。

    什么是“继承”?是不是一定要用“ CMyObject::CBaseObject ”这样的语法才叫继承?当然不是,“继承”应该是对“对象层次结构”的有效处理。只要能够有效地处理对象层次结构,使低层对象能够自动具备高层对象的特性、行为,就应该可以被叫做“继承”。“ CMyObject::CBaseObject ”干的是什么事?不就是把 CBaseObject 的成员变量复制给了 CMyObject ,并且使 CMyObject 的对象能够调用 CBaseObject 的公有和保护方法吗?

    再说“多态”。 C++ 语言说“多态”就是支持虚函数调用,这样讲对,但是局限在 C++ 语言本身上。“虚函数调用”是某些语言的特性,难道没有虚函数的语言就无法支持多态了?其实“多态”这个词本身译得很好,直抒其意——“多姿多态”。“多态”本质上是“运行时决定行为”。只要能够在运行时才决定如何行动,而不是在编译时决定,就是“多态”。

综合看来,“继承”和“多态”都不是面向对象语言的专利,其他的语言,只要能够通过某种机制实现这些特性,就可以实现“对象”。

2.2       COM 对象

COM 规范对于 COM 对象如何做到“封装、继承、多态”有自己的规定。该规定不依赖具体语言,不依赖具体的操作系统环境,所以,我们说 COM 规范是语言中立和平台中立的(当然,提供平台的人并不中立,这和规范的中立是两码事)。

  • “封装”: COM 对象只处理行为封装,其工具是“接口”;
  • “继承”: COM 的继承不是源代码级别,是二进制代码级别。 COM 对象提供了两种方式来继承对象的二进制代码——“包容”和“聚合”;
  • “多态”: COM 的“运行时决定行为”能力来自不同对象实现同一接口。使用 COM 的统一方式—— QueryInterface ,我们可以找到不同 COM 对象对同一接口的实现,从而实现“运行时决定行为”。

    当然, COM 对象除了这老三样之外,还要其他性质,其中最重要的就是对象的“生命周期管理”。“生命周期管理”通过 AddRef Release 这两个“引用计数”函数实现。


3         ATL COM 对象

  ATL 实现 COM 对象的基本思路是:针对不同的 COM 对象性质,分层处理。不同的 ATL 类层次处理特定的 COM 对象特性。

ATL COM 对象的层次结构如下图所示:

ATL COM对象模型


从上图可以看到:

    最基础的类是 CComObjectRootBase 。该类除了提供 InternalQueryInterface 方法外,还实现了若干帮助方法可供最终派生类 CComObject 调用; CComObjectRootEx 是个模板类。该类根据不同的线程模型生成足够线程安全的 InternalAddRef InternalRelease 函数。为什么只提供一个 CComObjectRootEx 类呢?

   我觉得主要的原因是: CComObjectRootBase 实现的 InternalQueryInterface 不涉及对类成员数据的线程保护,不涉及线程安全因素; CComObjectRootEx InternalAddRef InternalRelease 方法则和线程安全密切相关,故 CComObjectRootEx 有必要作为模板类实现。将这两个类揉到一起实现反而显得不清晰;

    我们自己定义的类直接从 CComObjectRootEx 继承,根据需要选择不同的线程模型;

  ATL 最后实际创建的 COM 对象是 CComObject CComAggObject 等类的实例。这些类负责真正实现 QueryInterface AddRef Release 方法,具体选择哪个类根据宏定义来决定。具体在哪里定义什么宏在 4.3 节会讲到。


4         ATL COM 对象创建——内部机制

   所谓内部机制,指的是类厂创建 COM 对象的过程。由于类厂也在 COM 对象的实现类中实现,所以,类厂对象创建相应 COM 对象的过程可以看作是 COM 对象的内部过程。

   正是在这个内部机制中,“ COM 对象创建”这个动作被转换到“ C++ 对象创建”这个动作上。

下图是对内部机制的简单勾勒:

ATL对象创建内部机制

   从这幅图中可以看到,内部创建主要涉及三个类的交互作用,它们是: CComCreator CComClassFactory CComCoClass 。下面就对这三个类分别讲述。


4.1       CComCreator —— COM 对象创建器

  COM 规范要求用类厂来创建 COM 对象,其目的是使 COM 对象能够控制自己的创建过程(“类厂”设计模式的典型应用)。由于类厂对象本身也是一个 COM 对象,所以, ATL 为了统一 COM 对象的创建过程,封装了一个 CComCreator 类。 ATL CComCreator 这个类的作用很单纯,正如其名字所表示的——创建 COM 对象。该类包装了一个 CreateInstance 静态方法(之所以是静态方法,因为该方法要放到 _ATL_OBJMAP_ENTRY 中,后面会讲到),正是在 CComCreator CreateInstance 方法中, ATL COM 对象创建被转换到具体的 C++ 对象创建上。由于这个类如此重要,因此有必要列出这个类的实现:

  其中,底色是黄色的那句代码就是实际创建 C++ 对象的代码。看到熟悉的“ new ”了。

  从这个类是模板类也可以看出, ATL 中所有的 COM 对象创建,最终其实都是由 CComCreator 类负责。比如,创建 COM 对象可以用 CComCreator<CComObject> 的形式;创建类厂类可以用 CComCreator<CComClassFactory> 的形式。后面那个 CComCreator CComClassFactory 就是我们说的类厂类。

4.2       CComClassFactory

每个 COM 对象类都有一个自己的类厂类,专门负责创建该类的类对象。在 ATL 中,缺省的类厂类是 CComClassFactory 。类厂类也有一个 CreateInstance 方法,该方法调用类厂类保存的 COM 对象类的 CComCreator 的静态 CreateInstance 函数指针,创建相应的 COM 对象。

4.3       CComCoClass

CComCoClass 是一个非常重要的 ATL 实现类。基本上我们自己的类都要从 CComCoClass 继承。为什么?因为 CComCoClass 定义了两个宏:

    DECLARE_CLASSFACTORY()

    DECLARE_AGGREGATABLE(T)

    前一个宏定义了 _ClassFactoryCreatorClass ——类厂类的创建者,该创建者可以使用不同的类厂类作为模板参数,为 COM 对象的创建过程提供了灵活性;后一个宏定义了 _CreatorClass —— COM 对象类的创建者,该创建者使用 CComObject 类族的不同类作为模板参数,为 COM 对象 QueryInterface AddRef Release 函数的实现方式提供了不同选择。

    通过继承 CComCoClass ,我们自己的类就继承了 CComCoClass 对类厂和最后生成类的实现。

  CComCoClass 也有一个 CreateInstance 方法。该方法纯粹是对 _CreatorClass::CreateInstance 方法的包装。因为我们的类继承自 CComCoClass ,经过这个包装后,就可以直接以 CUserClass::CreateInstance 的方式来调用 CComCreator::CreateInstance 了。


上图看到的三个 CreateInstance 方法,各有各的意义,这里总结一下:

CComCreator::CreateInstance

真正创建 C++ 对象的所在

CComClassFactory::CreateInstance

调用 _CreatorClass::CreateInstance

CComCoClass::CreateInstance

调用 _CreatorClass::CreateInstance

 

 至此,估计大家一定有一个疑问: _CreatorClass::CreateInstance 由类厂对象的 CreateInstance 调用; _ClassFactoryCreatorClass::CreateInstance 又由谁来调用呢?这就是我们要进入的下一个论题: ATL COM 对象创建的外部机制。


5         ATL COM 对象创建——外部机制

所谓“外部机制”,指的是应用程序创建 ATL COM 对象类厂的过程。应用程序并不关心 COM 对象是 MFC 实现方式的还是 ATL 实现方式的,它永远使用 CoCreateInstance 这类 API 函数,通过类厂创建 COM 对象。在 ATL 下,应用程序对 CoCreateInstance 的调用,是如何转换到对 ATL COM 对象类厂 CreateInstance 方法的调用的呢?

5.1       COM 服务器

COM 对象不能凭空存在,它必须存在于操作系统的某种可执行文件中。由于只有 Windows 操作系统支持 COM 规范,很自然地, COM 对象存在于 Windows 操作系统的可执行文件中。

Windows 操作系统的可执行文件,其格式主要有两种: EXE DLL 。这里就不必要说这两种文件格式的区别了吧。如果不知道,这篇文章你估计也看不懂了。

能够生成 COM 对象的可执行程序叫 COM 服务器。 EXE 是进程外服务器, DLL 是进程内服务器。这里只讨论 DLL 的情况。由于 DLL 本身只能通过对外输出的函数与外界交互,所以, DLL 作为 COM 服务器也是通过四个输出函数来体现其服务器的作用。这就是著名的四个函数:

  • DllRegisterServer
  • DllUnregisterSever
  • DllGetClassObject
  • DllCanUnloadNow

COM 服务器的工作机制可以用下图来表示:

ATL对象创建外部机制

COM 服务器的重要功能可以归纳为三个:

  • 管理服务器的生命周期;
  • 管理服务器和对象的注册;
  • 获得 COM 对象的类厂;

我们可以看到,作为 COM 服务器的 DLL ,用四个函数来完成这三个方面的功能。四个输出函数的调用时机分别如下:

  • DllRegisterServer DllUnregisterServer :使用 regsvr32 程序注册和反注册服务器时;
  • DllCanUnloadNow :当调用 CoFreeUnusedLibraries 系统函数时;
  • DllGetClassObject :从函数的字面意思来理解,应该是创建 COM 对象时该函数被调用。而我们知道创建 COM 对象的 API 函数是 CoCreateInstance CoCreateInstance 是个封装函数,它包装了对 CoGetClassObject ,以及相应类厂的 CreateInstance 函数的调用。 CoGetClassObject 通过注册表机制,找到相应的服务器,并且调用服务器的 DllGetClassObject 函数来获得类厂。一旦获得类厂对象,就可以调用类厂对象的 CreateInstance 方法来创建 COM 对象了。

5.2       ATL COM 服务器

  前面讲的是所有 COM 服务器都应该遵循的工作流程。不同的 COM 实现,实现这个流程的方式也不同。对于 ATL 来说,其具体的实现可以用下图简略体现:

ATL COM服务器

ATL COM 服务器主要通过 CComModule 类和 _ATL_OBJMAP_ENTRY 结构来实施服务器管理。前面讲过, COM 服务器的主要职能是三个:管理服务器生命周期、注册组件、获得 COM 对象的类厂,所以, CComModule 的成员函数也围绕这三个方面。同样, _ATL_OBJMAP_ENTRY 数据结构中的内容也紧紧围绕着这三个方面。由于本文讨论 COM 对象创建,所以,对服务器管理的讨论也局限在“获得 COM 对象的类厂”上。 ATL COM 服务器实现“获得 COM 对象的类厂”的步骤如下:

1、 所有的 ATL 工程都会生成一个全局变量,其类型为 CComModule ,名字固定为 _Module

2、 DLL 的四个输出函数内部都是调用 _Module 的成员函数来实现其功能。

3、 CComModule 提供了一系列成员函数来管理 COM 服务器,这些方法基本都工作在 _ATL_OBJMAP_ENTRY 结构数组上。

4、 _ATL_OBJMAP_ENTRY 结构内的成员基本上都是一些静态成员函数指针。最重要的函数指针是两个: pfnGetClassObject pfnCreateInstance ,它们都指向 CComCreator 的静态成员函数 CreateInstance

5、 _ATL_OBJMAP_ENTRY 结构数组由三个宏配合定义: BEGIN_OBJ_MAP OBJECT_ENTRY END_OBJ_MAP 。其中, OBJECT_ENTRY 宏比较重要,有必要在下面列出其定义:


#define OBJECT_ENTRY(clsid, class) /

{&clsid, class::UpdateRegistry, /

class::_ClassFactoryCreatorClass::CreateInstance , /

class::_CreatorClass::CreateInstance , NULL, 0, /

class::GetObjectDescription, class::GetCategoryMap, /

class::ObjectMain },

注意黄底色部分。该宏用 class 的数据成员 _ClassFactoryCreatorClass CreateInstance 静态函数指针填充到 pfnGetClassObject 位置。用 _CreatorClass CreateInstance 静态函数指针填充到 pfnCreateInstance 位置。

    要找到一个特定的类厂, DllGetClassObject 将调用 CComModule 的成员函数 GetClassObject GetClassObject 遍历结构数组,找到相应的 CLSID 对应的 _ATL_OBJMAP_ENTRY 结构。 ATL 会先检查结构中的 pCF ,这是 ATL 缓存的类厂对象指针,如果不为空,则可以直接利用该指针来创建 COM 对象,如果为空,则调用结构中的 pfnGetClassObject 函数指针,创建相应的类厂对象并且把类厂对象的指针缓存到 pCF 成员数据中。


6         ATL COM 对象创建——内外结合

本文中,先讲了 ATL COM 对象本身,接着讲了 ATL COM 对象创建的内部机制—— ATL COM 对象的类厂如何创建 ATL COM 对象;再接着讲了 ATL COM 对象创建的外部机制—— ATL COM 服务器如何创建 ATL COM 对象的类厂。有个这几方面的了解之后,我们再把相关的知识结合起来,看一看 ATL COM 对象创建的统一场景。图示如下:

ATL内外结合

    图中左上部分是 ATL COM 对象本身;右上部分是 ATL COM 对象的创建;中下部分是 ATL COM 服务器对 COM 对象的管理。

对每个部分的作用,本文各个部分已经有了具体描述,这里要强调的是图中标示为红色部分: _ATL_OBJMAP_ENTRY 结构和 CComCreator ,正是通过它们,图中三个部分有机地联系到了一起,完成了 ATL COM 对象创建的任务。

   通观本文,没有给什么“示范代码”,而是力求从本人理解的 COM 原理的角度探讨 ATL COM 对象创建机制。有可能这样的讨论在理论真正精深者看来不值一哂,然而,本人希望那些觉得 ATL 不好理解的人有了这次 ATL COM 对象创建过程探索的经历,能够感觉 ATL 好把握一些了,不再是若干莫名其妙的模板类的组合了。


源网址

http://www.diybl.com/course/3_program/c/c_js/2007109/77292.html

这篇关于ATL揭秘之“对象创建”篇(转)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/934838

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Python创建Excel的4种方式小结

《Python创建Excel的4种方式小结》这篇文章主要为大家详细介绍了Python中创建Excel的4种常见方式,文中的示例代码简洁易懂,具有一定的参考价值,感兴趣的小伙伴可以学习一下... 目录库的安装代码1——pandas代码2——openpyxl代码3——xlsxwriterwww.cppcns.c

使用Python在Excel中创建和取消数据分组

《使用Python在Excel中创建和取消数据分组》Excel中的分组是一种通过添加层级结构将相邻行或列组织在一起的功能,当分组完成后,用户可以通过折叠或展开数据组来简化数据视图,这篇博客将介绍如何使... 目录引言使用工具python在Excel中创建行和列分组Python在Excel中创建嵌套分组Pyt

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

JSON字符串转成java的Map对象详细步骤

《JSON字符串转成java的Map对象详细步骤》:本文主要介绍如何将JSON字符串转换为Java对象的步骤,包括定义Element类、使用Jackson库解析JSON和添加依赖,文中通过代码介绍... 目录步骤 1: 定义 Element 类步骤 2: 使用 Jackson 库解析 jsON步骤 3: 添

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

mysql外键创建不成功/失效如何处理

《mysql外键创建不成功/失效如何处理》文章介绍了在MySQL5.5.40版本中,创建带有外键约束的`stu`和`grade`表时遇到的问题,发现`grade`表的`id`字段没有随着`studen... 当前mysql版本:SELECT VERSION();结果为:5.5.40。在复习mysql外键约

Window Server创建2台服务器的故障转移群集的图文教程

《WindowServer创建2台服务器的故障转移群集的图文教程》本文主要介绍了在WindowsServer系统上创建一个包含两台成员服务器的故障转移群集,文中通过图文示例介绍的非常详细,对大家的... 目录一、 准备条件二、在ServerB安装故障转移群集三、在ServerC安装故障转移群集,操作与Ser

Window Server2016 AD域的创建的方法步骤

《WindowServer2016AD域的创建的方法步骤》本文主要介绍了WindowServer2016AD域的创建的方法步骤,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、准备条件二、在ServerA服务器中常见AD域管理器:三、创建AD域,域地址为“test.ly”

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去