COM in Wine(1)——COM基本概念

提前声明!!!!!
声明:本文以及后续相关博客只是总结,大部分内容都是翻译MSDN、Wine官网、《Inside OLE2》以及《COM原理与应用》中的内容,也参考了很多网络上的博客和论文,但时间久远,无法一一列出(鞠躬)

1.COM概述

COM是一种组件对象模型(Component Object Model),它是一种平台无关、语言中立、位置透明、支持网络的中间件技术。在COM构架下,人们可以开发出各种各样的功能专一的组件,然后将它们按照需要组合起来,构成复杂的应用系统。

COM组件的优点还是显而易见的:

  • 可以将系统中的组件用新的替换掉,以便随时进行系统的升级和定制;
  • 可以在多个应用系统中重复利用同一个组件;
  • 可以方便的将应用系统扩展到网络环境下;
  • COM与语言,平台无关的特性使所有的程序员均可充分发挥自己的才智与专长编写组件模块。

COM组件都遵循着COM标准,COM标准包括规范和实现两大部分,规范部分定义了组件和组件之间通信的机制,这些规范不依赖于任何特定的语言和操作系统,只要按照该规范,任何语言都可使用。COM标准的实现部分是COM库,COM库为COM规范的具体实现提供了一些核心服务。

  一般而言,COM组件的使用过程都如下图所示:
在这里插入图片描述
  客户程序通过COM库与组件程序交互,COM库通过类厂来创建所需的COM对象,COM对象以COM接口的形式向客户程序提供服务,这是COM组件使用的基础过程。

2.COM基本概念

(1)COM对象及接口

COM不仅仅提供了组件之间的接口标准,它还引入了面向对象的思想。在COM标准中,对象是一个非常活跃的元素,经常把它称为COM对象。组件模块为COM对象提供了活动的空间,COM对象以接口的方式提供服务,这种接口被称为COM接口。一个组件程序可以包含多个COM对象,并且每个COM对象可以实现多个接口。

  COM组件、COM对象、COM接口的关系如下图:在这里插入图片描述
  COM组件是通过接口来提供服务的,所谓接口,实际上是包含一系列函数的数据结构,这些函数就是COM对象对外提供的功能的入口,通过该数据结构,客户程序可以调用组件对象提供的功能来获取服务。客户程序使用接口指针来调用接口的成员函数,COM接口的结构如下图所示:
在这里插入图片描述
  在COM中,IUnknown是一个特殊的接口,任何组件的任何接口都必须从IUnknown继承,接口定义如下:

IUnknown{
    virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0;
    virtual ULONG AddRef() = 0;
    virtual ULONG Release() = 0;
);

其中,接口方法QueryInterface负责接口查询,AddRef和Release负责生存期控制,这两者是IUnknown提供的重要特性。
客户程序只能通过接口与COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。

  在Windows系统平台上,一个COM组件或者是一个DLL(dynamic linking library,动态链接库)文件,或者是一个EXE(可执行程序)文件。当另外的组件或者普通程序(即组件的客户程序)调用组件的功能时,它首先创建一个COM对象或者通过其它途径获得COM对象,然后通过该对象所实现的COM接口调用它所提供的服务。当所有服务结束后,如果客户程序不再需要该COM对象,那么它应该释放掉对象所占有的资源,包括对象自身。

(2)类厂(Class Factory)

所谓类厂,顾名思义就是COM类的工厂,COM类是COM对象的基本定义,因此类厂实际上是创建COM对象的基地。

  COM库通过类厂来创建各种各样的COM对象,每一个COM类都有一个相应的类厂来创建该类的COM对象。类厂本身也是COM对象,所有的类厂都支持IClassFactory接口,接口定义如下:

IClassFactory : public IUnknown{
    virtual HRESULT CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject) = 0;
    virtual HRESULT LockServer(BOOL fLock) = 0;
);

其中,CreateInstance函数是类厂用于创建COM对象的成员函数。

  而类厂对象是由组件程序的导出函数DllGetClassObject创建的,函数原型如下:

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID  riid,LPVOID  *ppv);

其中,参数rclsid是需要创建对象的COM类的CLSID,通过该参数指定COM类,进而创建相应的类厂。

  CLSID也是该COM类的唯一标识,参数riid是COM接口的唯一标识IID,在Wine中,这些作为唯一标识的参数都是有着统一格式的UUID(也称为GUID)。

