C 语言 COM 组件开发系列 第三部分






4.90/5 (37投票s)
C 语言中的 COM 集合
目录
- 引言
- 定义集合对象
- 一些辅助函数
- 我们的集合对象函数
- 应用程序如何获取我们的集合对象
- VBScript 示例
- C 语言示例
- IEnumVARIANT 对象
- 另一个 VBScript 示例
- 另一个 C 语言示例
- 更通用的方法
- 添加/删除项
引言
有时,我们可能需要维护一个项目列表。例如,假设我们的 COM 组件旨在操作我们设计的某些硬件 PCI 卡。并且,假设用户可以在一台计算机上安装多张此类卡,而我们希望我们的组件能够控制所有可用的卡,让应用程序能够检索每张卡的详细信息,并单独访问每张卡。
换句话说,当我们的组件运行时,它需要查询系统中存在哪些卡,并将有关所有卡的列表提供给应用程序。为了便于论证,我们假设我们唯一需要提供的信息是每张卡的“名称”,其中系统中的第一张卡将被命名为“Port 1”,第二张卡将命名为“Port 2”,依此类推。
由于我们无法提前知道系统中会有多少张卡,因此最好的方法是创建一个可以与其他相同类型的对象链接的结构,形成一个链表。例如,也许我们会定义一个 IENUMITEM
结构来保存有关一张卡的信息
typedef struct _IENUMITEM { // To link several IENUMITEMs into a list. 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 的术语中,我们将一组相关项称为“集合”。因此,上面包含三个项的链表就是我们的集合。
但是,我们上面的 IENUMITEM
有一个问题。它有一个 char *
成员,这不兼容自动化。我们可以将其更改为 BSTR
,它是兼容自动化的。更好的方法是,我们在 IENUMITEM
中直接放置一个 VARIANT
。这样做的好处是使 IENUMITEM
通用(即,它可以存储任何类型的自动化数据类型)。并且,正如我们稍后将看到的,我们必须使用 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");
定义集合对象
请记住,某些脚本语言没有指针的概念。 such a language can't possibly walk the above list on its own, because the first member of each IENUMITEM
is a pointer to the next IENUMITEM
. 因此,我们需要提供一个对象来帮助应用程序(使用我们的 COM 对象)遍历此列表,获取每个项的值。
由于微软将此对象的创建交给了 Visual Basic 程序员(ugh),因此他们选择将其基于 IDispatch
。换句话说,我们对象的 VTable 必须以所有 COM 对象都具备的三个 IUnknown
函数(QueryInterface
、AddRef
和 Release
)开头,然后紧接着是标准的四个 IDispatch
函数(GetTypeInfoCount
、GetTypeInfo
、GetIDsOfNames
和 Invoke
)。然后,我们的对象必须再有三个函数。在我们的 IDL 文件中,当我们定义 VTable(即接口)时,我们必须将这三个额外函数的名称命名为 Count
、Item
和 _NewEnum
。在我们对象的实际 VTable 中,我们可以为指针使用任何名称(尽管我会坚持使用这些名称)。为什么?因为没有人会直接调用它们。没有人会知道我们将在 VTable 中添加这三个函数。这些函数只能通过我们对象的 Invoke
函数间接调用——即使你使用的是像 C 这样的语言,如果微软的 Visual Basic 程序员设计得能够容纳更强大的语言,它也可以直接调用它们。这就是我们为让 Visual Basic 程序员设计它而付出的可怕代价。此外,在我们的 IDL 文件中,我们必须为 Item
函数分配 DISPID_VALUE
ID,为 _NewEnum
函数分配 DISPID_NEWENUM
ID。我们必须这样做。Count
函数可以分配任何我们选择的正数作为其 ID。这看起来有任何一致性或逻辑性吗?没有?记住——Visual Basic 程序员。
我们还需要为该对象的 VTable 生成一个新的 GUID,供我们的 IDL 文件(即类型库)使用。
让我们看一下这个对象的定义。我们可以给它取任何名字。我选择称它为 ICollection
// Our ICollection VTable's GUID // {F69902B1-20A0-4e99-97ED-CD671AA87B5C} DEFINE_GUID(IID_ICollection, 0xf69902b1, 0x20a0, 0x4e99, 0x97, 0xed, 0xcd, 0x67, 0x1a, 0xa8, 0x7b, 0x5c); // Our ICollection's VTable #undef INTERFACE #define INTERFACE ICollection DECLARE_INTERFACE_ (INTERFACE, IDispatch) { // IUnknown functions STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; // IDispatch functions 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; // Extra functions STDMETHOD (Count) (THIS_ long *); STDMETHOD (Item) (THIS_ long, VARIANT *); STDMETHOD (_NewEnum) (THIS_ IUnknown **); };
这看起来应该不完全陌生。首先,我运行了 GUIDGEN.EXE 来创建另一个 GUID。我将其粘贴在上面,并将其命名为 IID_ICollection
。这是我们 ICollection
对象 VTable 的 GUID。
接下来,我们定义 ICollection
对象 VTable。我们使用了与上篇文章中定义 IExample2
VTable 时相同的宏。正如前面提到的,它以三个 IUnknown
函数开头,后跟四个 IDispatch
函数,就像 IExample2
在我们添加对脚本语言支持时一样。这些函数将执行与 IExample2
中相应函数相同的职责。
最后,我们添加了三个额外函数:Count
、Item
和 _NewEnum
。稍后我们将研究它们的作用。
请记住,上面的宏会自动将我们的 ICollection
对象定义为如下所示
typedef struct { ICollectionVtbl *lpVtbl; } ICollection;
换句话说,它被定义为只有一个数据成员——指向我们 ICollection
VTable 的指针。该成员当然命名为 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
关键字来指示此 VTable 属于某个对象。它不是应用程序通过我们的 IClassFactory
获取的对象。(稍后,我们将看到应用程序如何获取我们的 ICollection
对象之一。)实际上,应用程序永远不会被告知我们的 ICollection
对象除了一个普通的 IDispatch
对象以外的任何东西(即,就应用程序或脚本引擎而言,我们的 ICollection
对象将伪装成普通的 IDispatch
)。由于我们的对象将伪装成普通的 IDispatch
,因此没有必要在我们的 IDL 文件中明确定义我们的 ICollection
对象本身(不像我们在列出其中包含的接口以及哪个是默认接口时对 IExample2
所做的那样)。所有脚本语言和应用程序都已经了解一个普通的 IDispatch
对象,因此没有必要将关于这个 IDispatch
伪装者(ICollection
)对象本身的任何信息放入我们的 IDL 文件中。我们只需要如上定义其 VTable,并将其标记为属于某个对象。
请注意,在接口行上,我们确实指定此 VTable 在预期位置和顺序包含 IDispatch
函数。因此,它肯定可以伪装成 IDispatch
。
然后,我们列出三个额外函数。请注意,它们都定义为 propget
。另请注意,Item
函数的 ID (DISPID) 为 DISPID_VALUE
,_NewEnum
函数的 ID 为 DISPID_NEWENUM
。(我们还在后者上使用了 restricted
关键字,因为对象浏览器不应显示 _NewEnum
函数。它应该是一个脚本引擎只能在内部调用的函数,但实际脚本永远不会调用它。)对于 Count
函数,我们可以选择任何正数 ID,所以我任意选择了 1。(IExample2
的 Buffer
函数的 ID 也是 1,这无关紧要。这些 VTable 用于两个不同的对象,因此它们的 ID 不需要在这两个 VTable 之间是唯一的)。
稍后,我们将实际编写这些函数。
在我们更改 IExample2
的源代码之前,让我们将整个 IExample2 目录复制到一个名为 IExample3 的新目录中。我们还将重命名所有文件以反映这个新目录(例如,IExample2.h 变成 IExample3.h,IExample2.c 变成 IExample3.c 等)。同时,让我们编辑这个新目录中的文件,将我们的 IExample2
对象重命名为 IExample3
,以区别于我们之前的源代码。我们所做的就是搜索并替换“IExample2”的每个实例为“IExample3”。并且,不要忘记运行 GUIDGEN.EXE 为 IExample3
、其 VTable 和类型库创建新的 GUID。替换 IExample3.h 中的旧 GUID,并更新 IExample3.idl 中的 UUID。毕竟,我们不希望我们的新 DLL(我们将命名为 IExample3.dll)与我们之前的 IExample2.dll 冲突。我已经为你完成了所有这些,并将生成的文件放在了一个 IExample3 目录中。
一些辅助函数
我们需要构建我们的端口名称列表。让我们编写一个辅助函数来完成此操作。我们将任意假设我们有三个端口,因此创建三个 IENUMITEM
。我们将此列表的头部存储在一个全局变量 PortsList
中。而且,我们还需要一个辅助函数来在我们完成列表后释放它。
IENUMITEM *PortsList; // This is just a helper function to free // up our PortsList. Called when our DLL unloads. void freePortsCollection(void) { IENUMITEM *item; item = PortsList; // Is there another item in the list? while ((item = PortsList)) { // Get the next item *before* we delete this one PortsList = item->next; // If the item's value is an object, we need to Release() // it. If it's a BSTR, we need to SysFreeString() it. // VariantClear does this for us. VariantClear(&item->value); // Free the IENUMITEM. GlobalFree(item); } } // This is just a helper function to initialize our Ports list. // Called when our DLL first loads. HRESULT initPortsCollection(void) { IENUMITEM *item; // Add a "Port 1" IENUMITEM to our list. 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"))) { // Add a "Port 2" IENUMITEM to our list. 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"))) { // Add a "Port 3" IENUMITEM to our list. 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); } } } } } // Error freePortsCollection(); return(E_FAIL); }
我们还将添加第二个名为 CollectionTypeInfo
的全局变量,用于存储我们 ICollection
... 嗯,IDispatch
对象的 ITypeInfo
。(稍后我们将讨论为什么需要它)。因此,我们需要添加该全局变量,然后让我们编写两个辅助函数——一个用于将变量初始化为零,一个用于 Release ITypeInfo
// Our ICollection's ITypeInfo. We need only one of these so // we can make it global ITypeInfo *CollectionTypeInfo; // This helper function initializes our ICollection TypeInfo. // It's called when our DLL is loading. void initCollectionTypeInfo(void) { // We haven't yet created the ITypeInfo for our ICollection CollectionTypeInfo = 0; } // This helper function just Release()'s our ICollection TypeInfo. // It's called when our DLL is unloading. void freeCollectionTypeInfo(void) { if (CollectionTypeInfo) CollectionTypeInfo->lpVtbl->AddRef(CollectionTypeInfo); }
现在,我们需要修改我们的 DllMain
来调用这些辅助函数
BOOL WINAPI DllMain(HINSTANCE instance, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: { MyTypeInfo = 0; // Initialize our ICollection stuff initCollectionTypeInfo(); // Initialize our Ports list if (initPortsCollection()) { MessageBox(0, "Can't allocate the PortsList", "ERROR", MB_OK); return(0); } OutstandingObjects = LockCount = 0; MyIClassFactoryObj.lpVtbl = (IClassFactoryVtbl *)&IClassFactory_Vtbl; DisableThreadLibraryCalls(instance); break; } case DLL_PROCESS_DETACH: { // Free our Ports list freePortsCollection(); // Free our ICollection ITypeInfo freeCollectionTypeInfo(); if (MyTypeInfo) MyTypeInfo->lpVtbl->Release(MyTypeInfo); } } return(1); }
我们的集合对象函数
现在,让我们为我们的 ICollection
(实际上是我们的 MyRealICollection
)编写实际的函数。我们不将此代码放在 IExample3.c 中,而是将其创建一个单独的源文件,名为 PortNames.c。并且,我们将把我们的 ICollection
VTable 及其 GUID 的定义放在一个单独的 PortNames.h 文件中。(我们也可以将上述辅助函数放在 PortNames.c 中)。
IUnknown
函数(QueryInterface
、AddRef
和 Release
)和 IDispatch
函数(GetTypeInfoCount
、GetTypeInfo
、GetIDsOfNames
和 Invoke
)几乎与我们 IExample3
对象相应的函数相同。因此,我不在此处复制代码,而是将您重定向到文件 PortNames.c(位于 IExample3 目录中)。
一个区别是,我们的 ICollection
函数被传递了一个 ICollection
对象指针(而不是 IExample3
对象指针),当然。并且,ICollection
的 Release
函数略有不同(因为与 IExample3
的 Release
不同,没有缓冲区成员需要释放)。
另一个关键区别是对 GetTypeInfoOfGuid
的调用。请注意,我们正在传递我们 ICollection
VTable 的 GUID(而不是像我们在 IExample3.c 中那样传递我们 IExample3
VTable 的 GUID)。情况是这样的。当我们获取 IExample3
的 ITypeInfo
(通过在 IExample3.c 中调用 loadMyTypeInfo
)时,我们将 IExample3
VTable 的 GUID 传递给 OLE 函数 GetTypeInfoOfGuid
。这意味着 Microsoft 为我们创建的默认 ITypeInfo
仅用于获取有关我们 IExample3
VTable 中函数的信息。它不能用于获取有关另一个对象 VTable 中函数的信息。但是,我们确实需要一个 ITypeInfo
来提供我们 ICollection
函数的信息。因此,现在我们必须再次调用 GetTypeInfoOfGuid
,但这次我们传递我们 ICollection
对象 VTable 的 GUID(即,我创建的新 GUID)。这将返回第二个 ITypeInfo
(我们将其存储在我们添加的全局变量 CollectionTypeInfo
中)。此第二个 ITypeInfo
可用于我们 ICollection
的 IDispatch
函数,以获取有关我们 ICollection
函数的信息。它还可以与 DispInvoke
和 DispGetIDsOfNames
在我们 ICollection
的 Invoke
和 GetIDsOfNames
函数中使用,为我们完成几乎所有工作——就像我们使用 IExample3
的 ITypeInfo
一样。
请注意,ICollection
的 IDispatch
函数使用这个新的 ITypeInfo
,而 IExample3
的 IDispatch
函数使用 IExample3
的 ITypeInfo
。它们不是同一个 ITypeInfo
,也不能互换使用。
剩下要做的就是编写三个额外函数:Count
、Item
和 _NewEnum
。
Count
函数很简单。它接收一个指向 long
的指针。Count
将该指针填充为我们列表中项的总数。例如,上面我们列表中的三个端口(IENUMITEM
结构),因此我们将返回 3。
这是我们的 Count
函数
STDMETHODIMP Count(ICollection *this, long *total) { DWORD count; IENUMITEM *item; // Count how many items in the list by just walking it all // the way to the last IENUMITEM, incrementing a count for // each item. count = 0; item = (IENUMITEM *)&PortsList; while ((item = item->next)) ++count; // Return the total. *total = count; return(S_OK); }
Item
函数也很简单。它接收一个 long
,告诉我们正在查询哪个项(其中 0 是第一项,1 是第二项,依此类推)。它还接收一个 VARIANT
,我们将项的值复制到其中。
这是我们的 Item
函数
STDMETHODIMP Item(ICollection *this, long index, VARIANT *ret) { IENUMITEM *item; // Assume we have nothing to return. ret->vt = VT_EMPTY; // Locate to the item that the caller wants. item = (IENUMITEM *)PortsList; while (item && index--) item = item->next; // Any more items left? if (item) { // Copy the item's value to the VARIANT that the caller supplied. // If what we're returning to the caller is an object, we must AddRef() // it on the caller's behalf. The caller is expected to Release() it // when done with it. If what we're returning is a BSTR, then we must // SysAllocString a copy of it. Caller is expected to SysFreeString it. // Other datatypes are simply copied to the caller's VARIANT as is. // VariantCopy() does all this for us. It also returns S_OK if all // went well. return(VariantCopy(ret, &item->value)); } // If no more items, return 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
。眨眼眨眼。这是我们更新的 IExample3
VTable
// IExample3's VTable #undef INTERFACE #define INTERFACE IExample3 DECLARE_INTERFACE_ (INTERFACE, IDispatch) { // IUnknown functions STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; // IDispatch functions 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; // Extra functions STDMETHOD (SetString) (THIS_ BSTR) PURE; STDMETHOD (GetString) (THIS_ BSTR *) PURE; STDMETHOD (GetPorts) (THIS_ IDispatch **) PURE; // <--- Added GetPorts here };
请注意,我将 GetPorts
添加到了 VTable 的末尾。另请注意,我指定 GetPorts
将填写一个 IDispatch
指针(即使它实际上是我们的 MyRealICollection
)。它是一个 IDispatch
。诚实。我会撒谎吗?
并且,我必须在 IExample3
VTable 的 IDL 文件中进行相同的更改
[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 **); // <--- Added GetPorts here };
请注意,我将这个新添加的函数作为 propget
,就像 Buffer
一样。这样,脚本就可以使用普通的赋值指令来获取我们的 MyRealICollection
... duh!...IDispatch
对象。从脚本的角度来看,该成员名为“Ports
”。我们实际上在 IExample3
对象中没有 Ports
数据成员,这无关紧要。这是一种虚假的成员。但脚本不需要知道这一点。
并且,我任意给它一个 DISPID 为 2。
别忘了,我们需要将此函数添加到 IExample3.c 中的静态声明的 IExample3Vtbl
中
static const IExample3Vtbl IExample3_Vtbl = {QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke, SetString, GetString, GetPorts}; // <--- Added GetPorts here
所以,我们需要编写 GetPorts
函数
static HRESULT STDMETHODCALLTYPE GetPorts(IExample3 *this, IDispatch **portsObj) { // Create an IDispatch to enumerate our port names. // Caller is responsible for Release()'ing // it. NOTE: We're really returning a MyRealICollection, // but the caller doesn't know that. // He thinks we're returning an IDispatch, // which is ok because a MyRealICollection's // VTable starts with the 3 IUnknown functions // followed by the 4 IDispatch functions, just // like a real IDispatch object's VTable if (!(*portsObj = allocPortsCollection())) return(E_OUTOFMEMORY); }
上面只是调用了我们将在 PortNames.c 中放置的另一个辅助函数(名为 allocPortsCollection
)。这个辅助函数负责 GlobalAlloc
一个 MyRealICollection
并对其进行初始化。这与我们 IClassFactory
的 CreateInstance
分配一个 IExample3
并对其进行初始化非常相似。我们甚至增加了未完成对象计数,因为我们的 MyRealICollection
...呃...IDispatch
对象将被提供给某个应用程序(预计之后会 Release
它)。
IDispatch * allocPortsCollection(void) { MyRealICollection *collection; // Allocate a MyRealICollection if ((collection = (MyRealICollection *)GlobalAlloc( GMEM_FIXED, sizeof(MyRealICollection)))) { // Store its VTable collection->lpVtbl = (ICollectionVtbl *)&ICollectionVTable; // AddRef it collection->count = 1; // Indicate another outstanding object since we'll be returning it // to an application which is expected to Release() it InterlockedIncrement(&OutstandingObjects); } // Return it as if it were an IDispatch (which it can be used as) return((IDispatch *)collection); }
我们完成了。你可以编译 IExample3.dll。要注册它,只需获取 IExample2
的注册实用程序(RegIExample2)并将每个“IExample2”替换为“IExample3”。毕竟,IExample3
没有什么需要我们以不同于 IExample2
的方式注册它的。同样,要注销它,请修改 IExample2
的注销实用程序(UnregIExample2)。
VBScript 示例
让我们编写一个使用我们的集合来显示端口名称的 VBScript 示例。我已经编写了这样一个示例(IExample3.vbs),并将其放在 IExample3 目录中。
当然,VBScript 需要首先调用 CreateObject
来获取我们的 IExample3
对象之一。如果安装正确,它应该具有 ProductID “IExample3.object
”。现在脚本拥有了我们的 IExample3
,它就可以简单地访问那个虚假的“Ports
”成员来获取我们 MyRealICollection
... 该死!...IDispatch
对象来使用。这里,我们将其分配给一个名为“coll
”的变量。
Set coll = myObj.Ports
接下来,我们可以调用 Count
函数来确定有多少个端口名称。实际上,因为我们的类型库将此函数定义为 propget
,所以脚本可以使用赋值。
count = coll.Count
现在,我们循环调用 Item
函数来获取每个端口名称,并显示它
For i = 0 To count - 1
MsgBox coll.Item(i)
Next
就是这样。
C 语言示例
对于 C/C++ 应用程序使用我们的集合对象,它需要间接通过 Invoke
函数来调用我们的 Count
和 Item
函数。坐稳了,这将会是一段颠簸的旅程。MS Visual Basic 程序员设计了 IDispatch
函数,以便可以轻松地将参数传递给它们,并从中返回参数,以便那些人可以轻松地为 VB 添加 COM 支持并尽快发布。但他们并没有考虑从其他语言使用 IDispatch
函数的便利性。从 C/C++ 来看,这是一个非常麻烦的事情。
在 IExample3App 目录中,有一个 C 语言应用程序示例,它执行与上面的 VBScript 相同的功能。它获取我们的 Ports
集合对象,并使用它来显示所有端口的名称。我不打算讨论 C 应用程序如何获取我们的 IExample3
对象。这与它获取 IExample2
对象的方式完全相同(除了我们 #include IExample3.h
,并使用 IExample3
对象的 GUID)。
主要关注点从我们调用我们 IExample3
的 GetPorts
函数以获取我们 MyRealICollection
...*咳嗽*...IDispatch
对象开始。这从我在(IExample3App.c 中)放置以下注释的地方开始
// STUDY THIS
仔细阅读所有代码并阅读注释。它们详细说明了您需要采取的步骤才能从 C/C++ 使用 IDispatch
函数。然后,请一位 MS Visual Basic 程序员出去吃午饭以“感谢”他,并在他没注意的时候在他的食物里放很多辣椒。
IEnumVARIANT 对象
如果我们查看我们 Item
函数中的以下几行,可能会引起警觉
// Locate to the item that the caller wants. item = (IENUMITEM *)PortsList; while (item && index--) item = item->next;
每次有人调用我们的 Item
函数时,我们都必须从列表开头开始,搜索到所需的项。假设列表中有 30,000 个项。例如,应用程序要求我们获取第 28,000 项。我们必须遍历 27,000 个项才能找到所需的项。现在,假设应用程序再次调用 Item
来请求第 29,000 项。即使它只差一个链接,我们也会从列表开头重新开始,并遍历 28,000 个项。显然,这效率不高。
我们可以也许为我们的 MyRealICollection
添加另一个数据成员。该成员将存储我们最后一次停留在列表中的“位置”。微软考虑了这一点,然后决定,与其 monkey-ing around with the collection object(由于 VB 开发人员的原因,这个对象实际上并不适合从 C/C++ 高效调用),不如定义第二个标准 COM 对象,称为 IEnumVARIANT
。IEnumVARIANT
的主要目的是存储应用程序“读取”的列表中的当前位置。但考虑到 C/C++ 应用程序通过 Invoke
间接调用我们集合的 Item
函数是多么低效和麻烦,MS 决定在 IEnumVARIANT
中设置一些函数,应用程序可以直接调用这些函数来完成我们之前使用集合的 Item
函数所做的事情……甚至更多。具体来说,IEnumVARIANT
有四个函数(当然,再加上三个 IUnknown
函数),分别命名为 Next
、Skip
、Reset
和 Clone
。
IEnumVARIANT
的Next
函数使我们集合对象的Item
函数变得多余。通过一次调用Next
,应用程序可以一次返回多个项的值(通过提供要返回的项数,以及一个用于存储所有值的VARIANT
数组)。因此,应用程序可以调用Next
来读取四个项。在下一次调用Next
时,我们的IEnumVARIANT
将自动从列表中的第五个项开始读取,并返回应用程序请求的任意数量的值。IEnumVARIANT
的Reset
仅将位置重置回列表的开头。IEnumVARIANT
的Skip
将位置设置为某个点(即,它类似于在磁盘文件中查找,但在这里它是在我们的列表中设置位置)。IEnumVARIANT
的Clone
分配(并返回给应用程序)另一个IEnumVARIANT
对象,其位置与被克隆的IEnumVARIANT
相同。这用于在应用程序想要嵌套循环并且需要记住特定列表中的多个位置的情况下。
微软已经定义了(在其编译器附带的包含文件中)一个 IEnumVARIANT
对象及其 VTable(即,微软已经决定了 VTable 中有哪些函数、这些函数接收什么参数以及它们返回什么)。因此,我们无需执行此操作。但是,与我们迄今为止创建的所有对象一样,我们需要为我们的 IEnumVARIANT
添加一些额外的成员。因此,我们将再次定义一个 MyRealIEnumVariant
,其中包含这些额外成员。
但在我们修改 IExample3
源代码之前,让我们再次创建一个名为 IExample4 的新目录。我们将复制源代码到新目录,然后重命名/编辑它们。搜索并替换“IExample3”为“IExample4”。运行 GUIDGEN.EXE 创建新的 GUID,并将它们放入 IExample4.h、PortNames.h 和 IExample4.idl。再次,我已经为你完成了这些,并创建了一个包含新文件的 IExample4 目录。
在 PortNames.c 中,我们将添加我们 MyRealIEnumVariant
对象的定义,编写其所有函数,并静态声明其 VTable。所有这些都从注释开始
//============================================================== //=================== IEnumVARIANT functions =================== //==============================================================
事实上,现在你应该很熟悉它的 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); }
这只是调用我们名为 allocIEnumVARIANT
的辅助函数,它分配并初始化我们的 IEnumVARIANT
(实际上是 MyRealIEnumVariant
),就像我们 IClassFactory
的 CreateInstance
分配我们的 IExample4
对象,或者我们 IExample4
的 GetPorts
函数分配我们的集合对象一样。这里没有什么新鲜事。
但请注意,_NewEnum
要求应用程序传递一个句柄,我们在其中返回一个 IUnknown
对象——而不是 IEnumVARIANT
。是的,我们实际上返回的是我们的 IEnumVARIANT
,但它伪装成 IUnknown
对象,并且可以做到这一点,因为它的 VTable 以三个 IUnknown
函数开头。
“但为什么要伪装?你不是刚刚说过 _NewEnum
用于获取我们的 IEnumVARIANT
吗?”
是的……以一种迂回的方式。[切换到恐怖怪物电影配乐。]
在之前的文章中,我曾暗示 COM 对象实际上可能包含多个 VTable。我们说这样的对象具有“多个接口”。微软决定 _NewEnum
应该能够传递一个可以有多个接口的对象,而 IEnumVARIANT
可能只是它其中的一个 VTable(甚至不是对象中的第一个 VTable)。因此,应用程序应该做的是获取我们给它的 IUnknown
对象,并调用该对象的 QueryInterface
,传递 IEnumVARIANT
的 GUID(IID_IEnumVARIANT
,它在微软的包含文件中为我们定义)。然后,QueryInterface
将返回指向 IEnumVARIANT
VTable 的指针(即,基本上是从 whatever object that IUnknown
object really is 中嵌入的 IEnumVARIANT
——因为你知道它就是别的什么伪装成 IUnknown
)。
在我们的例子中,应用程序将调用我们 IEnumVARIANT
的 QueryInterface
,要求我们提供一个 IEnumVARIANT
。而我们只会再次返回相同的指针。完全不必要。效率低下。不合逻辑。但 COM 有时就是这样,当其中有些部分是由 MS VB 开发人员设计的(他们对使整个 IDispatch
事情变得非常像 VB 内部调用其自身内置函数很感兴趣,从而最大限度地减少了他们的工作,并将这种设计强加给其他人,无论多么不方便和笨拙),而其他程序员则希望使用多接口之类的东西(VB 脚本甚至无法直接使用——真是个笑话)。
总之,我们已经完成了 IEnumVARIANT
的支持,因此我们可以编译 IExample4.dll。要注册它,再次,您可以修改 RegIExample2.c,搜索并替换“IExample2”为“IExample4”。
另一个 VBScript 示例
VBScript 绝对无法调用我们集合的 _NewEnum
函数(因为这可能会返回一个具有多个接口的对象,需要进行 QueryInterface
——而 VBScript 无法处理这样的对象)。因此,VBScript 无法获取我们的 IEnumVARIANT
并调用其函数。
这是否意味着 IEnumVARIANT
对 VBScript 无用?不。VBScript 引擎本身可以使用我们的 IEnumVARIANT
。引擎何时会这样做?当脚本使用 For Each
循环来枚举我们集合中的项时。在 IExample4 目录中有一个名为 IExample4.vbs 的 VBScript,它使用了这样一个 For Each
循环。这是一种不同的方式,可以让脚本完成与 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 的 ProductID)。
但循环不同。当 VBScript 引擎执行 For Each
行时,它会获取我们的 IEnumVARIANT
(通过调用我们集合的 _NewEnum
,并执行 QueryInterface
以获取 IID_IEnumVARIANT
)。它将此 IEnumVARIANT
存储在内部,以便在循环的后续迭代中使用。然后,它调用我们 IEnumVARIANT
的 Next
函数,请求返回一项的值。当然,第一次调用 Next
时,我们返回第一个项的端口名称(即字符串“Port 1”)。VB 引擎将此字符串存入变量“elem
”。这一切都在这一行 VBScript 指令中完成。现在,脚本只需显示“elem
”的值(即字符串“Port 1”)。在下一次循环迭代中,VB 引擎再次调用我们 IEnumVARIANT
的 Next
函数,请求另一个项的值。这是第二次调用 Next
,因此,当然,我们返回第二项的值,即字符串“Port 2”。VB 引擎现在将 elem
变量更新为这个新值。然后脚本显示“Port 2”。这会一直持续下去,直到 VB 引擎调用我们 IEnumVARIANT
的 Next
,而我们没有更多项要返回。此时,Next
返回 S_FALSE
(而不是 S_OK
)给引擎。然后,引擎退出循环(并 Release
我们的 IEnumVARIANT
)。
另一个 C 语言示例
在 IExample4App 目录中,有一个 C 语言示例演示了如何获取和使用我们的 IEnumVARIANT
。我们仍然需要处理集合对象及其 Invoke
。但至少,循环更有效率。而且整个过程比使用集合对象的 Item
函数要简单一些。
更通用的方法
如果您查看 PortName.c 中的 IEnumVARIANT
和集合函数,您会发现它们是硬编码的,只能作用于我们的端口名称列表(即 PortsList
)。但是,通过一点重构,我们可以重写这些函数,使它们能够作用于我们提供的任何 IENUMITEM
链表。换句话说,我们可以使这些函数通用,因此如果我们的组件需要维护几种不同类型的列表,我们可以更容易地提供进一步的集合和 IEnumVARIANT
对象,它们可以重用这些相同的函数而无需进一步修改。因此,让我们将 IEnumVARIANT
和集合函数分离到一个名为 IEnumVariant.c 的新独立源文件中。唯一留在 PortNames.c 中的代码是专门访问我们 PortsList
的代码。
但首先,我们将执行创建新的 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
中添加的新数据成员相同的功能(即,持有指向 IENUMLIST
的指针,我们的 IEnumVARIANT
将对其进行操作)。
其他更改是微不足道的,但最终留在 PortNames.c 中的只是用于创建和删除我们的 PortsList
的少量代码,以及创建专门包装 PortsList
的集合对象。
要创建另一个列表,并添加对集合和 IEnumVARIANT
对象支持,我们只需要创建一个新的源文件,类似于 PortNames.c 中的内容。事实上,让我们来做这件事。
假设我们要创建一个系统中网络卡的列表。并且对于每张网络卡,我们希望提供两类信息:网络卡的名称,以及其 MAC 地址。我们将代码放在 NetCards.c 和 NetCards.h 中。
我们希望每个项返回多条信息。(即,每个 IENUMITEM
将与一张网络卡相关。对于每张网络卡,我们希望脚本知道网卡的名称和 MAC 地址。)最好的方法是创建一个“子对象”,我们将其任意命名为 INetwork
对象。我们将在该对象中放置两个函数:Name
和 Address
。Name
函数将返回网络卡名称的 BSTR
,Address
函数将返回 MAC 地址的 BSTR
。
我们将为计算机中的每张网络卡创建一个 INetwork
对象。然后,我们将为该网卡在列表中创建一个 IENUMITEM
。我们将把 INetwork
指针放入 IENUMITEM
的 VARIANT
的 punkVal
字段,并将 vt
字段设置为 VT_DISPATCH
。这个包含 INetwork
对象的 IENUMITEM
列表在 allocNetworkObjectsCollection
(在 NetCards.c 中)中创建。
为了使其可从 VBScript 访问,我们需要将 IDispatch
函数包含在我们的 INetwork
VTable 中。当然,这意味着我们需要一个 ITypeInfo
来获取我们 INetwork
VTable 的信息。因此,我们必须为其 VTable 生成一个新的 GUID。并且,我们必须调用 GetTypeInfoOfGuid
,传递该新 GUID,以获取其 ITypeInfo
。我们将它保存在一个名为 NetTypeInfo
的全局变量中。所有这些代码都在 NetCards.c 中。这段代码看起来与我们之前的集合和 IExample5
对象非常相似,因为它们也有 IDispatch
函数并且需要它们的 ITypeInfo
对象。
并且,为了使我们 INetwork
的额外函数(即 Name
和 Address
)能够从 C/C++ 直接调用,我们需要在 IDL 文件中将 VTable 声明为“dual
”。我们还必须在 IExample5.h 中包含其 VTable 定义,以便 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
对象列表。然后,一旦最后一个使用该列表的集合/IEnumVARIANT
被 Release()
d,我们就删除 INetwork
对象列表。
在 IExample5 目录中有一个 VBScript 示例,它执行 For Each
循环来访问我们每个 INetwork
对象,然后调用 Name
和 Address
函数来显示网卡的名称和 MAC 地址。
添加/删除项
有时,我们可能希望给予脚本/应用程序能力来添加或删除列表中的项。传统的做法是将 Add
和 Remove
函数放入我们的集合对象。因为每个列表可能包含不同类型的项,需要不同类型的数据,您必须为特定列表定义另一个集合对象。您将定义其 VTable,并将这两个额外函数(Add
和 Remove
)放入其中。Add
函数必须编写成这样,以便脚本/应用程序可以传递创建新项所需的任何数据。(并且,该函数可以先搜索列表以查看是否已存在匹配项,从而避免重复项)。Remove
函数应传递一些参数,允许您定位要删除的所需项。
当然,您需要为此新集合的 VTable 生成一个 GUID。并且,您需要通过将 GUID 传递给 GetTypeInfoOfGuid
来创建一个 ITypeInfo
,并将其存储在 Invoke
、GetTypeInfo
和 GetIDsOfNames
使用的全局变量中。
好消息是,您可以使用许多与我们的原始集合(ICollection
)和 IEnumVARIANT
相同的函数(在 IEnumVariant.c 中)。因此,需要编写的新代码并不像您预期的那么多。