组件对象模型
一、历史
关于历史内容,不赘述,可以查看资料很多,这里分享一下知乎上一个有趣答案。
作者:灵剑 链接:https://www.zhihu.com/question/49433640/answer/115952604 来源:知乎
摘要如下:
1.dll的接口是为了C设计的,它导出的基本都是C的函数,从原理上来说,将dll加载到内存之后,会告诉你一组函数的地址,你自己call进去就可以调用相应的函数。
2.要解决这问题,我们必须在dll当中导出一个CreateObject的方法,用来代替构造函数,然后返回一个接口。然而,接口的定义在不同版本当中也是有可能会变化的,为了兼容以前的版本同时也提供新功能,还需要让这个对象可以返回不同版本的接口。
3.因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。
4.COM技术的奇怪地方在于微软实在是脑洞太大了,它们构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识,以后想要哪个Interface只要报出UUID来就行了。
5.Java的直接基于类的接口被广泛应用,开发使用起来远远来的简单……很快微软也抄了一个.NET托管dll的方案出来,于是纯的二进制接口COM就慢慢被抛弃了。
二、概述
- 组件对象模型 (COM) 是 Microsoft 于 1993 年推出的软件组件的二进制接口标准。
- 它的用途是:在大量编程语言中,创建进程间通信对象。
- COM 是其他 Microsoft 技术和框架的基础,包括 OLE、OLE 自动化、Browser Helper Object、ActiveX、COM+、DCOM、Windows shell、DirectX、UMDF 和 Windows Runtime。
- COM 的本质是一种实现对象的语言中立方式,这些对象可以在不同于创建对象的环境中使用,甚至可以跨越机器边界。【译者注:COM 的本质是,创建和使用与编程语言无关的对象,并且在不同环境中使用,甚至在不同的机器上使用】
- 复用性:对于编写良好的组件,COM 允许在不了解其内部实现的情况下重用对象,因为它迫使组件实现者提供“与实现分离的”定义良好的接口。
- “语言的不同分配语义”是通过“引用计数”“使对象负责自己的创建和销毁”来适应的。【译者注:语言的不同分配语义,是指不同编程语言对内存的分配和回收的那一部分语义。通俗地讲,通过“引用计数(reference-counting)”,对象实现自己的 new() 和 销毁】
- 对象不同接口之间的类型转换是通过QueryInterface方法实现的。
- COM 中“继承”的首选方法是创建方法“调用”被委托给的子对象【译者注:父对象的方法“调用”被委托给子对象】。
- COM 是一种接口技术,仅在 Microsoft Windows 和 “Apple 的 Core Foundation 1.3 和更高版本”的插件 API 上,被定义和“实现为标准”。后者只实现了整个 COM 接口的一个子集。
- 对于某些应用程序,COM 至少在某种程度上已被 Microsoft .NET 框架取代,并通过 WCF 支持 Web 服务。但是,COM 对象可以通过 .NET COM Interop 与所有 .NET 语言一起使用。联网 DCOM 使用二进制专有格式,而 WCF 鼓励使用基于 XML 的 SOAP 消息传递。
- COM 与其他组件软件接口技术(例如 CORBA 和 Enterprise JavaBeans)非常相似,尽管它们都有自己的优点和缺点。
- 与 C++ 不同,COM 提供了一个稳定的应用程序二进制接口 (ABI),在编译器版本之间不会改变。这使得 COM 接口对“使用不同编译器版本编译的客户端使用的面向对象的 C++ 库”具有吸引力。
三、技术细节
1.概述
COM 程序员使用 COM 感知组件构建他们的软件。 不同的组件类型由类 ID (CLSID) 标识,它们是全局唯一标识符 (GUID)。 每个 COM 组件都通过一个或多个接口公开其功能。 组件支持的不同接口使用接口 ID (IID) 来区分,接口 ID 也是 GUID。 COM 接口具有多种语言的绑定,例如 C、C++、Visual Basic、Delphi、Python 和一些在 Windows 平台上实现的脚本语言。 所有对组件的访问都是通过接口的方法完成的。 这产生了进程间进行编程的技术,甚至是计算机间进行编程的技术(使用 DCOM 的支持)。
2.接口
所有 COM 组件都实现了 IUnknown(自定义)接口,该接口公开了用于引用计数和类型转换(强制转换)的方法。 自定义 IUnknown 接口由一个指针组成,该指针指向一个虚拟方法表。该表又包含一组指针(函数指针),这组指针分别指向不同的函数,这些函数是对接口中声明的函数的实现;这组指针顺序与在接口中声明的顺序相同。 因此,进程内调用开销与 C++ 中的虚拟方法调用相当。
IUnknown接口提供的IUnknown::QueryInterface(),IUnknown::AddRef()及IUnknown::Release()三个方法。所有的COM组件都要实现IUnknown,表示每个COM组件都有相同的能力。
只由 IUnknown 派生实现出来的组件,称为纯COM组件。
除了自定义接口,COM 还支持调度接口(从 IDispatch 继承)。 调度接口支持 OLE 自动化的晚绑定(late binding)。相比自定义接口,有更广泛的编程语言本地访问(natively access)调度接口。
纯COM组件只能够支持C/C++的直接访问,为了要达到跨语言的能力,在COM中必须要支持在外部调用内部方法的机能,这个机能造就了Invoke()
方法,另外为了跨语言的支持,COM应该要提供简单的组件访问识别方式,这也就是会有GetIDsOfNames()
的原因,将这些方法组合起来,定义出的必要接口,称为IDispatch
接口,所有实现此接口的,都可以支持跨语言的支持。
微软将实现 IDispatch 接口的组件都称为自动化(Automation)组件。
3.类(coclass)
COM 类(“coclass”)是一个或多个接口的具体实现,非常类似于面向对象编程语言中的类。 类是基于它们的类 ID (CLSID) 或基于它们的编程标识符字符串 (ProgID) 而创建的。 像许多面向对象的语言一样,COM 提供了接口与实现的分离。 这种分离在 COM 中尤为明显,在 COM 中不能直接访问对象,而只能通过它们的接口访问。 COM 还支持同一接口的多个实现,以便客户端在运行时可以选择实例化接口的哪个实现。
4.接口定义语言和类型库
类型库包含用于表示 COM 类型的元数据。这些类型使用 Microsoft 接口定义语言 (MSIDL/IDL) 进行描述。 IDL 文件以独立于语言的方式定义面向对象的类、接口、结构、枚举和其他用户定义的类型。 IDL 在外观上类似于 C++ 声明,带有一些额外的关键字,例如“interface”和“library”【用于定义接口和类集合】。 IDL 还支持在声明前使用方括号属性来提供附加信息,例如接口 GUID 以及指针参数和长度字段之间的关系。
IDL 文件由 MIDL 编译器编译。
(1)对于 C/C++,MIDL 编译器生成一个独立于编译器的头文件。该头文件包含了一个结构体定义,此结构体用于匹配“IDL中声明的接口”的vtbl;该头文件同时包含了【接口 GUID 的 “C语言文件包含声明”】。
结构只包含一项成员,即指向函数的地址表的指针(vtbl),以模仿C++对虚函数的实现。
(2)MIDL 编译器还生成了代理模块的 C++ 源代码文件。此代理包含一些方法存根(method stubs),这些方法存根用于将“ COM 调用”转换为“远程过程调用(RPC)”,以启用 DCOM 进行进程外通信。
代理模块(proxy module),用以把COM调用转为远程过程调用,以支持跨进程的DCOM通信。
(3)IDL 文件也可以由 MIDL 编译器编译成类型库 (TLB)。 TLB 文件包含二进制元数据,这些元数据可被不同语言的“编译器和运行时环境”处理(例如 VB、Delphi、.NET 等),以生成特定于语言的构造(构造函数?),来表示 TLB 中定义的 COM 类型。
【译者注:tlb文件是一个说明文件,通过TLB文件,用户可以得知你的DLL中的COM接口以及常量等信息。其可以单独发行,也可以作为组建DLL中的一个资源随组件一起发行。你可以在VS的Object Browser中看到该文件中包括哪些常数、接口、类,而每个类又包括什么方法和属性。微软提供的各种SDK中通常包括一个或数个TLB文件以方便编程。】
5.作为对象框架
因为 COM 是一个运行时框架,所以,类型必须在运行时单独识别和指定。 为此,使用了全局唯一标识符 (GUID)。 每个 COM 类型都指定了自己的 GUID,以便在运行时进行识别。 为了在编译时和运行时都可以访问有关 COM 类型的信息,COM 使用类型库(.dll ?)。 正是通过有效使用类型库,COM 才实现了其作为动态框架的能力。
考虑以下 IDL 中的示例 coclass 定义:
coclass SomeClass { [default] interface ISomeInterface; };
上面的代码片段声明了一个名为 SomeClass 的 COM 类,它实现了一个名为 ISomeInterface 的接口。
这在概念上等同于定义以下 C++ 类:
class SomeClass : public ISomeInterface { ... ... };
其中 ISomeInterface 是 C++ 抽象基类。
包含 COM 接口和类的 IDL 文件被编译成类型库 (TLB) 文件后,客户端可以在运行时解析这些 (TLB) 文件(.dll文件?)以确定对象支持哪些接口,并调用对象的接口方法。
在 C++ 中,使用 CoCreateInstance 函数进行COM 对象的实例化,该函数将类 ID (CLSID) 和接口 ID (IID) 作为参数。 SomeClass 的实例化可以实现如下:
ISomeInterface* interface_ptr = NULL; HRESULT hr = CoCreateInstance(CLSID_SomeClass, NULL, CLSCTX_ALL, IID_ISomeInterface, (void**)&interface_ptr);
在本例中,COM 子系统用于获取“指向实现 ISomeInterface 接口的对象的”指针,并且需要 coclass CLSID_SomeClass 对该接口的特定实现。
6.引用计数
所有 COM 对象都利用引用计数来管理对象生命周期。客户端通过 IUnknown 接口中的 AddRef 和 Release 方法控制引用计数。当引用计数降为零时,COM 对象负责释放它们自己的内存。某些语言(例如 Visual Basic)提供自动引用计数,因此 COM 对象开发人员无需在其源代码中显式维护引用计数器。在 C++ 中,编码器可以执行显式引用计数或使用智能指针来自动管理引用计数。
以下是有关何时在 COM 对象上调用 AddRef 和 Release 的指南:
-
- 返回接口引用(通过返回值或通过“out”参数)的函数和方法,应在返回之前增加返回对象的引用计数。
- 在指针被覆盖或超出范围之前,必须在接口指针上调用 Release。
- 如果在接口引用指针上进行了复制,则应在该指针上调用 AddRef。
- AddRef 和 Release 必须在被引用的特定接口上调用,因为对象可以实现每个接口的引用计数,以便仅为被引用的接口分配内部资源。
并非所有引用计数调用都通过网络发送到远程对象;代理只保留对远程对象的一个引用,并维护自己的本地引用计数。为了简化 COM 开发,Microsoft 为 C++ 开发人员引入了 ATL(活动模板库)。 ATL 提供了更高级别的 COM 开发范例。通过提供智能指针对象,它还使 COM 客户端应用程序开发人员无需直接维护引用计数。其他支持 COM 的库和语言包括 Microsoft Foundation Classes、VC Compiler COM Support、VBScript、Visual Basic、ECMAScript (JavaScript) 和 Borland Delphi。
7.程序设计
COM 是一种与语言无关的二进制标准,可以用任何能够理解和实现其二进制定义的数据类型和接口的编程语言进行开发。 COM 的编程实现,就是为了完成:进入和离开 COM 环境、实例化和引用计数 COM 对象、查询对象以获取支持的接口以及处理错误。 Microsoft Visual C++ 编译器支持 C++ 语言的扩展,称为 C++ Attributes。这些扩展旨在简化 COM 开发,并删除在 C++ 中实现 COM 服务器所需的大部分管道代码(臃肿的代码)。
8.注册表使用
在 Windows 中,COM 类、接口和类型库在注册表中以 GUID 方式列出,在 HKEY_CLASSES_ROOT\CLSID 下为类,在 HKEY_CLASSES_ROOT\Interface 下为接口。 COM 库使用注册表为每个 COM 对象定位正确的本地库,或为远程服务定位网络位置。
免注册COM:
免注册 COM (RegFree COM) 是 Windows XP 中引入的一项技术,它允许组件对象模型 (COM) 组件在不使用注册表的情况下存储组件的激活元数据和 CLSID(类 ID)。
组件中实现的类的元数据和 CLSID 在程序集清单(使用 XML 描述)中声明,作为可执行文件中的资源,或作为与组件一起安装的单独文件存储。这允许同一组件的多个版本安装在不同的目录中,由它们自己的程序集清单来描述。这种技术对 EXE COM 服务器的支持有限,不能用于系统范围的组件,如 MDAC、MSXML 、DirectX 或 Internet Explorer。
在应用程序加载期间,Windows 加载程序会搜索程序集清单。如果存在,加载程序会将信息从程序集清单中添加到激活上下文。当 COM 类工厂尝试实例化一个类时,首先检查激活上下文,以查看是否可以找到 CLSID 的实现。仅当查找失败时,才会扫描注册表。
9.手动实例化 COM 对象
通过给定 DLL 文件的路径和对象的 GUID,也可以手动创建COM 对象。 这不需要在系统注册表中注册 DLL 或 GUID,并且不使用清单文件。
创建过程:COM DLL 暴露 了一个名为 DllGetClassObject 的函数。 调用 DllGetClassObject 函数(通过 GUID 和 IID_IClassFactory),将得到了一个 Factory 对象的实例,Factory 对象有一个 CreateInstance 方法,该方法可以创建给定接口 GUID 的对象的实例。
这与创建已注册 COM 组件的实例时内部使用的过程相同。
如果创建的 COM 对象使用通用 CoCreateInstance API 实例化另一个 COM 对象,它将尝试使用注册表或清单文件以通常的通用方式执行此操作。 但是它也可以创建内部对象(可能根本没有注册),并使用自己的私有知识向它们分发对接口的引用。
10.在进程和网络中的透明度
COM 对象可以在同一进程内(进程内)、跨进程边界(进程外)或远程通过网络 (DCOM) 透明地实例化和引用。 进程外和远程对象使用编组(marshalling)来序列化方法的调用并通过进程或网络边界返回值。 这种编组(marshalling)对客户端是不可见的,它像访问本地进程内对象一样访问对象。
11.线程化
【译者注:需要线程管理的场景是一个进程里有多个线程出现,线程管理的目的是实现线程安全。】
(1)在 COM 中,线程管理是通过一个称为单元(Apartment)的概念来解决的。
单元 是一个逻辑容器,收纳遵循相同线程访问规则的COM对象与COM线程(创建了COM对象的线程或者调用了COM对象的方法的线程)。单元本质上只是一个逻辑概念而非物理实体,没有句柄类型可以引用它,更没有可调用的API操纵它。
(2)共有三种类型的单元 :单线程单元 (STA)、多线程单元 (MTA) 和线程中立单元 (NA)。每个单元 代表一种机制,通过该机制可以跨多个线程同步对象的内部状态。
每个进程可以有:多个STA,至多有一个MTA,可以有一个NA。
(3)COM对象与单元
一个COM对象只能存在于一个单元 。COM对象一经创建就确定所属单元 ,并且直到销毁它一直存在于这个单元 。COM对象的单元类型写在Windows注册表相关条目中。
一个进程可以由多个 COM 对象组成,其中一些可能使用 STA,而另一些可能使用 MTA。
(4)线程与单元
一个COM线程从创建到结束都属于同一个单元 。COM线程只有两种模式:STA或MTA。线程必须通过调用CoInitializeEx()函数【Windows提供的API函数,为当前线程初始化COM库并设置并发模式 】,并且设定参数为COINIT_APARTMENTTHREADED或者COINIT_MULTITHREADED,来指明该线程的单元 模式。调用了CoInitializeEx()函数的线程即已进入单元 ,直到线程调用CoUninitialize()函数或者自身终止,才会离开单元 。
每个STA只能有一个线程。所有MTA性质的线程都属于MTA(即MTA有多个MTA性质的线程)。
COM为每个STA的线程自动创建了一个隐藏窗口,其Windows class是"OleMainThreadWndClass" 。跨单元 调用这个STA单元内的COM对象,实际上是向这个隐藏窗口发送了一条窗口消息,通过消息循环与分派,该窗口所在的线程收到这条窗口消息并调用相应的COM对象的接口方法。
(5)COM对象与线程
线程访问属于同一单元 的COM对象,直接执行方法调用而不需COM设施的辅助。线程跨单元边界去调用COM对象,传递的指针需要 marshalling。如果通过标准的COM的API来调用,可以自动完成安整。例如,把一个COM接口指针作为参数传递给另外一个单元的COM对象的proxy的情形。但如果软件编程者跨单元传递接口指针而没有使用标准COM机制,就需要手工完成安整(通过CoMarshalInterThreadInterfaceInStream函数)与反安整(通过CoGetInterfaceAndReleaseStream函数获取COM接口的proxy)。例如,把COM接口指针作为线程启动时的参数传递的情形。
(6)进程之间
跨进程的调用COM对象类似于同一进程内跨单元的调用COM对象。
(7)COM 对象的单元选择和线程的单元选择是在运行时确定的,不能更改。
单元类型 | 描述 |
STA |
单个线程专用于执行对象的方法。 在这样的机制中,来自单元之外的线程调用方法时,将被系统编组并自动排队(通过标准的 Windows 消息队列)。 因此,COM 运行时提供自动同步,以确保对象的每个方法调用执行完成之后,再调用另一个方法。 因此,开发人员无需担心线程锁定或竞争条件。【译者注:被STA线程创建的对象,只能由这个线程直接调用(Single Threaded Apartment的本义),其他线程对它的调用,必须通过操作系统的消息对列】 |
MTA |
COM 运行时不提供同步,并且允许多个线程同时调用 COM 对象。 因此,COM 对象需要执行自己的同步,以防止多个线程同时访问导致竞争条件。 即使是来自 STA 中的线程,对 MTA 对象的调用,也要被编组。 |
动态确定 |
在两个单元模式下,服务器在创建对象时自动选择 STA 或 MTA 以匹配调用线程的单元类型。 当 STA 线程访问 MTA 服务器时,这可用于避免封送处理开销。 |
NA(ThreadingModel=Neutral) |
一个没有任何分配线程的特殊单元。 当STA或MTA线程在同一个进程中调用NA对象时,调用线程会暂时离开自己的单元,直接在NA中执行代码,无需任何线程切换。 因此,可以将NA看作是对高效的单元之间方法调用的优化 . |
属于同一单元的线程和对象遵循相同的线程访问规则。 因此,在同一单元内进行的方法调用是直接执行的,无需来自 COM 的任何帮助。 跨单元的方法调用是通过编组(marshalling)实现的。 这需要使用代理和存根(stubs)。
四、安全
COM 和 ActiveX 组件在用户机器上作为本机代码运行,没有沙箱。因此,代码可以做什么几乎没有限制。因此,使用 Internet Explorer 在网页上嵌入 ActiveX 组件的先前做法确实会导致恶意软件感染问题。 Microsoft 早在 1996 年就意识到 ActiveX 的问题,当时 Charles Fitzgerald 说:“我们从未预先声明 ActiveX 本质上是安全的”。最近[何时?] 版本的 Internet Explorer 在安装 ActiveX 控件之前会提示用户,使用户能够禁止从用户不信任的站点安装控件。 ActiveX 控件使用数字签名进行签名以保证其真实性。也可以完全禁用 ActiveX 控件,或仅允许选定的几个控件。
对进程外 COM 服务器的透明支持仍然在进程隔离方面促进了软件安全。这对于将大型应用程序的子系统解耦为单独的进程非常有用。进程隔离限制了一个进程中的状态损坏对其他进程的完整性产生负面影响,因为它们仅通过严格定义的接口进行通信。因此,只有受影响的子系统需要重新启动才能重新获得有效状态。对于同一进程中的子系统,情况并非如此,其中一个子系统中的流氓指针可以随机破坏其他子系统。
五、批评
由于 COM 具有相当复杂的实现,程序员可能会被一些“管道”问题分散注意力。
1.消息泵
当 STA 初始化时,它会创建一个隐藏窗口,用于跨单元和跨进程间的消息路由。该窗口必须定期“抽取”其消息队列。这种构造被称为“消息泵”。在早期版本的 Windows 上,如果不这样做可能会导致系统范围的死锁。某些 Windows API 将 COM 作为其实现的一部分进行初始化,这会导致实现细节的“泄漏”,从而使这个问题变得复杂。
2.引用计数
如果循环引用两个或多个对象,COM 中的引用计数可能会导致问题。应用程序的设计必须考虑到这一点,以便对象不会成为孤立的存在。如果使用 COM“事件接收器”模型,引用计数的使用,也可能会导致对象成为孤立的存在。由于触发事件的对象需要对响应事件的对象的引用,因此后者的引用计数永远不会达到零。通常使用“带外终止技术(out-of-band)”或“拆分标识技术(split identity)”来破坏“引用的循环”。在带外终止技术中,对象公开一个方法,该方法在调用时强制它放弃对其他对象的引用,从而打破循环。在拆分标识技术中,单个的实现(a single implementation)公开两个分离的 COM 对象(也称为标识)。这会在 COM 对象之间创建弱引用,从而防止引用循环。
3.DLL 地狱
因为进程内 COM 组件是在 DLL 文件中实现的,并且注册只允许每个 CLSID 有一个版本,所以它们在某些情况下可能会受到“DLL 地狱”效应的影响。免注册 COM 功能消除了进程内组件的这个问题;但免注册 COM 不适用于进程外服务器。
本文译自:https://en.wikipedia.org/wiki/Component_Object_Model
部分内容来自:https://zh.wikipedia.org/wiki/组件对象模型