郁闷阳光的专栏

欢迎朋友们经常来逛逛,一起交流

COM自动化,第一部分

 
在这部分和以后的内容中,我们将深入COM 自动化世界。希望用一个简要的专栏来研究这个题目。我们将谈论怎样进行自动化(IDispatch)调用和处理自动化对象需要做什么。然后,我们将讨论用于自动化的特殊COM数据类型和研究双重接口。

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

    自动化是使用标准COM接口IDispatch来存取对象的自动化接口。因此,我们说任何实现IDispatch的对象实现了自动化。

为什么要自动化?

最初开发自动化是作为一种应用程序(例如Word和Excel)用以把其功能显露给其他应用,包括脚本语言的方式。目的是提供一种简单方式来访问属性和调用方法,这种方式尽可能少的占用自动化客户的资源,并且不需要被访问对象的类型信息就可以进行调用的方法。

在C++头文件中描绘接口的类型信息决不是浪费时间,描绘方法的vtable偏移量也很重要,最困难的是,设置正确的C++堆栈框架以便正确的执行方法调用。对一个基于文本的解释语言所有这些尤其需要技巧。

如果每个脚本语言都不得不做这个机灵的程序,那么很少有能存取COM对象的了。使用自动化,对象就可以提供一个简单的自动化接口,这样脚本语言作者只需掌握IDispatch和几个COM应用程序接口就可以了。

Visual Basic的第一个32位版使用自动化存取OLE控件(现在叫ActiveX控件),他代替了16位的Visual Basic的VBX控件。Visual Basic仍然可以使用自动化存取一个控件的属性和方法,但是更近的版本也支持使用标准COM vtable接口。这次我们创建的例子将使用自动化接口。

脚本语言,例如Visual Basic for Applications、VBScript和J/Script,以独占模式使用自动化。所以如果你想要你的对象可以被脚本语言使用,你必须实现一个自动化接口。

对象和属性和方法,噢,我的上帝!
世界上关于自动化有三个主要概念。对象是最重要的概念。对象显露属性和方法。
 
图1. 自动化对象的属性和方法
把这个与更复杂的世界的COM观点对比,在这种观点中,是接口,而不是对象,是第一位的,而属性是不存在的,并且每个对象能有多个包含多个方法的接口。
 
图2. COM对象,接口,方法(包括没有标签的IUnknown)

方法与C++成员函数相似,而自动化的属性则与C++数据成员和实例数据(也叫属性)相似。注意接口没有独立的概念,每个对象有一个自动化接口。进一步注意到COM接口没有属性的概念,它们只有方法。(但是我们可以使用get/set方法对模拟属性。)

自动化对象怎样被创建?

创建一个自动化对象是一个简单的操作。这儿我将使用Visual Basic作为例子,但是在任何兼容自动化的语言中,方法基本上一样。

在Visual Basic,你应先创建一个对象变量:
Dim Beeper as Object
……接着设置它指向一个特殊的对象:
Set Beeper = CreateObject("BeepCntMod.BeepCnt")

在这个例子中我们创建了一个BeepCnt对象

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

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

我们早已知道我们将通过IDispatch COM接口访问自动化对象。所以DIM语句只显示集合至少需要的内存,因此Visual Basic能为我们即将创建的对象访问IDispatch指针。

CreateObject调用需要有一点技巧。首先,GUID在哪里?对象的CLSID没有GUID我们怎样创建它?

你可以重新调用,这样我们可以通过对象的ProgID引用对象类型。你也可以重新调用我们在注册表用ProgID作为键名注册的一个键。该键用一个CLSID作为子键。

COM提供一个叫CLSIDFromProgID的函数,它根据给出的ProgID查找CLSID。Visual Basic用我们传送到CreateObject的字符串调用这个函数。在这个例子中,Visual Basic将传送"BeepCntMod.BeepCnt"。CLSIDFromProgID查阅那个键和返回与它相关的CLSID。(顺便说一句,ProgID的第一部分是模块或应用程序名,第二部分是模块或应用程序中的对象名。)

在这一点上Visual Basic调用我们的老朋友CoCreateInstanceEx,传送CLSID和请求IDispatch接口。如果CoCreateInstanceEx成功,VB创建一个包含由CoCreateInstanceEx收到的IDispatch指针的对象变量,并且把它分配给我们的对象变量。