UUID是包含一组十六进制数字的字符串,它的文本表示形式是一个字符串,由8个十六进制数字和一个连字符组成,再由三个由连字符分隔的4个十六进制数字组成的组,再由一个连字符和12个十六进制数字组成。以下示例是有效的UUID字符串:ba209999-0c6c-11d2-97cf-00c04f8eea45

(3)COM库与类厂交互

在COM的实现中,客户程序是在运行时与组件程序建立连接的,连接的建立主要依靠COM库的参与。客户程序通过调用COM库来创建组件对象,首先将对象创建指令发送给COM库,COM库通过类厂创建COM对象,然后通过COM对象来获得组件程序提供的服务。

  在COM库中,提供了三个API函数来创建COM对象,分别为:函数CoCreateInstance、函数CoCreateInstanceEx以及函数CoGetClassObject。三者调用关系以及关键参数关系如下图所示:
在这里插入图片描述
  在Wine的具体实现中,函数CoCreateInstance实际上调用的是它的扩展函数CoCreateInstanceEx,它们两者都是被包装过的辅助函数,其内部也调用了函数CoGetClassObject,只是对外屏蔽了类厂的细节,即在函数内部完成对类厂的操作,对外不提供类厂的接口,只提供COM对象的接口。
  函数CoGetClassObject返回的是类厂对象的接口指针,所以,如果客户程序需要获取类厂对象或调用类厂的成员函数,那么可以使用函数CoGetClassObject来创建COM对象。而如果客户程序需要创建远程对象则可以使用函数CoCreateInstanceEx,否则,一般情况下,常用函数CoCreateInstance来创建COM对象。

(4)进程内/进程外组件

组件程序通常可以两种形式实现,分别为:DLL(Dynamic Link Library,动态链接库)和EXE(Executable File,可执行文件)。

进程内组件

如果组件程序是以DLL程序的形式实现的话,那么当客户程序在使用组件程序时,会将其装载到自己的进程中,因此两者就运行在同一进程空间中,这种组件程序被称为进程内组件。两者连接建立之后,客户程序可以直接调用组件程序提供的接口的成员函数。该方式使用效率高,因此应用广泛。

进程外组件

如果组件程序是以EXE程序的形式实现的话,那么当客户程序在使用组件程序时,组件程序会有属于自己的进程空间,因此两者就运行在不同的进程空间中,这种组件程序被称为进程外组件。在这种情况下,客户程序和组件程序之间需要跨越进程边界才能通信,这涉及到两个问题:

  • 一个进程调用另一个进程的函数。
  • 一个进程的参数传递到另一个进程。

  在COM中,使用的是LPC(Local Procedure Call,本地过程调用)和RPC(Remote Procedure Call,远程过程调用)来实现进程间的通信。LPC是同一机器上不同进程间的通信,RPC是不同机器不同进程间的通信,两者原理相同,但RPC更复杂,LPC可以看做是对RPC的一个优化实现。

(5)套间(Apartment)

在COM中,COM对象都是属于套间(Apartment)的。所谓套间,就是一个用来装COM对象的容器。一个套间内可以有多个COM对象,但是每个COM对象在某一时刻只能属于某一个套间。

  套间的提出是为了组件在多线程环境下安全执行,因为有跨线程调用同一个组件方法的状况存在。若该组件接口是线程安全的,则无须套间,否则需要套间的协助,就如窗口过程函数一样,窗口过程本身并不是线程安全的,但是消息队列的机制,保证了窗口过程总是在一个线程中执行,串行地处理消息。
  Windows的消息机制是通过窗口来实现的,那么一个线程要接收消息,也应该有一个窗口。COM API的设计者在它们的API函数中实现了一个隐藏的窗口。在我们调用CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)的时候,会生成这个窗口。该窗口是隐藏的,有了这个窗口,就可以支持消息机制,就有办法来实现对象中函数的逐一执行。这样当对象指针被传到其它线程的时候,从外部调用该对象的方法的时候,就会先发一个消息到原线程,而不再直接访问对象。
  这样设计的机制保证COM组件对象中的方法总是在一个线程中被调用(串行的),是线程安全的。

