组件技术的本质COM理论知识分析
组件技术的本质COM理论知识分析
谈组件技术
————组件技术的本质COM理论知识
前记:本来我已经将组件的实际操作ocx写完了,很长时间了,总是不敢放上来,怕大家认为太小儿科,而且,有的朋友也提醒我,写的东西太过于累赘,不是很简明易懂就能揭示核心问题,而ocx也有此方面的嫌疑,所以没有放上来。好了,继续我们今天的讨论,希望大家看后都有自己的见解。
也许您对这个标题有些疑问,组件技术的本质是COM吗?
申明一点,此处所说的组件技术都是就windows平台而言的,那么在windows平台下,组件的本质是什么?无可非议,COM!虽然,DCOM、MTS、COM+、甚至Not Net都已经常的被挂在了程序员的嘴边,可是本质是什么?就是COM。暂且不谈我为何要这样说,或许在下边的阐述中,您就会对这个问题自然而然的知道答案。
在介绍COM(Component Object Model ,组件对象模型)的时候,我们会省略一些细节,因为我要可以将COM的很多细节都写了出来的话,我想我们其码要谈很长时间,再者,作者在此也不敢说自己已经可以完全的驾驭COM。但会尽力的在本篇文章和以后的文章中对COM作一个多方位的介绍,力争看过文章的朋友都可以对COM有一个明确的认识,并且可以独立的完成COM组件的编写。让我们继续。
如上图所示,它就是一个完整的COM组件图,其实可以扩展到任何一个组件中,它们的关系,也如上图所示,包含于被包含的关系。其中,CoClass是正真的封装接口的部分, 我们根据上图所示进行一步一步的分析,实际上,每一个Coclass都可以是一个COM对象,而每个CoClass又可以实现多个Interface,Interface在之前的文章中已经给以了介绍,那么我们此处标识的COM接口和Interface有什么区别呢?在一定的程度上而言,我们所标识的COM接口和Interface可以认为是一个概念,只是此处为了更能明确的划分出它们的每一个细节才这样做的,希望看到这篇文章的朋友不要混淆。一位朋友说的相当好,我们所谓的OOP就是源码级上将用户的操作上进行了一定的规范,而组件是从底层,二进制上对用户的操作进行了一定的规范,所以组件才能有抹杀语言的区别!但无论是作为一个小型的COM组件还是一个大型的COM组件,它都要遵守COM规范来编写,COM组件是以Win32动态连接库(DLL)或是以可拨行文件(Exe)的形式而存在,每一个COM组件都是一些二进制可执行文件。作为一个组件,必须要作到以下的几点:
它必须以给其它的客户端提供服务的形式而存在,当然,它也可以获取其它的组件的服务。
COM组件可以动态的插入或卸出应用
COM组件必须是动态链接的
COM组件必须隐藏(封装)其内部实现细节
COM组件必须将其实现的语言隐藏
COM组件必须以二进制的形式发布
COM组件必须可以在不妨碍已有用户的情况下被升级
COM组件可以透明的在网络上被重新分配位置
COM组件按照一种标准的方式来宣布它们的存在
……
它既然是以提供服务的形式而存在,并且是完全可以脱离物理机的限制,那么它是如何被各个客户端所认识的呢?与接口类似,每个COM对象也有一个128位的GUID来标识,称为CLSID(Class Identifier,类标识符或类ID),并且,它也是全球唯一的,可以结合接口GUID进行理解。根据COM组件识意图,我们可以看出一个COM组件可以包含多个COM对象,而这些COM对象是如何联系的呢?我们是否可以通过对象A而去访问对象B呢?从理论上而言,是不应该的,甚至一个COM组件只包含一个COM对象,COM对象之间是互不相关的,但是在实际的操作中可以吗?当然可以,你会在后边看到相关的实例的。每一个COM对象作为一个黑盒子,它的内部都有什么呢?就是对接口的实现!通过实现接口来封装逻辑规则,这也是COM的本质!所以,在COM中,接口就是一切。我们可以说脱离接口的COM将不会存在,而没有实现接口的COM是没有任何意义的,对于我们来说,COM组件、对象就是一组接口的集合,只可以通过接口和COM打交道,没有任何接口访问权限的用户,其COM组件对它是没有丝毫的用处的。这就是封装的体现。而在上两篇文章中我们介绍了接口,此处将不花费过多的笔墨进行阐述。
OK,在你对COM组件有了这些认识之后,我们现在就可以进行COM组件的进一步分步的讨论,从其最细节的地方来进行讨论。(对于COM其它的一些知识,如:IMarshal,代理、存根DLL等相关知识会在以后的文章进行专门的介绍)
COM对象 (COM Object)
COM对象?如何理解COM对象?他有什么东西?
(在此处,仅以Object Pascal对本篇文章进行阐述)。COM对象是接口的集合没错,但是COM对象是如何实现、驾驭这些接口的呢?其实,我们可以在ComObj单元中看到COM的很多相关类,此处我们将以TComObject为例,以下代码摘自Delphi6
TComObject = class(TObject, IUnknown, ISupportErrorInfo)
private
FController: Pointer;
FFactory: TComObjectFactory;
FNonCountedObject: Boolean;
FRefCount: Integer;
FServerExceptionHandler: IServerExceptionHandler;
function GetController: IUnknown;
protected
{ IUnknown }
function IUnknown.QueryInterface = ObjQueryInterface;
function IUnknown._AddRef = ObjAddRef;
function IUnknown._Release = ObjRelease;
{ IUnknown methods for other interfaces }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
{ ISupportErrorInfo }
function InterfaceSupportsErrorInfo(const iid: TIID): HResult; stdcall;
public
constructor Create;
constructor CreateAggregated(const Controller: IUnknown);
constructor CreateFromFactory(Factory: TComObjectFactory;
const Controller: IUnknown);
destructor Destroy; override;
procedure Initialize; virtual;
function ObjAddRef: Integer; virtual; stdcall;
function ObjQueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
function ObjRelease: Integer; virtual; stdcall;
{$IFDEF MSWINDOWS}
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; override;
{$ENDIF}
property Controller: IUnknown read GetController;
property Factory: TComObjectFactory read FFactory;
property RefCount: Integer read FRefCount;
property ServerExceptionHandler: IServerExceptionHandler
read FServerExceptionHandler write FServerExceptionHandler;
end;
TComObject是直接从TObject中继承而来的,而且实现了默认接口IUnKnown和ISupportErrorInfo,不同于我们之间说的TinterfaceObjected的是,它实现了COM对象的必要的功能,如function IUnknown._AddRef = ObjAddRef;是从_AddRef的引用。HResult 是一个特殊的返回值,它是ISupportErrorInfo 的产物,用来标识函数调用的世功或是失败。如下:
ISupportErrorInfo = interface(IUnknown)
['{DF0B3D60-548F-101B-8E65-08002B2BD119}']
function InterfaceSupportsErrorInfo(const iid: TIID): HResult; stdcall;
end;
其实现过程:
function TComObject.InterfaceSupportsErrorInfo(const iid: TIID): HResult;
begin
if GetInterfaceEntry(iid) <> nil then
Result := S_OK else
Result := S_FALSE;
end;
此处对于另外一个用来处理检测调用失败的处理方法的过程:OleCheck进行说明,见其代码如下:
procedure OleCheck(Result: HResult);
begin
if not Succeeded(Result) then OleError(Result);
end;
可以很明显的看的出,它是专门用来处理返回HResult 的函数,因此,我们在程序中可以利用它来调用返回HResult的COM函数。
但到底什么是COM对象呢?说了这么多,似乎还没有进入正体,已下将对COM对象给于详细的解释,COM对象就是上边的CoClass!为什么要这么说呢?其实COM就是实现了一组接口给用户提供服务的载体,如示意图所示COM对象就是要用户指通过访问其实现的接口来得到服务,那么COM对象有什么必要的因素?就是Interfaces(是一组,至少应该包括两个Interface),Implementation Interface的CoClass,而COM Interface实际是一组虚拟函数做成的,他是要被CoClass Implenentation的,最终我们访问Interface 的方法时就上就是访问CoClass 实现的方法,此处我们用OO的方式来进行讨论!一个CoClass有多个Interface,而又可以有多个CoClass来Implentation一个Interface,这象不象多态?每个Interface 的方法仅仅是一个定义!它留下了很大的空间去让Implentation CoClass去发挥,每个Client没有办法知道Interface’s Methods的精确实现过程,结合上边的每个Interface 可以被不同的CoClass 所 Implentation,这就是一种多态性的体现,同时也是封装的最好说明,应为我们知道,如果每个Interface在所Implentation CoClass如果是相同的话,其就没有必要被多个CoClass Implentation。这就说明了同样的Interface在不同的CoClass中的服务是不一样的!OK,说到这儿,您可以说出COM对象的必要因素吗?Interface/CoClass,就这两个!有了这两个因素,就可以组成一个COM对象。Interface用Guid来标示,而COM对象用Class ID来标示;
我们知道这些够吗?当然,离跨进COM大门还差很大一段距离,请继续往下看。
1:Implentation Interface.
如何让CoClass去实现Interface?这个问题看上很简单,但真的简单吗?看完你就会知道。
CoClass就是要实现Interface的各个virtual Method,如:TmyClass = Class(Object,IMyInterface),这就是了,但我们要知道我们做什么?为什么要这样做?可不可以用另外的方式去做?如上边Object时做什么的?我们可以用Tcomponent吗?或是别的呢?答案是肯定的,我们完全可以写成TmyClass=(TComboBox,IMyInterface),这就有一个效率的问题,在上边说了TcomObj提供了COM对象实现的最基本的功能,可应该知道可以用其它的方式去实现!
2:COM对象是Exe?是DLL?
是的,COM对象/COM组件最终是Exe or DLL,但是如果因为这样我们就说一个COM对象就是Exe or DLL的话未免有些太勉强了,可以相比一下,前边说了COM组件的必要功能:COM组件可以动态的插入或卸出应用,可是一个Exe or DLL是否可以这样做?动态的插入或卸出?不可以!他要编译,但COM对象用吗?不用,COM对象是以二进制的形式发布的,所以他不需要重新的编译!我们可以说COM对象真正意义上的实现了二进制代码的重用而不是源代码的重用!而他和DLL的对比更明显,虽然DLL也可以是语言无关性的,但你真的或这样做吗?你会尝试着用Delphi调用别人用C写成的DLL吗?首先我不会那样去做,我不会尝试由于调用约定不匹配而导致的 DLL 失败。而最重要的就是您是如何更新他们?如果当您更新对象时要向其中添加任何虚函数,那么您就会像浑身插满软管的病人一样动弹不得。您可能会认为在对象的末尾添加新函数不会出问题,但是实际上并非如此:这样会将所有从您的对象派生的对象的虚函数表入口平移。并且,因为调用虚函数需要使用虚函数表中的固定偏移,以便调用正确的函数,所以您不能对虚函数表进行任何更改——至少不重新编译每个有关的程序(这些程序使用您的对象或任何从您的对象派生的对象),就不能进行更改。很明显,每次更新您的对象时都重新编译全世界,不是个好主意。最后(也是最重要的),更新 DLL 简直就是一场恶梦,因为您处于两难境地,两种选择都很令人倒胃口:要么通过覆盖 DLL 来“就地”更新该 DLL,要么重新命名一个新的版本。就地更新 DLL 很糟糕:即使您保持接口的统一,DLL 的某些用户程序也会被破坏,这样的几率很高!
好了,我们应该说一说如何用COM对象了,这才是我们真正想要的,之所以用COM就是希望它可以为我们服务,那么如何达到这个想法呢?
在此我不会花费太多的笔墨来进行描述的,如果有兴趣的话,可参考下一篇文章。
我们知道,可以以LPC(Local procedcdure Call) or RPC (Remote Procedure Call)的方式去和COM组件进行通信,但是无论以那种方式,它们都是如何去访问的?这不得不说到存根DLL和代理DLL了,如下图所示:
从上图可以看出,以LPC的方式也罢,用RPC的方式也罢,作为一个进程外访问机制,其实它并非是Client 直接Call Server!实际上,Client 是通过本身的Proxy DLL来访问无程的,而Server端也并非是直接将其对象、接口以服务的形式提供给Client的,它是通过Stub DLL和Client 的 DLL进行通信,这样一来,Stub DLL将Client 的 DLL的请求反馈给正真的Server,于是Server将接口提供给Stub DLL,此时Stub DLL再将这个接口回传给Proxy DLL,那么此时的Client才可以正真的通过Proxy DLL看到Server的接口、服务!但是进程内的就不一样了,因为他们同驻于一个进程空间,它们是通过一个Vtable来进行信息的通讯的,而且进程内的服务甚至可以于Client共享同一全局变量!但是,无论以LPC的方式也罢,用RPC的方式也罢,可是你倒低是怎么知道Remote Server有那些服务的?我不可能凭空的去想象一个Tcomponents Server都提供了什么功能吧,这就是说Remote Server要注册,将注册信息留在了注册表中,Client访问其Remote Server的时候,只要知道其注册表里有什么服务,我直接调用注册表里所示的服务,而如何实现的细节就如上边所说的Proxy DLL和Stub DLL来协调完成!
这篇文章暂且写到这儿,下一篇将对Proxy DLL and Stub DLL进行详细的说明,以及如何创建Componets Server,如何操作都进行阐述。但必须明确一点的是,你一定要将接口理解,否则将无法进行下去!
后注:是的,此处还没有对为什么COM是组件技术的本质疑问进行解释,因为没有到时间,之后的文章会给您一个满意的解释的