如果因任何原因创建失败:对象不存在,或者它没实现IDispatch,则对CreateObject的调用失败。

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

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

访问我们的对象的Visual Basic源代码可能像下面这样:
BC = Beeper.Count
Beeper.Count = 5
Beeper.Beep

这三个语句分别访问一个属性、设置一个属性和调用一个方法,都只使用了两种IDispatch方法:GetIDsOfNames和Invoke。IDispatch::GetIDsOfNames获得与方法或属性的文本名有关的整型ID。Visual Basic调用它发现"Beep(嘟嘟响)"对应ID 1和"Count(计数)"对应ID 2。当我们调用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结构的指针,就像下面定义的:

typedef struct FARSTRUCT tagDISPPARAMS{
// Pointer to array of arguments, named and unnamed
VARIANTARG FAR* rgvarg; 
// Array of Dispatch IDs of named arguments
DISPID FAR* rgdispidNamedArgs;
// Total number of arguments, named and unnamed
unsigned int cArgs;
// Number of named arguments.
unsigned int cNamedArgs;
} DISPPARAMS;

我们将不会使用名字化的参数,所以rgdispidNamedArgs将为空,而cNamedArgs将为零。
进行这个简单调用的代码为:
DISPID dispid;
OLECHAR * szMember = "Beep";
// No parameters, so no array
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};

// pdisp is an IDispatch pointer 
// to the Beeper object
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++客户来说容易使用,但是他总比直接调用来得慢。然而,如果服务器在另一个进程中或另一个机器上,设置(和执行)自动化调用的额外时间变的可以忽略。

获取一个属性的值

获取一个属性的值和调用一个方法很相似,唯一的不同是当调用返回时我们将注意返回值。
Visual Basic中获得一个属性的值的代码写为:
BC = Beeper.Count

调用Invoke的C++代码是:
VARIANT varResult;
// No parameters, so no array
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};

// dispid set by call to GetIDsOfNames
// (omitted for brevity)

hresult = pdisp->Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET,
&dispparamsNoArgs, &varResult, NULL, NULL);

// Property's value stored in varResult

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

注意Visual Basic语法不能区分获取属性和调用没有参数但有返回值的方法之间的区别。换句话说,不可能从语法上区别BC = Beeper.Count是存储一个叫Count的属性或是调用一个叫Count的方法。

作为一个结果,一些自动化客户将为一个属性访问传送两个标记,在一个属性获取的情况下,可能传送DISPATCH_PROPERTYGET | DISPATCH_METHOD因为它不能区别是否它在做一个属性存取或一个方法调用。自动化对象不得不通过根据dispid是指向一个属性或是一个方法来正确运行。

设置一个属性的值

设置一个属性与获取一个属性有三种区别: 
  • 属性的新值有一个参数。
  • 使用dispid DISPID_PROPERTYPUT为参数命名。
  • 返回的值参可以忽略。

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

为什么我们不得不进行这么详细的讨论?记住属性可以被参数化的。属性的值有一个特殊的名字使自动化对象容易认出哪个参数是属性的值。(好大夫真正想避免使用命名的参数,但是这是我们绝对需要的例子)

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

调用的C++代码是:

// parameter structure
DISPPARAMS dispparams;
// one-element array of parameter names
DISPID mydispid[1] = { DISP_PROPERTYPUT };
// one-element array of parameters
VARIANTARG vararg[1];

dispparams.rgvarg = vararg; // 1-element array
VariantInit(&rgvarg[0]);
dispparams.rgvarg[0].vt = VT_I4; // 32-bit integer
dispparams.rgvarg[0].iVal = 5; // here's our 5!
dispparams.rgdispidNamedArgs = mydispid; // name array
dispparams.cArgs = 1; // total args
dispparams.cNamedArgs = 1; // named args

// dispid set by call to GetIDsOfNames
// (omitted for brevity)

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-bit integer
dispparams.rgvarg[0].iVal = 5; // here's our 5!

……每个参数会被重复--一个语句初始化变量、一个语句设置它的类型、一个语句设定它的值。如果参数需被命名,在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("IBeepCount Interface"),
pointer_default(unique)
]
interface IBeepCount : IDispatch
{
[id(1), helpstring("method Beep")] HRESULT Beep();
[propget, id(2), helpstring("property Count")]
HRESULT Count([out, retval] long *pVal);
[propput, id(2), helpstring("property Count")]
HRESULT Count([in] long newVal);
};