套间既不是进程,也不是线程。

  • 每个使用COM的进程都有一个或多个套间
  • 一个套间只能包含在某一个进程中
  • 每个套间可以有一个(STA)或多个(MTA)线程
  • 一个线程只能在某一个套间中执行
  • 每个套间可以包含多个COM对象

  COM规定,只有运行在对象的套间中的线程才能访问该对象。CoInitializeEx是一个创建套间的过程,我们使用CoInitializeEx(NULL, COINIT_MULTITHREADED)后,会创建一个MTA套间。CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)创建个STA套间。一个进程可以包含多个STA,但只能有一个MTA。 一个STA只能包含一个线程,一个MTA可以包含多个线程。

(6)代理(Proxy)和存根(Stub)

  套间就是一个和线程相关的上下文。拥有同一个上下文的线程中的COM对象可以相互调用,而不同上下文的COM对象,则需要通过代理(Proxy)来转发。
  COM中,只有进程外组件才会用到代理和存根,代理在客户进程中创建,存根在组件COM对象的进程中创建。如下图:
在这里插入图片描述
  在客户端发起一个请求时,它会首先将对象列集。如果用户没有继承IMarshal接口,那么将采用微软的标准列集。
  标准列集会为客户端创建一个代理,代理负责将客户端的请求打包,然后传递给COM底层实现的管道。同时,服务端会生成一个存根,用于解析代理发过来的请求,并且完成响应事宜。它们底层通过管道(Channel)通信,这个管道是微软提供的一个现成的机制。
  可以看出,代理只需要进行打包,在形式上,客户端拿到了一个代理,感觉就像拿到了那个在远程机器上的类一样,所以代理需要能够QueryInterface出目标对象的所有接口。在实现上,它表现为一个代理管理器(Proxy Manager),它拥有很多Interface Proxy,并且外部能够通过QueryInterface获取它们。
  假如目标对象实现了IA1接口和IA2接口,其代理的模型如下:
在这里插入图片描述
  客户端从CoCreateInstance拿到的代理,暴露出来了和目标对象一模一样的接口,每个接口都实现了IRpcProxyBuffer接口用于传递数据。当客户端调用IA1或者IA2中的接口时,对应地它将执行代理中的方法——也就是将消息打包,发送给管道。
  当消息发送给管道后,管道会将消息提供给位于服务端的存根,模型如下:
在这里插入图片描述
  存根由多个Interface Stub构成,每一个表示解析对应地一种接口类型。和代理不同的是,代理是一个聚合模型,它暴露出去的接口让人认为它就是所请求的对象,而Stub只是解析请求,并且转调真正的目标对象而已。

(7)列集(Marshal)和散集(Unmarshal)

  介绍列集之前,回顾一下进程外组件对象和客户程序之间的通信过程,如下图所示:
在这里插入图片描述

客户程序创建了组件对象之后,它通过接口指针调用的是组件对象的成员函数,但实际上,接口指针所指的是本进程中的代理对象,客户调用的是代理对象的成员函数,由代理对象通过跨进程的调用方法(LPC/RPC)与对象进程中的存根代码通信,存根代码再调用组件对象的成员函数。函数返回的是顺序刚好相反,由组件对象,经过存根代码和代理对象,最后返回给客户程序。所有跨进程的操作完全由代理对象和存根代码包揽了。

  在代理和存根交互的过程中,涉及到参数传输的问题,代理和存根需要完成对参数以及返回值的传递和解析,这个过程为列集和散集过程。

  • 列集,是指客户进程可以透明地调用另一进程中地对象成员函数的一种参数处理机制。代理对象用列集手段处理成员函数的参数,通过列集处理后得到一个数据包(数据流),然后通过一种跨进程的数据传输方法,比如共享内存方法,甚至是网络协议等。

  • 当数据包传输到对象进程后,存根代码用散集的方法把数据包参数解译出来,再用这些参数去调用组件对象,当组件对象成员函数返回后,存根代码又把返回值和输出参数列集成新的数据包,并把数据包传到客户进程中,代理对象接收到数据包后,把数据包解译出来再返回给客户函数,从而完成一次调用。

简而言之,列集和散集,其实就是序列化和反序列化。

