COM自动化.docx

上传人:b****4 文档编号:5465054 上传时间:2022-12-16 格式:DOCX 页数:14 大小:26.91KB
下载 相关 举报
COM自动化.docx_第1页
第1页 / 共14页
COM自动化.docx_第2页
第2页 / 共14页
COM自动化.docx_第3页
第3页 / 共14页
COM自动化.docx_第4页
第4页 / 共14页
COM自动化.docx_第5页
第5页 / 共14页
点击查看更多>>
下载资源
资源描述

COM自动化.docx

《COM自动化.docx》由会员分享,可在线阅读,更多相关《COM自动化.docx(14页珍藏版)》请在冰豆网上搜索。

COM自动化.docx

COM自动化

COM自动化,第一部分

--------------------------------------------------------------------------------

在这部分和以后的内容中,我们将深入COM自动化世界。

希望用一个简要的专栏来研究这个题目。

我们将谈论怎样进行自动化(IDispatch)调用和处理自动化对象需要做什么。

然后,我们将讨论用于自动化的特殊COM数据类型和研究双重接口。

自动化(从前叫做OLE自动化)是一个和迄今为止我们曾认为标准COMvtable接口完全不同的客户调用服务器方法。

自动化是使用标准COM接口IDispatch来存取对象的自动化接口。

因此,我们说任何实现IDispatch的对象实现了自动化。

为什么要自动化?

最初开发自动化是作为一种应用程序(例如Word和Excel)用以把其功能显露给其他应用,包括脚本语言的方式。

目的是提供一种简单方式来访问属性和调用方法,这种方式尽可能少的占用自动化客户的资源,并且不需要被访问对象的类型信息就可以进行调用的方法。

在C++头文件中描绘接口的类型信息决不是浪费时间,描绘方法的vtable偏移量也很重要,最困难的是,设置正确的C++堆栈框架以便正确的执行方法调用。

对一个基于文本的解释语言所有这些尤其需要技巧。

如果每个脚本语言都不得不做这个机灵的程序,那么很少有能存取COM对象的了。

使用自动化,对象就可以提供一个简单的自动化接口,这样脚本语言作者只需掌握IDispatch和几个COM应用程序接口就可以了。

VisualBasic的第一个32位版使用自动化存取OLE控件(现在叫ActiveX控件),他代替了16位的VisualBasic的VBX控件。

VisualBasic仍然可以使用自动化存取一个控件的属性和方法,但是更近的版本也支持使用标准COMvtable接口。

这次我们创建的例子将使用自动化接口。

脚本语言,例如VisualBasicforApplications、VBScript和J/Script,以独占模式使用自动化。

所以如果你想要你的对象可以被脚本语言使用,你必须实现一个自动化接口。

对象和属性和方法,噢,我的上帝!

世界上关于自动化有三个主要概念。

对象是最重要的概念。

对象显露属性和方法。

图1.自动化对象的属性和方法

把这个与更复杂的世界的COM观点对比,在这种观点中,是接口,而不是对象,是第一位的,而属性是不存在的,并且每个对象能有多个包含多个方法的接口。

图2.COM对象,接口,方法(包括没有标签的IUnknown)

方法与C++成员函数相似,而自动化的属性则与C++数据成员和实例数据(也叫属性)相似。

注意接口没有独立的概念,每个对象有一个自动化接口。

进一步注意到COM接口没有属性的概念,它们只有方法。