注意双重接口的IID和接口是一个双重接口的事实在接口属性中被指定。同时也应该注意到接口派生自IDispatch,而非IUnknown。

方法属性中的IDs是自动化接口的dispids。注意实现属性的方法有一个特别的属性--在这个情况中,Count属性有两种方法,一个是获取值和另一个是设置它。当MIDL产生C++头文件时,这两种方法将被命名为get_Count和put_Count。 

顺便说一句,这个接口的vtable将有10个入口:三个为IUnknown,四个为IDispatch,和三个IBeepCount方法。 
 
类型库

类型库由IDL文件的库部分产生:

[
uuid(4F745303-3943-11D2-A2B5-00C04F8EE2AF),
version(1.0),
helpstring("BeepCnt 1.0 Type Library")
]
library BEEPCNTLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");

[
uuid(4F745310-3943-11D2-A2B5-00C04F8EE2AF),
helpstring("BeepCount Class")
]
coclass BeepCount
{
[default] interface IBeepCount;
};
};



库属性指定LIBID、版本和帮助字符串。该库引入标准类型库,接着为这个对象指定了coclass。

注意对象的CLSID被GUID在coclass属性表中指定。因为这是一个简单的对象,剩下的只是指定对象实现接口。(IUnknown和IDispatch被继承性所覆盖。)

MIDL在一个.tlb文件里产生类型库。你可以独立地使用这个文档,但是你通常不必这样。这文件在资源部分里被包含在DLL内,所以它建立到DLL中。