Wine中列集的3种方式:

  • 类型库列集

    它可以列集与OLEAUTOMATION兼容的任何接口,意思是你的接口的返回值必须是HRESULT,所使用的参数的类型也应该是与C++的VARIANT结构兼容。

  • 通过创建Stub / proxy DLL

    这个DLL的源代码由MIDL产生。你必须在服务器和客户机上都注册这个DLL(这是标准的marshal 方式)使用此方法时,最好把stub / proxy代码编译作为一个独立的组件。

  • 自定义marshaling

    自定义marshal要求在你的组件中必须实现IMarshal接口。当COM需要marchal时,他首先通过QueryInterface看你是否支 持IMarshal接口,如果你实现了该接口,也就是说,由你控制了你的COM的所有参数和返回值的打包、解包的方法模式。

(8)类型库(Type Library)

编写IDL并使用C编译器将其编译为DLL之类的事情并不容易。因此,发明了type library。

Typelib类型库

Typelib类型库:typelib存储有关COM对象的信息:classid,对象支持的接口,这些接口上的方法,以及几乎所有能在IDL文件中找到的内容。

  类型库仅仅是描述每个元素的数据结构的集合。而且那些数据结构包含额外的嵌套数据结构描述它的内容。在这种情况下,库本身不是一个"对象",因为它本身不具有固有的行为。OLE提供包装对象,它的类型库有适当的接口,使你与底层的数据打交道,而却不需要了解这些具体的数据结构本身。因为接口是语言无关的,所以OLE提供的服务在这里对任何客户都有用,而且可以被用于描述任何对象,而不用管具体的实现。就像上面提到的,类型信息是语言无关的,而且构造了方法去合并头文件,帮助文件和输入库等内容。

类型库的元素

任何在类型库中的元素属于下面五类元素之一:

  • coclass:描述一个特定对象实现的接口或调度接口(用CLSID唯一标识)
  • interface:描述一个vtable接口(用IID唯一标识):详细指出成员函数的名字,函数的返回类型,函数参数的名字和类型
  • dispinterface:描述一个被OLE自动化使用的调度接口(用IID唯一标识),详细指出,名字,调度ID和方法和属性的类型(包括接口的方法的返回类型和参数名字和类型)
  • module:描述一个DLL模块(用一个DLL文件名唯一标识)包含输出函数的名字和序号,和全局变量
  • typedef:描述一个用户定义的数据结构,枚举或联合(用名字或可选的GUID标识)

处理类型信息的大部分工作是,找到想要的元素,处理那些元素中具体包含的信息。

interface和dispinterface:
interface:为C++提供,通过vptr调用接口中的方法
dispinterface:自动化接口,继承自IDispatch的接口,为一些宏语言和脚本语言提供接口,通过方法的名称调用,dispinterface中实现GetIDsOfNames方法从方法明到ID的映射,Invoke()去调用而已。

类型库和元素的属性

一个类型库或一个元素的属性被称作属性:

属性描述
Name描述类型库或元素的名字,不能含有空格和标点,例如KoalaTypeLibray,KoalaObject,或者IKoala.每个元素都有一个名字。
Guid(或者Uuid)库或元素的编程标识。库的GUID与其他的CLSID或IID不同。一个模块不能有GUID。对于一个typedef它是可选的。其他的元素都要有它。
Version库或元素的主要和次要版本。
DocString一小块文本,描述库或元素的目的。
HelpFileName一个帮助文件的名字(没有路径)。它包含更多的关于库的信息。一个库只能有一个帮助文件,所以这个属性仅用于一个库,不用与单个元素。
HelpContext在HelpFileName中的上下文标识,它指出库或元素在信息在哪里才能找到。
Lcid一个地区标识符,它指出类型库和元素使用的所有的字符串的单一的国家语言。一个类型库是为一个指定国家语言编写的,除了个别的函数参数能够接受一个地区用于自动适应语言功能,可能提供翻译或自动转换。
Flags指出库或元素额外部分的位。尽管有许多元素标志,一个库只有几种可能:根本没有标志,隐藏(不可通过用户接口浏览),或者约束(为了安全的约束)。
类型库列集器

COM系统包括一组COM对象,可以以编程方式遍历typelib的内容。

