原文:http://www.codeproject.com/Articles/14037/COM-in-plain-C-Part-3
用C编写COM集合
下载例程-174kb
内容
有时我们可能需要维护一个元素列表。例如,我们可能设计了一个COM组件用于操作我们设计的PCI硬件板卡,并且用户可以在一台计算机上安装几个这种板卡,我们要让我们的组件能控制所有可用的板卡,允许应用程序获得每块板卡的信息,同时可单独访问每块板卡。
换句话说,当我们组件运行时,它需要查询系统中的板卡,然后把所有可用板卡的信息的列表给应用程序。为了这点,假设我们只需要每块可用板卡的“名字”,系统中第一块板卡名字可能是“Port 1”,第二块板卡名字是“Port 2”,等等。
因为我们事先不知道系统中到底有多少块板卡,所以最好的方法是创建一个结构,它们可以链接同类其它成员成一个链表。例如我们可以定义一个IENUMITEM结构来保存板卡信息:
typedef struct _IENUMITEM { // To link several IENUMITEMs into a list. // 把多个IENUMITEM链接到一个列表中 struct _IENUMITEM *next; // This item's value (ie, its port name). // 元素值(也就是它的端口名) char *value; }IENUMITEM;
如果我们有三个端口,我们的IENUMITEM链表看起来可以是这样(我们通常应该通过GlobalAlloc分配IENUMITEM,但为了省事我在下面把它们申明为静态的并初始化它们):
IENUMITEM Port1 = {&Port2, "Port 1"}; IENUMITEM Port2 = {&Port3, "Port 2"}; IENUMITEM Port3 = {0, "Port 3"};
在COM术语中,我们把一组相关元素叫做“集合(collection)”。所以上面三个元素的链表就是我们的集合。
但我们上面的IENUMITEM存在一个问题,它有一个char*成员,它不是自动化兼容类型。我们可以把它改为BSTR,这样它就是自动化兼容类型了。更好一点,我们可以放一个VARIANT在IENUMITEM中,这样做的优点是构造了一个通用的IENUMITEM(也就是说它可以存储各种自动化数据类型数据)。像我们稍后看到的,我们必须用VARIANT来返回元素的值给应用程序。因为我们总是要处理VARIANT类型数据,所以我们就把它定义为VARIANT存储在我们的IENUMITEM中。下面是IENUMITEM的新定义:
typedef struct _IENUMITEM { struct _IENUMITEM *next; VARIANT *value; }IENUMITEM;
下面是我们如何分配一个实例并把它的值设置为“Port 1”(忽略错误检查):
IENUMITEM *enumItem; enumItem = (IENUMITEM *)GlobalAlloc(GMEM_FIXED, sizeof(IENUMITEM)); enumItem->next = 0; enumItem->value.vt = VT_BSTR; enumItem->value.bstrVal = SysAllocString(L"Port 1");
记得脚本语言没有指针概念。这些语言本身不可能遍历上面的列表,因为每个IENUMITEM的第一个成员是指向下一个IENUMITEM的指针。所以我们需要提供一个对象来帮助应用程序遍历这个列表,获取每个元素的值。
因为微软应Visual Basic程序员提出这个对象,所以他们选择了它是基于IDispatch的。换句话说,我们对象的虚表必须像所有COM对象那样以三个IUnknown函数(QueryInterface、AddRef和Release)开始,后面必须紧跟四个标准的IDispatch函数(GetTypeInfoCount、GetTypeInfo、GetIDsOfName和Invoke)。我们的对象还必须有三个函数。在我们IDL文件中,当我们定义我们要创建的这个对象的虚表(也就是接口)时,我们必须把这三个额外的函数命名为Count、Item和_NewEnum。在我们的这个对象的实际虚表中,我们可以使用其它的名字来命名这些指针(尽管我坚持使用了这几个名字)。为什么呢?因为永远不会有人直接调用它们,甚至永远不会有人知道我们会添加这三个函数到我们的虚表中。这些函数只能通过我们对象的Invoke函数来调用-即使你使用像可以直接调用它们的C一类的语言,因为是微软的Visual Basic程序员设计它来提供给更多有影响的语言。为了让Visual Basic程序员可以使用它我们要付出极大的代价。此外在我们的IDL文件中,我们必须分配一个ID为DISPID_VALUE给Item函数,和DISPID_NEWENUM给_NewEnum函数。我们必须这么做。我们可以选择一个正数分配给Count函数做ID。似乎有点逻辑混乱?不是吗?记住-它是Visual Basic程序员设计的。
为了在我们的IDL文件(也就是类型库)中使用,我们还需要给这个对象的虚表生成一个新的GUID。
让我们看一下这个对象的定义。我们可以用我们喜欢的名字给它命名。我选择用ICollection给它命名:
// 我们的ICollection虚表的GUID // {F69902B1-20A0-4e99-97ED-CD671AA87B5C} DEFINE_GUID(IID_ICollection, 0xf69902b1, 0x20a0, 0x4e99, 0x97, 0xed, 0xcd, 0x67, 0x1a, 0xa8,0x7b, 0x5c); // 我们的ICollection的Vtable #undef INTERFACE #define INTERFACE ICollection DECLARE_INTERFACE_ (INTERFACE, IDispatch) { // IUnkown函数 STDMETHOD (QueryInterface) (THIS_REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS)PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; // IDispatch函数 STDMETHOD_ (ULONG, GetTypeInfoCount) (THIS_ UINT *) PURE; STDMETHOD_ (ULONG, GetTypeInfo) (THIS_ UINT, LCID, ITypeInfo **) PURE; STDMETHOD_ (ULONG, GetIDsOfNames) (THIS_ REFIID, LPOLESTR *, UINT, LCID,DISPID *) PURE; STDMETHOD_ (ULONG, Invoke) (THIS_ DISPID, REFIID, LCID, WORD,DISPPARAMS *, VARIANT *, EXCEPINFO *, UINT*) PURE; // 额外函数 STDMETHOD (Count) (THIS_ long *); STDMETHOD (Item) (THIS_ long, VARIANT *); STDMETHOD (_NewEnum) (THIS_ IUnknown **); };
这看起来完全不陌生。首先,我运行GUIDGEN.EXE来创建另一个GUID。我把它粘在上面,然后给它取了一个IID_Icollection的变量名。这就是我们ICollection对象虚表的GUID。
接下来,我们定义我们ICollection对象的虚表。我们使用了我们在上一篇文章定义IExample2虚表中使用的同样的宏。就像早些时候提到的,它以三个IUnknown函数开始,紧接着是四个IDispatch函数,就像我们为IExample2添加脚本语言支持那样。这些函数完成像在IExamle2中各自函数的职责。
最后,我们添加三个额外的函数Count、Item和_NewEnum。稍后,我们会展示它们能做什么。
记住上面的宏会自动把我们的ICollection对象定义成这样:
typedef struct { ICollectionVtbl *lpVtbl; }ICollection;
换句话说,它被定义为只有一个数据成员-一个指向我们对象虚表的指针。当然这个成员被命名为lpVtbl。但我们需要添加一个额外的DWORD成员来做引用计数(就像我们在IExample2做的那样,因此定义一个MyRealIExample2来包含这些额外的数据成员)。因此,我们这样定义MyRealICollection:
typedef struct { ICollectionVtbl *lpVtbl; DWORD count; }MyRealICollection;
现在,让我们看看我们怎样在我们的IDL文件中定义这个VTable(接口):
[uuid(F69902B1-20A0-4e99-97ED-CD671AA87B5C), oleautomation, object] interface ICollection : IDispatch { [propget, id(1)] HRESULT Count([out, retval] long *); [propget, id(DISPID_VALUE)] HRESULT Item([in] long, [out, retval] VARIANT *); [propget, id(DISPID_NEWENUM), restricted] HRESULT _NewEnum([out, retval] IUnknown **); };
注意对于我们的ICollection VTable我们使用一个新创建的GUID。
我们使用oleautomation关键字,因为我们的函数只接受和返回自动化兼容的数据类型。
最后,我们用object关键字来表明这个虚表是对象。它不是一个应用程序可以通过我们的IClassFactory来获得的对象。(稍后我们会看到应用程序是如何获得我们的ICollection对象的)。事实上,对于应用程序来说,除了知道我们的ICollection对象是一个标准的IDispatch对象,其他一无所知(也就是说我们的ICollection对象伪装成一个应用程序或脚本引擎关心的普通的IDispatch)。由于我们的对象伪装成一个普通的IDispatch,在我们的IDL文件中不需要明确定义ICollection对象本身(不像我们在IExample2做的那样,在列出所有接口时要指明哪个是缺省的接口)。所有的脚本语言和应用程序已经知道了一个普通的IDipatch对象意味着什么,所以不再需要放一些关于IDispatch的信息-在我们的IDL文件中模拟(ICollection)对象本身。所以,我们只需要像上面那样定义它的虚表,把虚表标记为是对象。
注意,interface一行,我们指定了IDispatch函数包含在VTable中位置和顺序。所以,它就理所当然可以伪装成一个IDispatch了。
然后我们列出三个额外函数。注意他们全部被定义为propget。还要注意Item函数有一个DISPID_VALUDE的ID(DISPID),_NewEnum函数有一个DISPID_NEWENUM的ID 。(对于后者我们也使用restricted关键字,因为我们不希望对象浏览器显示_NewEnum函数。它是一个只由脚本引擎在内部调用的函数,实际的脚本根本不会调用它。)对于Count函数,我们可以选择正数作为ID值,因此我随便选择了1。(对于IExample2的Buffer函数也有一个值是1的ID,这点没关系。因为这些VTable被用于两个不同的对象,所以在两个VTble间他们的ID不需要是唯一的)。
稍后我们会写这些函数。
在我们修改我们的IExample2源代码前,我们拷贝整个IExample2目录到一个新的命名为IExample3目录。我们重命名所有文件名映射到新目录(即IExample2.h变为IExample3.h,IExample2.c变为IExample3.c等等)。做完这些后,我们编辑新目录中的文件,把IExampl2对象重命名为IExample3来与我们先前的代码加以区别。我们要做的是搜索和用“IExample3”替换每个“IExample2”实例。然后不要忘记运行GUIDGEN.EXE来为IExample3生成一个新的GUID,用新的UUID更新IExample3.idl。毕竟我们不希望我们的新的DLL(我们给他命名为IExample3.dlll)与我们先前的IExample2.dll冲突。我为你做了这一切,把结果文件放在IExample3目录中。
我们需要构造我们的port名字列表。我们写一个辅助函数来做这些。我们假定我们有三个port,所以创建三个IENUMITEM。我们把这个列表头存储到一个全局变量PortsList中。同时我们还需要一个辅助函数在我们使用完后释放这个列表。
IENUMITEM *PortsList; // 释放PostsList的辅助函数。当DLL被卸载时调用。 void freePortsCollection(void) { IENUMITEM *item; item = PortsList; // 有其他元素在列表中吗? while ((item = PortsList)) { // 在我们删除这个元素前得到下一个元素 PortsList = item->next; // 如果元素的值是一个对象。我们需要对它Relesase()。如果它是一个BSTR,我们需 // 要它他SysFreeString()。我们对它调用VariantClear。 VariantClear(&item->value); // 释放IENUMITEM GlobalFree(item); } } // 初始化我们的Portslist的辅助函数。当DLL第一次被加载时调用 HRESULT initPortsCollection(void) { IENUMITEM *item; // 添加一个“Port 1”IENUMITEM到我们的列表中 if ((PortsList = item =(IENUMITEM *)GlobalAlloc(GMEM_FIXED,sizeof(IENUMITEM)))) { item->next = 0; item->value.vt = VT_BSTR; if ((item->value.bstrVal = SysAllocString(L"Port 1"))) { // 添加一个“Port 2”IENUMITEM到我们的列表中 if ((item->next = (IENUMITEM *)GlobalAlloc(GMEM_FIXED,sizeof(IENUMITEM)))) { item = item->next; item->value.vt = VT_BSTR; if ((item->value.bstrVal =SysAllocString(L"Port 2"))) { // 添加一个“Port 3”IENUMITEM到我们的列表中 if ((item->next =(IENUMITEM *)GlobalAlloc(GMEM_FIXED, sizeof(IENUMITEM)))) { item = item->next; item->next = 0; item->value.vt =VT_BSTR; if((item->value.bstrVal = SysAllocString(L"Port 3"))) return(S_OK); } } } } } // 错误 freePortsCollection(); return(E_FAIL); }
我们还得加第二个名为CollectonTypeInfo的全局变量来为我们的ICollection保存一个ITypeInfo…嗯,IDispatch对象。(我们稍后讨论为什么需要这个)。因此我们需要加全局变量,于是我们写两个辅助函数-一个把这个变量初始化为零,另一个来Release这个ITypeInfo:
// 我们的Icoolection的ITypeInfo。我们只需要一个所以我们把它定义为全局的 ITypeInfo *CollectionTypeInfo; // 初始化我们的ICollecton TypeInfo辅助函数 void initCollectionTypeInfo(void) { // 我们还没有为我们的ICollection创建ITypeInfo CollectionTypeInfo = 0; } // Release()我们的ICollection的TypeInfo的辅助函数。当我们的DLL被卸载时调用它 void freeCollectionTypeInfo(void) { if (CollectionTypeInfo) CollectionTypeInfo->lpVtbl->Release(CollectionTypeInfo); }
现在我们需要修改我们的DllMain来调用这些辅助函数:
BOOL WINAPI DllMain(HINSTANCE instance, DWORD fdwReason, LPVOIDlpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: { MyTypeInfo = 0; // 初始化我们的ICollection原型 initCollectionTypeInfo(); // 初始化我们的Port列表 if (initPortsCollection()) { MessageBox(0, "Can'tallocate the PortsList", "ERROR", MB_OK); return(0); } OutstandingObjects = LockCount = 0; MyIClassFactoryObj.lpVtbl = (IClassFactoryVtbl*)&IClassFactory_Vtbl; DisableThreadLibraryCalls(instance); break; } case DLL_PROCESS_DETACH: { // 释放我们Port列表 freePortsCollection(); // 释放我们ICollection ITypeInfo freeCollectionTypeInfo(); if (MyTypeInfo) MyTypeInfo->lpVtbl->Release(MyTypeInfo); } } return(1); }
现在,我们来写ICollection的实际函数(事实上是我们的MyRealICollection)。比把这些代码放在IExample3.c中更好的是,我们单独为它创建一个名为PortNames.c的源文件。然后我们把我们的ICollection的 Vtable和他的GUID放在一个独立的PortsNames.h文件中。(我们也把我们上面的辅助函数放在PortNames.c中)
IUnknown函数(QueryInterface、AddRef和Release)和 IDispatch函数(GetTypeInfoCount、GetTypeInfo、GetIDsOfNames和Invoke)几乎与我们的IExample3对象相应的函数一样。所以最好是复制代码到这,我要你查看PortNames.c文件(在IExample3目录中)。
当然一个不同是我们的ICollection函数传递一个ICollection对象指针(而不是一个IExample3对象指针)。ICollection的Release函数也有点不同(不像IExample3的Release,因为不需要释放buffer成员)。
另外主要区别是对GetTypeInfoOfGuid的调用。注意我们传递的是ICollection VTable的GUID(而不是像我们在IExample3.c中传递的是IExample3 VTable的GUID)。在这我们这样做,当我们获得一个IExample3的ITypeInfo时(通过在IExample3.c中调用 loadMyTypeInfo),我们传递IExample3的VTable的GUID给OLE函数GetTypeInfoOfGuid。这暗示着微软给我们创建的缺省的ITypeInfo只能获得我们IExample3 VTable中函数的信息。它不能用于获得其他对象的VTable的函数信息。这样我们就需要一个能提供给我们ICollection函数信息的ITypeInfo。所以现在我们必须再次调用GetTypeInfoOfGuid,但这次我们传递的是我们的ICollection对象的VTable的GUID(即我创建的新GUID)。它会返回第二个ITypeInfo(我们把它存储到我们添加的名为CollectonTypeInfo的全局变量中)。这个第二个ITypeInfo可用于使用ICollection的IDispatch 函数来获取ICollection的函数信息。它也可用于在ICollection的Invoke和GetIDsOfName函数中使用 DispInvoke和DispGetIDsOfNames来为我们做几乎所有的工作-就像我们用IExample3的ITypeInfo做的那样。
注意ICollection的IDispatch函数使用这个新的ITypeInfo,而IExample3的IDispatch函数使用IExample3的ITypeInfo。他们不是同一个ITypeInfo,也不能交替使用。
剩下的事是写这个三个额外函数,Count、Item和_NewEnum。
Count函数非常简单。它传入一个指向long的指针。Count用在我们的列表中的元素总数来填充这个指针。例如,前面在我们的列表中有三个port(IENUMITEM结构),那么我们返回3。
这是我们的Count函数:
STDMETHODIMP Count(ICollection *this, long *total) { DWORD count; IENUMITEM *item; // Count通过从头到尾遍历IENUMITEM,对每个元素增加count来获得元素总数 count = 0; item = (IENUMITEM *)&PortsList; while ((item = item->next)) ++count; // 返回total *total = count; return(S_OK); }
Item函数也很简单。它传入一个long告诉我们它需要那个元素(0表示第一元素,1表示第二个元素等等)。同时传入一个VARIANT用于我们拷贝这个元素的值给它。
这是我们的Item函数:
STDMETHODIMP Item(ICollection *this, long index, VARIANT *ret) { IENUMITEM *item; // 假定我们什么也不返回。 ret->vt = VT_EMPTY; // 定位调用者需要的元素 item = (IENUMITEM *)PortsList; while (item && index--) item = item->next; // 还有其他元素吗? if (item) { // 拷贝这个元素的值到调用者提供的VARIANT中。如果我们返回给调用者是一个对象, // 我们必须体替调用者对它调用AddRef。调用者在使用完后应该对他调用Release。如 // 果我们返回的是一个BSTR,那么我们必须通过SysAllocString来做一个拷贝,调用 // 者也应该对它调用SysFreeString。其他数据类型只要像这样简单拷贝到调用者的 // VARIANT中。VariantCopy()为我们做了这一切。如果一切没问题返回S_OK。 return(VariantCopy(ret, &item->value)); } // 如果没有其他元素,返回S_FALSE。 return(S_FALSE); }
就像你在上面注释中看到的,OLE函数VariantCopy替我们做了所有的工作。
现在,我们掩掉_NewEnum函数。我们做一个空桩返回E_NOTIMPL。
一旦我们写完ICollection的全部函数,我们静态申明它的VTable:
static const ICollectionVtbl ICollectionVTable = {Collection_QueryInterface, Collection_AddRef, Collection_Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke, Count, Item, _NewEnum};
让我们考虑应用程序如何获得我们的MyRealICollection对象的一个实例。最容易做的是添加另一个(额外的)函数到IExample3对象。应用程序调用这个新的函数来分配和接受我们的MyRealICollection对象的一个实例。(但我们要欺骗一下应用程序,告诉应用程序它就是一个普通的IDispatch)
我们需要改变IExample3的VTable(在IExample3.h中)的定义,添加这个新的函数,我随意给它命名GetPorts。我把它定义为可接受传入一个IDispatch句柄,我们会把我们新分配的MyRealICollection指针通过它返回…哦,是IDispatch。是,是它。它就是一个IDispatch。Wink, wink。这是我们更新后的IExample3的VTable。
// IExample3的VTable #undef INTERFACE #define INTERFACE IExample3 DECLARE_INTERFACE_ (INTERFACE, IDispatch) { // IUnkown函数 STDMETHOD (QueryInterface) (THIS_REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; // IDispatch functions // IDispatch函数 STDMETHOD_ (ULONG, GetTypeInfoCount)(THIS_ UINT *) PURE; STDMETHOD_ (ULONG, GetTypeInfo) (THIS_ UINT, LCID, ITypeInfo **) PURE; STDMETHOD_ (ULONG, GetIDsOfNames) (THIS_ REFIID, LPOLESTR *, UINT, LCID, DISPID *) PURE; STDMETHOD_ (ULONG, Invoke) (THIS_ DISPID, REFIID, LCID, WORD, DISPPARAMS *, VARIANT*, EXCEPINFO *, UINT *) PURE; // 额外函数 STDMETHOD (SetString) (THIS_BSTR) PURE; STDMETHOD (GetString) (THIS_ BSTR*) PURE; STDMETHOD (GetPorts) (THIS_IDispatch **) PURE; // <---在这添加GetPorts };
注意我在VTable的最后面添加了GetPorts。还要注意我指明GetPorts要用一个IDipatch指针来填充(尽管它其实是我们的MyRealICollection)。它是一个IDispatch。诚实点。我撒谎了?
同时我必须对在IDL文件中我们的IExample3的VTable做同样的改变:
[uuid(CFADB388-9563-4591-AABB-BE7794AEC17C), dual, oleautomation,hidden, nonextensible] interface IExample3VTbl : IDispatch { [helpstring("Sets the test string.")] [id(1), propput] HRESULT Buffer([in] BSTR); [helpstring("Gets the test string.")] [id(1), propget] HRESULT Buffer([out, retval] BSTR *); [helpstring("Gets the enumeration for our hardware ports.")] [id(2), propget] HRESULT Ports([out, retval] IDispatch **); // <--- 在这添加GetPorts };
注意我给新添加的函数加了propget,就像Buffer那样。这样脚本就可以用一个普通的方法来获取我们的MyRealICollection了…咄!…IDispatch对象。这个脚本相关的成员叫“Ports”。不用担心实际上在我们的IExample3对象中没有Ports这个数据成员。这是一个假的成员。但脚本不需要知道这些。
同时我随意给它一个2作为DISPID。
不要忘记我们需要把这个函数添加到IExample3.c中我们的IExample3Vtbl静态声明中:
static const IExample3Vtbl IExample3_Vtbl = {QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke, SetString, GetString, GetPorts}; // <--- 在这添加GetPorts
那么,我们需要写GetPorts函数:
static HRESULT STDMETHODCALLTYPE GetPorts(IExample3 *this, IDispatch**portsObj) { // 创建IDispatch来枚举我们的port的名字。调用者负责对他调用Release。注意:我们 // 实际上返回的是一个MyRealICollection。但调用这不知道这些。他认为我们返回的是一 // 个IDispatch。这没错因为MyRealICollection的VTable以三个IUnkown函数开始,后 // 面紧跟四个IDispatch函数。就像是一个真正的IDispatch对象的VTable一样。 if (!(*portsObj = allocPortsCollection())) return(E_OUTOFMEMORY); }
上面简单调用另一个我们放在PortNames.c中的辅助函数(名为allocPortsCollection)。这个辅助函数完成调用GlobalAlloc分配一个MyRealIcollection并初始化它的工作。它与我们的IClassFactory的CreateInstance通过GlobalAlloc分配IExample3并初始化的方法非常类似。我们要增加未完对象计数因为我们的MyRealICollection…哦,IDispatch对像会提供给应用程序(它被认为会在稍后对其调用Release)。
IDispatch * allocPortsCollection(void) { MyRealICollection *collection; // 分配MyRealICollection if ((collection = (MyRealICollection *)GlobalAlloc( GMEM_FIXED,sizeof(MyRealICollection)))) { // 存储它的VTable collection->lpVtbl = (ICollectionVtbl *)&ICollectionVTable; // 对它进行AddRef collection->count = 1; // 需要另一个未完对象因为我们会把它返回给应用程序,应用程序应该对它调用Release InterlockedIncrement(&OutstandingObjects); } // 把它当作IDispatch(它可以被用作)返回 return((IDispatch *)collection); }
我们做完了。你可以编译这个IExample3.dll。注册它,就是用IExample的注册工具(RegIExample2),用“IExample3”替换每个“IExample2”。毕竟除了我们要注册的是IExample3,其他与IExample2没什么区别。同样,对于反注册,修改IExample2的反注册工具(UnRegIExample2)。
让我们来写一个使用我们的集合来显示port名的VBScript例程。我已经泄了这个例程(IExample3.vbs),把它放在IExample3目录中。
当然这个VBScript需要首先调用CreateObject来获得我们的IExample3对象的一个实例。如果正确地安装了它,它应该有一个“IExample3.object”的ProdID。现在脚本拥有了我们的IExample3,它可以简单访问假的“Ports”成员来获得我们的MyRealIcollection一个实例…该死!…IDisaptch对象来用。在这,我们给它指定一个“coll”变量名。
Set coll = myObj.Ports
接下来我们调用Count函数来获取有多少个port名。事实上,因为我们的类型库把这个函数定义为proget,脚本可以使用赋值。
count = coll.Count
现在循环调用Item函数来获取每个port名,显示它:
For i = 0 To count - 1 MsgBox coll.Item(i) Next
它就是这样。
对于使用我们的集合对象的C/C++应用程序,它需要通过Invoke函数来间接调用我们的Count和Item函数。坐好了因为这会是一段坎坷之旅。微软Visual Basic程序员设计了IDispatch传递参数和返回值,这样那些人就会很容易来对VB添加COM支持和快速入门。但他们对于其他语言如何容易的利用IDisaptch的函数没有给予过多的关心,这对于C/C++来说就相当痛苦。
在IExample3App目录里是一个完成上面VBScript所做的C应用程序例程。它获得我们的Ports集合对象,用它显示所有的port名。我没讨论C应用程序如何获得我们的IExample3对象。它与如何获得一个IExample3对象一样(除了我们#include IExample3.h和使用IExample3对象的GUID)。
重点关注一下开始部分,我们调用IExample3的GetPorts函数来获取我们的MyRealICollection…咳咳…IDispatch对象。我放了下面的注释在开始部分(在IExample3App.c中):
// STUDY THIS
细读那块代码和注释。他们叙述了对于C/C++使用IDisaptch函数你需要做的每一步。这时请一个微软的Viaual Basic程序员吃午餐来“感谢”它,趁它不注意放许多辣椒到他的食物中。
如果在我们的Item函数中看到下面几行,我们会注意到些东西:
// 定位调用者需要的元素 item = (IENUMITEM *)PortsList; while (item && index--) item = item->next;
每次当有人调用我们的Item函数,我们必须从列表头开始搜索需要的元素。假设我们有30,000个元素在这个列表中。比如应用程序要我们取第28,000个元素。我们在得到想要的元素前必须要越过27,000个元素。这时假设应用程序再次调用Item请求第29,000个元素。即使只是相差一点,我们也必须重新从列表头开始越过28,000个元素。很明显,这样效率很低。
也许我们可以增加一个数据成员到我们的MyRealICollection中. 这个成员用于存储我们最后停止在列表中的“position”。微软已想到这一点,然后决定,不是修改集合对象(由于VB开发者的原因,没有从C/C++调用角度设计得更高效),而是定义了一个名为IEnumVARIANT的标准COM对象。其主要目的是要IEnumVARIANT来存储应用程序在列表中“读取”的当前位置。但对于通过Invoke间接调用我们集合Item函数的C/C++应用程序来说则相当低效和麻烦,微软在IEnumVARIANT中规定了几个函数,应用程序可以直接调用他们来做我们先前用集合的Item函数做的事情…和其他一些事。在一个IEnumVARIANT中有四个(当然还得有三个IUnkown函数)名为Next、Skip、Reset和Clone的函数。
微软已定义了(在和你的编译器一起的包含文件中)IEnumVARIANT对象和它的VTable(也就是微软已经规定在VTable中函数做什么、参数及返回值)。所以我们不需要做这些了。但像我们先前创建的对象一样,我们需要添加一对额外的数据程序到IEnumVARIANT中。那么再做一次,我们定义一个有三个额外成员的MyRealIEnumVarinat。
但在我们修改IExample3源代码前,我们再次创建一个名为IExample4的新目录。我们要做的是拷贝源代码到新目录下,改名、编辑他们。搜索“IExample3”用“IExample4”替换它。运行GUIDGEN.EXE来创建新的GUID,把它们放在IExample4.h、PortNames.h和IExample4.idl中。我再次替你做了这些,用这些新文件创建一个IExmpale4目录。
在PortNames.c中,我们添加我们的MyRealIEnumVariant对象的定义,写它的所有函数,把它的VTable声明为静态的。他们以这个注释开始:
//============================================================== //=================== IEnumVARIANTfunctions =================== //==============================================================
事实上,现在你对它的QueryInterface、AddRef和Release函数做什么应该非常熟悉了。其他四个函数不是很重要,所以你可以细读源代码注释部分来得到那些函数的细节。
注意问题是“应用程序如何获取我们的IEnumVARIANT对象?”。记得先前,我们忽略我们集合对象的_NewEnum函数,只让它返回E_NOTIMPL吗?好的,猜得没错。应用程序可以调用这个函数来获取我们的IEnumVARIANT对象的一个实例,所以现在我们给它写些真正的代码。
换句话说,应用程序要获得我们IEnumVARIANT的一个实例,首先必须获得我们的IExample4对象,调用我们IExample4的GetPorts函数来获取我们的集合对象,然后调用我们集合对象的_NewEnum函数来得到IEnumVARIANT。它不是最好的方法,但它可以工作。坏消息:C/C++应用程序不能直接调用我们集合的_NewEnum函数。就像我们集合的Count和Item函数一样,C/C++应用程序必须通过Invoke来间接调用_NewEnum。好消息是一旦我们的C/C++应用程序拥有了这个IEnumVARIANT,就可以对集合对象调用它的Release()而这样做了不会出现异常。
所以,我们给出我们集合的_NewEnum函数:
STDMETHODIMP _NewEnum(ICollection *this, IUnknown **enumObj) { IEnumVARIANT *enumVariant; if (!(enumVariant = allocIEnumVARIANT())) return(E_OUTOFMEMORY); *enumObj = (IUnknown *)enumVariant; return(S_OK); }
就是调用名为allIEnumVARIANT的辅助函数来分配和初始化我们的IEnumVARIANT(实际上是MyRealIEnumVariant),就像我们的IClassFactory的CreateInstance分配IExample4的对象或我们的IExample4的GetPorts函数分配我们的集合对象一样。在这实际没有新的东西。
但要注意_NewEnum需要应用程序传入一个我们用于返回IUnkown对象的句柄-不是一个IEnumVARIANT。是的,我们实际上返回的是我们的IEnumVARIANT,但它伪装成了一个IUnknown对象,这样做的原因是它的VTable以三个IUnkown函数开始。
“但为什么要伪装呢?你刚才不是说_NewEnum被用于获取我们的IEnumVARIANT吗?”
是的…有点绕。[Cue scary monster movie soundtrack.]
在前面的文章中,我提到过这样的事实,一个COM对象实际上在它内部可以有许多VTables。我们称这样的对象有“多个接口”。微软规定_NewEnum应该能回传一个拥有多个接口的对象,IEnumVARIANT可能就是它多个VTable其中之一(可能不是这个对象的第一个VTable)。所以,应用程序应该做的是得到我们给它的这个IUnkown对象,传递IEnumVARIANT的GUID(IID_IEnumVARIANT,微软的包含文件已经替我们定义好了)调用这个对象的QueryInterface。然后,QueryInterface会返回一个指向IEnumVARIANT VTable的指针(也就是说,本质上IEnumVARIANT包含在一个真正的IUnknown对象的内部-因为你知道的它就是一个伪装的IUnknown)。
就我们而言,应用程序会调用我们IEnumVARIANT的QueryInterface,向我们要一个IEnumVARIANT。我们就再次返回同一个指针。这完全没有必要、低效、不合理。但,事实就是这样,你获得这部分COM是由微软的VB程序员(他们得益于让IDispatch做大量的事情,就像VB内部调用它自己的内建函数,从而减少他们的工作,并把这个设计强塞给每个人,不管它是多么不便和不实用)和其他要使用像多接口的程序员设计的(VBScript甚至不能直接使用它-这个错误说起来有点搞笑)。
总之,我们得让我们的IEnumVARIANT支持这个,然后我们可以编译IExample4.dll。注册它,你可以再次修改RegIExample2.c,查找“IExample2”并用“IExample4”替换。
VBScript完全不能调用我们集合的_NewEnum函数(因为它返回的是一个需要调用QueryInterface的多接口对象)。所以,VBScript不能获取我们的IEnumVARIANT和调用它的函数。
这意味着IEnumVARIANT对VBScript不可用?不。VBScript引擎本身可以使用我们的IEnumVARIANT。引擎什么时候会使用它呢?当脚本使用For Each循环来遍历我们集合的元素时。在IExample4目录中有一个叫IExample4.vbs使用了For Each循环的VBScript。它与做同样事情的IExample3.vbs脚本实现有点不同,但在内部它更高效(因为引擎使用我们IEnumVARIANT的Next函数,而不是像VBScript使用我们集合对象的Item函数)。脚本代码部分只有细微的不同因为引擎替脚本获取元素的值。这是它的做法:
set myObj = CreateObject("IExample4.object") Set coll = myObj.Ports For Each elem In coll MsgBox elem Next
前两行与IExample3.vbs一样(除了我们现在用了IExample4.dll的ProdID)。
但这个循环是不一样的。当VBScript引擎执行For Each行时,它要获得我们的IEnumVARIANT(通过调用我们集合的_NewEnum,然后QueryInterface IID_IEumVARIANT)。它把这个IEnumVARIANT存储在内部,这样它可以在后面的循环中反复使用它。然后,它调用我们IEnumVARIANT的Next函数,返回获取元素的值。当然,第一次Next调用,我们返回第一个元素的port名(也就是“Port 1”字符串)。VB引擎把这个字符串填充到变量“elem”中。这是VBScript指令做法。现在,脚本简单显示“elem”的值(“Port1”字符串)。在下一次循环,VB引擎再次调用我们的IEnumVARIANT的Next函数,获取另一个元素的值。Next第二次被调用,所以当然我们返回第二个元素的值,“Port 2”字符串。VB引擎现在用新值来更新elem变量。脚本显示“Port 2”。这个操作继续直到VB引擎调用我们的IEnumVARIANT的Next,我们不返回元素为止。这时Next返回S_FALSE(而不是S_OK)给引擎。然后,引擎退出循环(释放我们的IEnumVARIANT)。
在IExmaple4App目录下有一个示范如何获得和使用我们IEnumVARINAT的C例程。我们还必须摆弄集合对象和它的Invoke。但至少这个循环是相当高效。整个过程没有使用集合对象的Item函数。
如果你查看PortName.c中IEnumVARIANT和集合函数,你会发现这些硬代码只能工作于我们的端口名列表(也就是PortsList)。只要一点小的调整,我们重写那些函数来使他们工作于我们给出的任意IENUMITEM链表。换句话说,我们可以使这些函数更通用,这样如果我们的组件需要维护几种不同类型的列表,我们就可以比较容易在不做其他修改重新使用这些同样的函数来提供其他集合和IEnumVARIANT对象。所以,我们就把IEnumVARIANT和集合函数分割到一个新的名为IEnumVariant.c的源文件中。我们只把访问我们PortsList的特殊代码留在PortNames.c中。
但首先我们要做的事是创建一个IExample5的新目录,把文件拷贝进去,重命名、编辑他们。你现在应该知道这个流程了。我已经做了这个工作,创建了IExample5目录。
不是简单声明一个全局列表自身变量,我们把列表封装在另一个名为IENUMLIST结构中,像这样:
typedef struct { struct _IENUMITEM *head; DWORD count; }IENUMLIST;
Head成员用于存放列表。我们添加的count字段当每次我们创建另一个集合或用这个指定的列表创建IEnumVARIANT时自动增长。
现在,我们修改全局PortsList为新结构:
IENUMLIST PortsList;
让我们的集合和IEnumVARIANT函数更通用的关键点是添加一个额外数据成员到我们的MyRealIEnumVariant和MyRealICollection对象中。我们添加一个成员到MyRealICollection中用于保存指向要被操作的IENUMLIST的指针。同时我们写一个新的辅助函数来分配MyRealICollection对象。这个辅助函数接受一个IENUMLIST指针,把它存储在我们的MyRealICollection新加的数据成员中。我已经写了这样的函数(allocICollection)并把它放在IEnumVariant.c中。它接受一个指向我们集合对象要操作的IENUMLIST指针。
我们还得添加一个数据成员到MyRealIEnumVariant中。它做像添加到MyRealICollection的新的数据成员同样的事情(也就是保存一个指向我们的IEnumVARIANT要操作的IENUMLIST指针)。
其他的改变不是很重要,除了我们留在PortNames.c中那点创建和删除我们Ports的代码,和创建封装PorstList的特定集合对象的代码。
为了创建另一个列表,添加集合和IEnumVARIANT对象的支持,我们需要做的是创建另一个源文件,就像PortNames.c中的。事实上,我们这样做的。
假设我们要创建一个系统网卡的列表。对于每一块网卡我们要提供两条信息:网卡名和它的MAC地址。我们把代码放在NetCards.c和NetCards.h中。
我们要返回的每个元素的信息不只一个。(即每个IENUMITEM会对应一个单独的网卡。对于每块网卡我们要让脚本知道网卡的名字和它的MAC地址。)实现它的最好方法是我们创建一个“子对象(sub-object)”来专门调用INetwork对象。我们在这个对象中加入两个函数: Name和Address。这个Name函数返回一个BSTR类型的网卡名字,Address函数返回一个BSTR类型的MAC地址。
我们会为计算机上的每块网卡创建一个INetwork对象。然后我们会在我们的列表中为网卡创建一个IEUMITEM。我们会把INetwork指针填充到IENUMITEM VARIANT的punVal字段,并把vt字段设置为VT_DISPATCH。AllocNetworkObjectsCollection(在NetCards.c中)负责创建IENUMITEM列表(包括INetwork对象)。
为了VBScript能用,我们需要在INetwork的VTable中包含IDispatch函数。当然,这也意味这我们的INetwork的VTable需要一个ITypeInfo。所以我们必须得给它的VTable生成一个新的GUID。然后我们必须通过传递这个新的GUID调用GetTypeInfoOfGuid来为它获取一个ITypeInfo。我们把它保存到全局变量NetTypeInfo中。所有这些代码在NetCards.c中。这些代码看起来与你看到得我们的集合对象和IExample5对象很相似,因为那些对象也有IDispatch函数,也需要他们的TypeInfo对象。
同时为了使我们的INetwork的额外函数(即Name和Address)可以被C/C++直接调用,我们需要在我们的IDL文件把它的VTable声明为“dual”。我们还必须把它的VTable定义包含到IExample5.h中以便C/C++应用程序确切知道那些额外函数的顺序和参数。我已经把INetwork的VTable定义添加到IExample5.h和IExample5.idl中了。注意它看起来和我们的IExample5对象十分相似。都包含IDispatch函数,都被声明为dual。他们只是额外的函数不同而已。但像我们的IEnumVARIANT对象一样,我们的INetwork对象本身不需要在IDL中声明。只需要声明它的VTable。毕竟对于应用程序来说我们的INetwork看起来像一个标准的IDispatch对象,除了它的额外函数会在它的VTable中,并且C/C++应用程序可以直接调用他们。
通过给我们的IENUMLIST一个count字段,我们可以确认什么时候所有的集合对象和IEnumVARIANT对象使用完它的列表,并在我们需要的任何时候删除这个列表。(即不像前面的例程,只有我们的DLL终止时才可以删除这个列表)事实上,你会注意到我们在应用程序真正向我们请求我们的网卡集合前不会真正创建INetwork对象列表。于是只要最后使用这个INetwork对象列表的集合、IEnumVARIANT调用了Release()我们就可以删除这个类别。
在IExample5目录中有一个使用For Each循环来获取访问每一个我们的INetwork对象的VBScript例程。
有时我们可能要让脚本、应用程序向我们的列表添加或删除元素。惯用方法是在我们的集合对象中添加Add和Remove函数。因为每个列表可能包含不同种的元素,需要不同种类的数据,你必须定义另一个的集合对象来给指定的列表。你要定义它的VTable,放这两个额外的函数(Add和Remove)到它里面。Add函数必须写成可以接受脚本、应用程序需要创建的新元素的任何数据。(这个函数首先要在列表查找是否已经有匹配的元素,以消除重复元素)
当然,你需要为这个新集合的VTable生成一个GUID。同时,你需要通过把它的GUID传给GetTypeInfoOfGuid来为它创建一个ITypeInfo,把这个ITypeInfo保存在一个全局变量中以供Invoke、GetTypeInfo和GetIDsOfNames使用。
好消息是你可以使用我们原始的集合(ICollection)和IEnumVARIANT使用过的许多同样的函数(在IEnumVariant.c中)。所以,没有你想象的那么多的新的代码。