类型库的原始使用也很重要的:它们是一些像Visual Basic、Visual J++和Visual C++的智能指针(#输入)的工具,常用于描绘对象有什么方法和属性。

ATL怎样使用COM实现IDispatch

在这一点上,ATL的IDispatch实现非常简单。首先,在DllMain函数(当DLL载入时运行)中,Module对象的Init方法在其他事务中载入类型库。COM允许我们通过标准COM接口ITypeLib和ITypeInfo访问类型库。Init为对象的类型库ITypeInfo接口存取一个指针。

我们的好朋友ITypeInfo

ITypeInfo接口有两个叫做GetIDsOfNames和Invoke的方法。ITypeInfo::GetIDsOfNames运用类型库信息获取你发送的名字的正确的调度IDs。这个方法的实现是COM一个内置部分,所以你不必写它--只需提供类型库。

ITypeInfo::Invoke更有趣:它解析dispid和参数(等等),并且实际上通过你的双重接口的vtable调用适当的C++方法。它使用类型库信息决定参数将被转换成什么类型;怎样处理缺省的、可选择的和指定的参数;vtable偏移量是多少。然后它为参数建立一个堆栈框架和执行调用。

因为COM为你做所有这些工作,在IDispatchImpl里的IDispatch方法的ATL实现很简单:当对象被初始化时,它只是通过存储的ITypeInfo接口指针把调用传送到类型库。例如,IDispatchImpl::Invoke的编码很简单:

STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, 
VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
return _tih.Invoke((IDispatch*)this, dispidMember, riid, lcid,
wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
}


ITypeInfo指针将被贮存到tih去。我们要做的全部是把指针传送到传递给我们的对象和参数上,COM做其余的工作。不可能更简单了。 

好的,那么有什么收获呢?

但是COM实现被优化到最小的管理开销--因为在调用结束必要的全部工作(例如数组设置和调用堆栈设置,像刚刚叙述的),IDispatch调用在任何情况下都很慢。所以差别没有那么巨大--尤其考虑到如果你自己执行IDispatch::Invoke你不得不做相同数量的工作,除去挖掘类型库。

什么时候你的组件应该支持自动化?

那么什么时候你应该和不应该支持自动化?


明显地,如果你需要你的组件被脚本语言和其他只能自动化的客户应用,你必须支持自动化。那么如果你的组件将用于网页,被Windows脚本主机,或者Office应用程序和许多其他应用程序中Visual Basic for Applications使用,你必须支持自动化。
 
换句话说,如果你不因为你的组件而彻底的分割市场,你需要支持自动化。它和在ATL里单击正确按钮一样容易,所以没有太多的理由不去做它。

另一方面,有一些组件你将只能从vtable兼容的语言(Visual C++,Visual J++,Visual Basic,等等)中调用。这里有一个好例子,如果因为你的系统设计成为组件化(一个很好的东西),而你正在编写组件。像这样的组件经常不能用于它们被设计的系统外,因为它们是专用的--所以使用这些组件可能没有意义,比如说,在网页上。

支持自动化将耗费你什么?

一些与支持自动化相关的费用。

大小和速度


如果你提供一个双重接口,你的ATL组件的大小将有一个小的增长。通常这不重要,但是如果你在一个一个的计算字节,那就有必要看看两种方式产生组件的差别是否重要了。

IDispatch调用速度慢,但是如果你用一个双重接口,你可以让顾客选择是用快的(vtable)还是慢的(自动化)调用。所以性能不是一个真正的大问题。

但是如果涉及到编组(marshalling),就有一个微妙的性能问题会减慢你的调用,比如客户端和服务器在不同的进程(或者在不同的机器)中时。双重接口需要使用COM的通用编组器(marshaller),它是由类型库信息驱动。这个编组器(marshaller)比用MIDL更慢几分。然而,花费在编组器(marshaller)调用上的额外时间与花费在转换过程或者和网络上另一个机器通信上的时间相比是可以忽略的。

对于同一个进程中(DLL)服务器编组(marshalling)通常不是必需的,所以这个问题在大多数情况下不影响性能。然而,如果客户和对象在不同的"房间"里,编组(marshalling)也被用于进程内服务器,因为它们经常处在多线程的应用程序中。在进程内部跨房间的情况下,额外的编组(marshalling)时间是明显的。

没有C++灵活

较大的问题是处理自动化接口模型的和标准COM接口模型之间的差别。首先,对一个特别的对象通常只有唯一的自动化接口。在某些设计中可能存在问题。

更坏的是,你的参数和返回值受限于可以被放入变量(Variant)的那些类型。我们在下一专栏里将讨论变量,但是现在你应该知道变量支持大多数类型,比如字符串和数组。所以它包括了要用到的大部分类型。

但是自动化接口不支持结构。你可以不去考虑它,但它确实是一件痛苦的事。顺便说一句,那意味着连接数据结构也是困难的。

所以如果你需要传送高级C++数据结构,自动化不适合你。但是假如你无论如何都要支持自动化呢?

智能选择

如果你需要支持自动化,即使你有一些好理由不支持它,有一个办法:实现一个双重接口和一个等价的(在函数性方面)常规接口。

常规接口可以利用C++强大的数据构造,并且不需要通用的编组(marshaller)--事实上,你可以定制的编组器(marshaller)使性能达到最优。这将成为你的C和C++客户使用的接口。

每个人愿意使用双重接口。你可能不得不做一些充满想象力的工作来描绘出怎样把一个复杂的C++数据结构转换成一个可以放入变量(Variant)的类型,但是一旦你实现了它,你将拥有一个任何客户都能用的对象。

给一个机会!

如果你不知道是否你真的明白这一部分所谈论的内容,除非你试一下--所以给一个机会!

· 通过编写IDispatch::Invoke调用(和相关的代码),独立写一个应用程序调用一个简单的COM自动化组件。你不需要经常做,但是做一次它将会很好的帮助你--如果没有更好的理由帮你评价你的脚本语言为你做了什么。

· 独立写一个简单的自动化组件,执行IDispatch,包括可怕的Invoke方法。有两个简单的方法我们没有提及--检查文档找出有关的信息。如果你愿意,使用COM的标准化IDispatch实现改变你的解决方案。

这次,我们通过讨论自动化属性和方法如何被使用,创建了一个COM自动化的诀窍。在下一部分里,我们将谈论有关自动化数据类型和更深入的钻研双重接口。

阅读更多
个人分类: WINDOWS ATL/COM
想对作者说点什么? 我来说一句

COM原理与应用.rar

2009年09月03日 14.65MB 下载

atl开发指南[1][1].part01.rar

2012年09月19日 1.39MB 下载

atl开发指南[1][1].part06.rar

2012年09月19日 1.39MB 下载

atl开发指南[1][1].part03.rar

2012年09月19日 1.39MB 下载

atl开发指南[1][1].part02.rar

2012年09月19日 1.39MB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