Type Library 生成:

  • 在IDL文件中加入library项来描述相应的组件
  • 用MIDL编译得到相应的tlb文件
  • 通过resource script 文件将类型库嵌入组件。

  实际上,类型库它们是作为COM开发的平行线(称为“ OLE自动化”)的一部分而发明的。类型库基本上是IDL二进制文件,除了有两种类型库格式之外,它们都不能完全表达IDL中可以表达的所有内容。无论如何,使用类型库(可以作为资源嵌入到DLL中),除了编译MIDL输出之外还有其他选择——可以将接口的ProxyStubClsId32注册表项设置为“type library marshaller”或“universal marshaller”的CLSID。这两个术语都被使用,但是在Wine的源代码中,它称为typelib marshaller。
  typelib marshaller可动态构造代理和存根对象。这是通过使用generic marshalling glue实现的,它从类型库中读取信息,并直接从堆栈中取出参数。CreateProxy方法实际上是根据拼接在一起的程序块构建vtable的,这些程序块将控制权传递给_xCall,然后进行marshal。可以在dlls / oleaut32 / tmarshal.c中看到这些。

(9)ActiveX Data Objects

  说ActiveX之前,先简单介绍一下OLE。所谓OLE(Object Linking and Embedding)是对象连接与嵌入技术。

OLE是封装了一些软件(对象)的库文件,这个库文件通常称为“部件”,它有几个特征:
(1)它是可运行代码 
(2)它是可被其它外部应用程序调用的代码
(3)外部程序可以重复调用库中的代码,通常称为代码重用

  当发展到网络时代的时候,OLE需要能够与Web浏览器交互,嵌入到网页中,随网页传送到客户的浏览器上,并在客户端执行。这个时候,OLE的基础技术也有了发展,就是我们常听说的COM(Component Object Model)。按照一般的升级命名原则,这时应该叫OLE 2.0,但微软给OLE改名了,它就是ActiveX。所以可以说,ActiveX其实就是OLE 2.0,或者是支持网页技术的OLE。

由于互联网本身具有安全问题,访问速度远低于本地访问速度等一些特殊性,ActiveX部件通常还有如下特征: 
(1)一般都提供“代码签名”或要求注册使用,以保证其安全性。
(2)占用内存尽可能小,效率(速度)尽可能高。但这也不是绝对的,随着网速的提升,很多ActiveX部件的制作要求也在下降。

  ADO是一种用来管理数据的数据对象,使客户端应用程序能够通过 OLE DB 驱动访问和操作在数据库服务器中的数据。
  ADO 支持用于建立基于客户端/服务器和 Web 的应用程序的主要功能。其主要优点是易于使用、高速度、低内存支出和占用磁盘空间较少。
  要使用ADO就要引用Microsoft ActiveX Data Objects 2.x Library。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
贝叶斯分类算法是一种基于贝叶斯定理的统计算法,常用于文本分类、垃圾邮件过滤和数据挖掘等任务中。在对wine数据集进行分类时,我们可以使用贝叶斯分类算法。 首先,我们需要了解wine数据集的特征和标签。根据数据集的描述,wine数据集包含了一些葡萄酒的化学分析结果作为特征,以及该葡萄酒所属的类别作为标签。这些特征可以包括酒精含量、苹果酸含量、灰分含量等。 贝叶斯分类算法的核心思想是基于训练集计算每个类别的先验概率和条件概率,然后使用贝叶斯定理来计算给定特征时,每个类别的后验概率,最终选择后验概率最大的类别作为预测结果。 为了使用贝叶斯分类算法对wine数据集进行分类,我们需要进行以下步骤: 1. 数据预处理:对原始数据进行清洗和处理,包括去除缺失值、标准化特征等。 2. 特征选择:根据具体问题的要求,选择合适的特征来训练模型,可以使用相关性分析等方法进行特征选择。 3. 训练模型:将数据集分成训练集和测试集,使用训练集来计算每个类别的先验概率和条件概率。 4. 预测分类:对测试集中的每个样本,根据贝叶斯定理计算该样本属于每个类别的后验概率,选择后验概率最大的类别作为预测结果。 5. 模型评估:使用测试集评估模型的性能,可以使用准确率、精确率、召回率等指标来评估模型的好坏。 贝叶斯分类算法的优点是简单、直观,能够处理多分类问题和高维数据。然而,贝叶斯分类算法也有一些限制,例如对特征之间的关联性要求较高,对输入的先验概率分布有一定假设等。 在应用贝叶斯分类算法对wine数据集进行分类时,我们需要根据具体情况选择适合的特征和合适的先验分布,对模型进行调优,以获得更好的分类结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值