ATL揭秘之“对象创建”

2024-03-07 19: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对象除了这老三样之外,还要其他性质,其中最重要的就是对象的“生命周期管理”。“生命周期管理”通过AddRefRelease这两个“引用计数”函数实现。

 

3         ATL COM对象

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

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

从上图可以看到:

  • 最基础的类是CComObjectRootBase。该类除了提供InternalQueryInterface方法外,还实现了若干帮助方法可供最终派生类CComObject调用;
  • CComObjectRootEx是个模板类。该类根据不同的线程模型生成足够线程安全的InternalAddRefInternalRelease函数。为什么只提供一个CComObjectRootEx类呢?我觉得主要的原因是:CComObjectRootBase实现的InternalQueryInterface不涉及对类成员数据的线程保护,不涉及线程安全因素;CComObjectRootExInternalAddRefInternalRelease方法则和线程安全密切相关,故CComObjectRootEx有必要作为模板类实现。将这两个类揉到一起实现反而显得不清晰;
  • 我们自己定义的类直接从CComObjectRootEx继承,根据需要选择不同的线程模型;
  • ATL最后实际创建的COM对象是CComObjectCComAggObject等类的实例。这些类负责真正实现QueryInterfaceAddRefRelease方法,具体选择哪个类根据宏定义来决定。具体在哪里定义什么宏在4.3节会讲到。

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

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

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

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

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

4.1      CComCreator——COM对象创建器

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

 

template <class T1>

class CComCreator

{

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->QueryInterface(riid, ppv);

           if (hRes != S_OK)

              delete p;

       }

       return hRes;

    }

};

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

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

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对象QueryInterfaceAddRefRelease函数的实现方式提供了不同选择。

通过继承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操作系统的可执行文件,其格式主要有两种:EXEDLL。这里就不必要说这两种文件格式的区别了吧。如果不知道,这篇文章你估计也看不懂了。

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

  • DllRegisterServer
  • DllUnregisterSever
  • DllGetClassObject
  • DllCanUnloadNow


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

 

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

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

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

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

5.2      ATL COM服务器

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

 

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结构内的成员基本上都是一些静态成员函数指针。最重要的函数指针是两个:pfnGetClassObjectpfnCreateInstance,它们都指向CComCreator的静态成员函数CreateInstance

5、 _ATL_OBJMAP_ENTRY结构数组由三个宏配合定义:BEGIN_OBJ_MAPOBJECT_ENTRYEND_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的数据成员_ClassFactoryCreatorClassCreateInstance静态函数指针填充到pfnGetClassObject位置。用_CreatorClassCreateInstance静态函数指针填充到pfnCreateInstance位置。

要找到一个特定的类厂,DllGetClassObject 将调用CComModule的成员函数GetClassObjectGetClassObject遍历结构数组,找到相应的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 COM对象本身;右上部分是ATL COM对象的创建;中下部分是ATL COM服务器对COM对象的管理。

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

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

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



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

相关文章

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

揭秘世界上那些同时横跨两大洲的国家

我们在《世界人口过亿的一级行政区分布》盘点全球是那些人口过亿的一级行政区。 现在我们介绍五个横跨两州的国家,并整理七大洲和这些国家的KML矢量数据分析分享给大家,如果你需要这些数据,请在文末查看领取方式。 世界上横跨两大洲的国家 地球被分为七个大洲分别是亚洲、欧洲、北美洲、南美洲、非洲、大洋洲和南极洲。 七大洲示意图 其中,南极洲是无人居住的大陆,而其他六个大洲则孕育了众多国家和

三国地理揭秘:为何北伐之路如此艰难,为何诸葛亮无法攻克陇右小城?

俗话说:天时不如地利,不是随便说说,诸葛亮六出祁山,连关中陇右的几座小城都攻不下来,行军山高路险,无法携带和建造攻城器械,是最难的,所以在汉中,无论从哪一方进攻,防守方都是一夫当关,万夫莫开;再加上千里运粮,根本不需要打,司马懿只需要坚守城池拼消耗就能不战而屈人之兵。 另一边,洛阳的虎牢关,一旦突破,洛阳就无险可守,这样的进军路线,才是顺势而为的用兵之道。 读历史的时候我们常常看到某一方势

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

无线领夹麦克风什么牌子好用?揭秘领夹麦克风哪个牌子音质好!

随着短视频行业的星期,围绕着直播和视频拍摄的电子数码类产品也迎来了热销不减的高增长,其中除了数码相机外,最为重要的麦克风也得到了日益增长的高需求,尤其是无线领夹麦克风,近几年可谓是异常火爆。别看小小的一对无线麦克风,它对于视频拍摄的音质起到了极为关键的作用。 不过目前市面上的麦克风品牌种类多到让人眼花缭乱,盲目挑选的话容易踩雷,那么无线领夹麦克风什么牌子好用?今天就给大家推荐几款音质好的

负债不再是障碍?银行信贷“白名单“揭秘

谈及银行信贷产品,常闻有言称存在无需考量负债与查询记录之奇品,此等说法十有八九为中介诱人上钩之辞。轻信之下,恐将步入连环陷阱。除非个人资质出类拔萃,如就职于国央企或事业单位,工龄逾年,五险一金完备,还款能力卓越,或能偶遇线下产品对查询记录稍显宽容,然亦非全然无视。宣称全然不顾者,纯属无稽之谈。 银行非慈善机构,不轻易于困境中援手,更偏爱锦上添花之举。若无坚实资质,即便求助于银行亦难获青睐。反

顺序表之创建,判满,插入,输出

文章目录 🍊自我介绍🍊创建一个空的顺序表,为结构体在堆区分配空间🍊插入数据🍊输出数据🍊判断顺序表是否满了,满了返回值1,否则返回0🍊main函数 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾”

Maven创建项目中的groupId, artifactId, 和 version的意思

文章目录 groupIdartifactIdversionname groupId 定义:groupId 是 Maven 项目坐标的第一个部分,它通常表示项目的组织或公司的域名反转写法。例如,如果你为公司 example.com 开发软件,groupId 可能是 com.example。作用:groupId 被用来组织和分组相关的 Maven artifacts,这样可以避免