(但是我们可以使用get/set方法对模拟属性。

自动化对象怎样被创建?

创建一个自动化对象是一个简单的操作。

这儿我将使用VisualBasic作为例子,但是在任何兼容自动化的语言中,方法基本上一样。

在VisualBasic,你应先创建一个对象变量:

DimBeeperasObject

……接着设置它指向一个特殊的对象:

SetBeeper=CreateObject("BeepCntMod.BeepCnt")

在这个例子中我们创建了一个BeepCnt对象(见第一篇ATL文章,这篇文章将会在以后刊出-译者注)。

我们可以接着调用对象上的方法控制它的属性,就像我们不久将看到的。

但是首先,让我们讨论VisualBasic(或者任何自动化客户程序)在幕后真正做什么。

我们早已知道我们将通过IDispatchCOM接口访问自动化对象。

所以DIM语句只显示集合至少需要的内存,因此VisualBasic能为我们即将创建的对象访问IDispatch指针。

CreateObject调用需要有一点技巧。

首先,GUID在哪里?

对象的CLSID没有GUID我们怎样创建它?

你可以重新调用,这样我们可以通过对象的ProgID引用对象类型。

你也可以重新调用我们在注册表用ProgID作为键名注册的一个键。

该键用一个CLSID作为子键。

COM提供一个叫CLSIDFromProgID的函数,它根据给出的ProgID查找CLSID。

VisualBasic用我们传送到CreateObject的字符串调用这个函数。

在这个例子中,VisualBasic将传送"BeepCntMod.BeepCnt"。

CLSIDFromProgID查阅那个键和返回与它相关的CLSID。

(顺便说一句,ProgID的第一部分是模块或应用程序名,第二部分是模块或应用程序中的对象名。

在这一点上VisualBasic调用我们的老朋友CoCreateInstanceEx,传送CLSID和请求IDispatch接口。

如果CoCreateInstanceEx成功,VB创建一个包含由CoCreateInstanceEx收到的IDispatch指针的对象变量,并且把它分配给我们的对象变量。

如果因任何原因创建失败:

对象不存在,或者它没实现IDispatch,则对CreateObject的调用失败。

就像你所看到的,VisualBasic(或任何自动化客户)的开销是最小的,所有必须知道的是用两个简单的COM函数创建对象。

那么你怎样访问自动化属性和方法?

访问我们的对象的VisualBasic源代码可能像下面这样:

BC=Beeper.Count

Beeper.Count=5

Beeper.Beep

这三个语句分别访问一个属性、设置一个属性和调用一个方法,都只使用了两种IDispatch方法:

GetIDsOfNames和Invoke。

IDispatch:

:

GetIDsOfNames获得与方法或属性的文本名有关的整型ID。

VisualBasic调用它发现"Beep(嘟嘟响)"对应ID1和"Count(计数)"对应ID2。

当我们调用IDispatch:

:

Invoke时,我们需要这些叫做dispids的ID。

所有现行的自动化属性和方法访问都是通过调用IDispatch:

:

Invoke实现。

换句话说,你的自动化客户要访问自动化对象所必须知道是几个简单的COM调用。

如果你的执行语言不是C或C++,你可以为你的运行时间编写帮助者来做那些调用,所以从任何程序使用自动化是简单的。

也许很简单,但并不是不重要:

IDispatch:

:

Invoke接收一批参数,所有的参数必须被正确设置。

最重要的是:

一个叫dispid的整型ID,它指定要被访问的属性和方法(我们通过调用包含属性或方法名的字符串的GetIDsOfNames获得它)。

一个包含一列参数指针的结构。

(每个参数被存储到包含一个典型标记和一个叫variant的共用体的结构中。

一个包含指向属性(设置它、获得它、用一个引用设置它)或者方法(调用它)数列的指针的结构。

一个作为属性获取的或者是方法调用返回的返回值参数,也是一个变量。

噢,万一你想本地化方法、属性、名字化参数名或参数值,Invoke和GetIDsOfNames都接受一个本地ID。

Invoke也有几个其他参数可以把错误信息传递给自动化客户。

在这里我们将假定我们处在一个完美的世界,暂时不考虑它们。

变量(Variant)以16个字节存储。

前两个字节是一个标记,包含一个代表变量类型的数,其次的六个字节填满,最后的八个字节是变量的值。

值的格式取决于标记的值。

在C/C++中,我们使用一个共用体表示变量的值。

变量可以拥有大多数C++数据类型,包括指针、数组、串、日期和当前对象。

下一次我们将对包括变量COM数据类型做完整的处理。

那么为什么它比常规接口容易?

注意不一定是必须通过IDispatch:

:

Invoke进行一次调用:

没有偏移量--我们使用一个dispid,我们通过请求对象本身获得它。

没有C/C++参数列表和调用约定--我们使用变量的数组。

不用C/C++头文件告诉你上面的东西--但是典型库是可选择的。

你不能完全使用C/C++来进行分发。

很显然,这四个调用需要使用C/C++调用约定完成。

但这是你作为一个自动化客户唯一需要担心的地方。

那么,所有需要为调用做的是一个对象的IDispatch指针,你想访问的属性、方法名和参数的列表。

出于同样的原因,如果你想用脚本语言中编写COM对象,只有用IDispatch(噢,一种创建对象的方式)代替试图处理常规接口的无数变量的无数细节,这会使你的语言运行时间实现更方便。

COM接口和自动化间的不同

从前面的描述,你可以直接的看到自动化与COM接口间不同的几个方式:

自动化接口不必是永恒的,虽然在运行的时候你不必改变它们,因为客户能缓存dispids。

但是自动化接口的改变是平常的,尤其是从对象的一个版本到另一个版本时添加方法。

(如果你删除方法或改变参数,你可能破坏现有的客户代码。

自动化方法(和属性)可以获取包括不同类型的可变长度参数列表。

在运行时解析参数和执行任何必需的类型转变是IDispatch:

:

Invoke的事。

(如果不能实现参数的转换,对象的IDispatch:

:

Invoke实现将返回一个错误,HRESULT。

自动化方法和属性的访问是最后限制的;换句话说,确定要被访问的方法/属性被推迟到调用时才进行。

因为所有这个后期连接,自动化方法和属性是多形态的,在某种意义上更像Smalltalk而非C++;你可以存取任何对象上任何方法或属性,拒绝有害的调用是对象的责任。

例如,你可以在一个任意对象上调用一个Print方法。

任何对象调用Print方法大概都会打印它的值;没有任何对象会发生调用失败。

在C++中,你只能在那些定义了Print方法的类的对象上调用Print:

在编译时检查名字和参数,而不是运行时。

那么看一些例子怎么样?

作为一个快速例子,让我们着眼于刚展示的三个调用怎样使用。

调用一个方法

因为没有参数传递到Beep方法,也没有从Beep方法或返回值,让我们先调用它。

记住调用被写作:

Beeper.Beep

首先你需要知道一件事:

参数指针传递到Invoke实际上是一个指向一个DISPPARAMS结构的指针,就像下面定义的:

typedefstructFARSTRUCTtagDISPPARAMS{

//Pointertoarrayofarguments,namedandunnamed

VARIANTARGFAR*rgvarg;

//ArrayofDispatchIDsofnamedarguments

DISPIDFAR*rgdispidNamedArgs;

//Totalnumberofarguments,namedandunnamed

unsignedintcArgs;

//Numberofnamedarguments.

unsignedintcNamedArgs;

}DISPPARAMS;

我们将不会使用名字化的参数,所以rgdispidNamedArgs将为空,而cNamedArgs将为零。

进行这个简单调用的代码为:

DISPIDdispid;

OLECHAR*szMember="Beep";

//Noparameters,sonoarray

DISPPARAMSdispparamsNoArgs={NULL,NULL,0,0};

//pdispisanIDispatchpointer

//totheBeeperobject

hresult=pdisp->GetIDsOfNames(IID_NULL,&szMember,1,

LOCALE_USER_DEFAULT,&dispid);

hresult=pdisp->Invoke(

dispid,

IID_NULL,

LOCALE_USER_DEFAULT,

DISPATCH_METHOD,

&dispparamsNoArgs,NULL,NULL,NULL);

首先,我们通过调用GetIDsOfNames获得dispid。

在两次调用中,IID_NULL值是保留参数的值。

我们传递一个指向字符指针的数组的指针(在这个例子中,只有一个字符指针)和数组中指针的数目、地区(万一我们想要本地化名字)和dispid数组的一个指针(同样,这儿只有一个指针)。

当调用返回时,dispid将包含与"Beep"对应的dispid。

如果我们提供一种方式记住"Beep"的dispid,我们不用多次调用GetIDsOfNames。

一旦我们有dispid,我们可以调用Invoke。

你将注意即使参数列表是空的,我们仍不得不提供一个最小的DISPPARAMS结构。

也会注意我们不得不说明我们想通过DISPATCH_METHOD执行方法调用。

你也将注意到这比只调用Beep()更加复杂的多。

自动化对非C/C++客户来说容易使用,但是他总比直接调用来得慢。

然而,如果服务器在另一个进程中或另一个机器上,设置(和执行)自动化调用的额外时间变的可以忽略。

获取一个属性的值

获取一个属性的值和调用一个方法很相似,唯一的不同是当调用返回时我们将注意返回值。

VisualBasic中获得一个属性的值的代码写为:

BC=Beeper.Count

调用Invoke的C++代码是:

VARIANTvarResult;

//Noparameters,sonoarray

DISPPARAMSdispparamsNoArgs={NULL,NULL,0,0};

//dispidsetbycalltoGetIDsOfNames

//(omittedforbrevity)

hresult=pdisp->Invoke(

dispid,

IID_NULL,

LOCALE_USER_DEFAULT,

DISPATCH_PROPERTYGET,

&dispparamsNoArgs,&varResult,NULL,NULL);

//Property'svaluestoredinvarResult

通过传递参数获取参数化属性(或者"属性数组")很容易,就象当获取(和设置)属性时用到的一个索引或查找关键字。

注意VisualBasic语法不能区分获取属性和调用没有参数但有返回值的方法之间的区别。

换句话说,不可能从语法上区别BC=Beeper.Count是存储一个叫Count的属性或是调用一个叫Count的方法。

作为一个结果,一些自动化客户将为一个属性访问传送两个标记,在一个属性获取的情况下,可能传送DISPATCH_PROPERTYGET|DISPATCH_METHOD因为它不能区别是否它在做一个属性存取或一个方法调用。

自动化对象不得不通过根据dispid是指向一个属性或是一个方法来正确运行。

设置一个属性的值

设置一个属性与获取一个属性有三种区别:

属性的新值有一个参数。

使用dispidDISPID_PROPERTYPUT为参数命名。

返回的值参可以忽略。

设置一个属性与调用一个方法的不同之处在于属性设置的参数使用特殊的dispid命名。

为什么我们不得不进行这么详细的讨论?

记住属性可以被参数化的。

属性的值有一个特殊的名字使自动化对象容易认出哪个参数是属性的值。

(好大夫真正想避免使用命名的参数,但是这是我们绝对需要的例子)

回想在Beeper对象上设置Count属性的VB代码是:

Beeper.Count=5

调用的C++代码是:

//parameterstructure

DISPPARAMSdispparams;

//one-elementarrayofparameternames

DISPIDmydispid[1]={DISP_PROPERTYPUT};

//one-elementarrayofparameters

VARIANTARGvararg[1];

dispparams.rgvarg=vararg;//1-elementarray

VariantInit(&rgvarg[0]);

dispparams.rgvarg[0].vt=VT_I4;//32-bitinteger

dispparams.rgvarg[0].iVal=5;//here'sour5!

dispparams.rgdispidNamedArgs=mydispid;//namearray

dispparams.cArgs=1;//totalargs

dispparams.cNamedArgs=1;//namedargs

//dispidsetbycalltoGetIDsOfNames

//(omittedforbrevity)

hresult=pdisp->Invoke(

dispid,

IID_NULL,

LOCALE_USER_DEFAULT,

DISPATCH_PROPERTYPUT,

&dispparams,NULL,NULL,NULL);

所有这些代码是一个小调用!

对于实现自动化调用需要多少代码和为什么自动化可能很缓慢,你可能开始有一个概念了。

如果有更多的参数,这三行设置每个变量:

VariantInit(&rgvarg[0]);

dispparams.rgvarg[0].vt=VT_I4;//32-bitinteger

dispparams.rgvarg[0].iVal=5;//here'sour5!

……每个参数会被重复--一个语句初始化变量、一个语句设置它的类型、一个语句设定它的值。

如果参数需被命名,在rgdispidNamedArgs中有一个附加的语句设置每个参数名。

并且参数的大小应被正确设置。

所以自动化是用代码的尺寸作为代价实现了灵活调用。

并且它还放弃一件事情:

你唯一能传递的参数是那些能在变量(variant)中被描述的参数。

而最大的损失是你不能使用变量描述结构。

我们不久将讨论变量。

如果你想知道更多关于自动化参数传递的信息,查找你最喜欢的COM参考书。

准备写你自己的IDispatch:

:

Invoke调用?

阅读本部分……

除非你正在编写你自己的脚本语言或直接调用IDispatch:

:

Invoke,否则你不会遇到我们将要讨论的问题,对于一个C++客户来讲是一个纯自动化对象。

在阅读关于可选择的自变量时,会发现一个令人迷惑的情形:

现在对IDispatch:

:

Invoke的COM文档中,你不得不为每个遗漏的没命名的变量传送带有VT_ERROR标记的变量。

如果你跳过一个没命名的自变量,就像在object.method(a,,c)中,是明显正确的。

但是如果可选择的自变量是在最后呢?

脚本语言能否根本不使用类型库就知道要传递多少哑元自变量?

答案:

不能。

如果你最后想省略没命名的可选择的自变量,你可以省略它们。

使用实现了IDispatch的COM对象将会通过向在IDL中指定的双重接口函数提供自变量来正确处理这种情况。

但是有一个gotcha:

如果你在调用的对象本身实现了IDispatch:

:

Invoke本身,而不依赖COM,它可以被写入,所以它假设所有可选择的和缺省值的自变量为哑元自变量,--所以如果你能获取类型库信息,你应该可以确定参数的数目,,因此你能提供哑元自变量。

自动化:

服务器端

关于怎样调用方法、设置和获取属性、存取返回值和传送参数,我们现在看到的比我们曾经想知道的还要多。

自动化对象怎样处理所有这些问题?

简而言之,对象通过实现IDispatch:

:

Invoke(当然还有IDispatch其他部分,包括GetIDsOfNames)处理所有这些问题。

但是让我们假定你正在实现这个对象。

你怎样实现这些方法?

实现IDispatch困难的方式

如果你的方法没有任何参数(或者可能只有一个参数)和没有参数化的属性,你自己实现IDispatch:

:

Invoke就非常简单。

全部需要做的是一个开关语句,它为每个dispid和类型的组合调用正确的函数。

(你也可以使用一个调用表。

)你将不得不把参数转换为你需要的类型,但是调用VariantChangeType可以很容易的从任何类型转换变量到你需要的任何类型。

(如果转换失败,就返回一个错误给调用者。

但是可以想象如果你有多个参数的混乱状况。

首先,未命名的参数在数组中是颠倒次序的。

如果他们被命名,顺序由数组的dispids决定,意味着你将不得不把它挑选出。

但是等一下--那不是全部--你可能需要支持可选择的参数和带有缺省值可选择的参数。

给出所有的这些的描绘是一巨大的混乱。

它不仅仅困难--你很可能在做它时产生错误。

如果你去做,在你能处理的调用上你将多半以对一个你本来能处理的调用给出一个错误而结束---这不是在你的用户当中鼓舞信心的一种伟大的方式。

有比较容易的方式吗?

你可以打赌一定有。

不要用困难方式做它。

让COM帮你做它!

如果你的服务器遇到两个相对简单的要求,你能利用COM的内建的IDispatch的实现。

要求是:

·你的自动控制接口必须是一个双重接口(作为ATL产生的),不是一个纯粹的dispinterface。

 

·你必须产生和利用一个类型库告诉COM是哪些方法和属性。

ATL甚至不支持引入的纯disp接口,所以你的选择是双重的(包括自动控制)和自定义接口。

一个双重接口和等价的常规接口之间主要的差别是双重接口派生自IDispatch而不是IUnknown。

这意味着除了QueryInterface、Release和AddRef外,双重接口也必须实现所有的IDispatch方法(包括GetIDsOfNames和Invoke)。

正如ATL为你提供了IUnknown方法的实现,它也提供了IDispatch方法的实现。

双重接口的IDL

双重接口的IDL看起来非常像常规接口的IDL。

对BeepCnt对象,IbeepCnt接口的IDL是:

[

object,

uuid(4F74530F-3943-11D2-A2B5-00C04F8EE2AF),

dual,

helpstring("IBeepCountInterface"),

pointer_default(unique)

]

interfaceIBeepCount:

IDispatch

{

[id

(1),helpstring("methodBeep")]HRESULTBeep();

[propget,id

(2),helpstring("propertyCount")]

HRESULTCount([out,retval]long*pVal);

[propput,id

(2),helpstring("propertyCount")]

HRESULTCount([in]longnewVal);

};

注意双重接口的IID和接口是一个双重接口的事实在接口属性中被指定。

同时也应该注意到接口派生自IDispatch,而非IUnknown。

方法属性中的IDs是自动化接口的dispids。

注意实现属性的方法有一个特别的属性--在这个情况中,Count属性有两种方法,一个是获取值和另一个是设置它。

当MIDL产生C++头文件时,这两种方法将被命名为get_Cou

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 解决方案 > 学习计划

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1