65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (37投票s)

2006年5月7日

CPOL

30分钟阅读

viewsIcon

115044

downloadIcon

2358

C 语言中的 COM 集合

目录

引言

有时,我们可能需要维护一个项目列表。例如,假设我们的 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 链表将如下所示(我们通常会使用 GlobalAllocIENUMITEM 分配内存,但为了方便说明,我将在下面直接静态声明/初始化它们)

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 函数(QueryInterfaceAddRefRelease)开头,然后紧接着是标准的四个 IDispatch 函数(GetTypeInfoCountGetTypeInfoGetIDsOfNamesInvoke)。然后,我们的对象必须再有三个函数。在我们的 IDL 文件中,当我们定义 VTable(即接口)时,我们必须将这三个额外函数的名称命名为 CountItem_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 中相应函数相同的职责。

最后,我们添加了三个额外函数:CountItem_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。(IExample2Buffer 函数的 ID 也是 1,这无关紧要。这些 VTable 用于两个不同的对象,因此它们的 ID 不需要在这两个 VTable 之间是唯一的)。

稍后,我们将实际编写这些函数。

在我们更改 IExample2 的源代码之前,让我们将整个 IExample2 目录复制到一个名为 IExample3 的新目录中。我们还将重命名所有文件以反映这个新目录(例如,IExample2.h 变成 IExample3.hIExample2.c 变成 IExample3.c 等)。同时,让我们编辑这个新目录中的文件,将我们的 IExample2 对象重命名为 IExample3,以区别于我们之前的源代码。我们所做的就是搜索并替换“IExample2”的每个实例为“IExample3”。并且,不要忘记运行 GUIDGEN.EXEIExample3、其 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 函数(QueryInterfaceAddRefRelease)和 IDispatch 函数(GetTypeInfoCountGetTypeInfoGetIDsOfNamesInvoke)几乎与我们 IExample3 对象相应的函数相同。因此,我不在此处复制代码,而是将您重定向到文件 PortNames.c(位于 IExample3 目录中)。

一个区别是,我们的 ICollection 函数被传递了一个 ICollection 对象指针(而不是 IExample3 对象指针),当然。并且,ICollectionRelease 函数略有不同(因为与 IExample3Release 不同,没有缓冲区成员需要释放)。

另一个关键区别是对 GetTypeInfoOfGuid 的调用。请注意,我们正在传递我们 ICollection VTable 的 GUID(而不是像我们在 IExample3.c 中那样传递我们 IExample3 VTable 的 GUID)。情况是这样的。当我们获取 IExample3ITypeInfo(通过在 IExample3.c 中调用 loadMyTypeInfo)时,我们将 IExample3 VTable 的 GUID 传递给 OLE 函数 GetTypeInfoOfGuid。这意味着 Microsoft 为我们创建的默认 ITypeInfo 仅用于获取有关我们 IExample3 VTable 中函数的信息。它不能用于获取有关另一个对象 VTable 中函数的信息。但是,我们确实需要一个 ITypeInfo 来提供我们 ICollection 函数的信息。因此,现在我们必须再次调用 GetTypeInfoOfGuid,但这次我们传递我们 ICollection 对象 VTable 的 GUID(即,我创建的新 GUID)。这将返回第二个 ITypeInfo(我们将其存储在我们添加的全局变量 CollectionTypeInfo 中)。此第二个 ITypeInfo 可用于我们 ICollectionIDispatch 函数,以获取有关我们 ICollection 函数的信息。它还可以与 DispInvokeDispGetIDsOfNames 在我们 ICollectionInvokeGetIDsOfNames 函数中使用,为我们完成几乎所有工作——就像我们使用 IExample3ITypeInfo 一样。

请注意,ICollectionIDispatch 函数使用这个新的 ITypeInfo,而 IExample3IDispatch 函数使用 IExample3ITypeInfo。它们不是同一个 ITypeInfo,也不能互换使用。

剩下要做的就是编写三个额外函数:CountItem_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 并对其进行初始化。这与我们 IClassFactoryCreateInstance 分配一个 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 函数来调用我们的 CountItem 函数。坐稳了,这将会是一段颠簸的旅程。MS Visual Basic 程序员设计了 IDispatch 函数,以便可以轻松地将参数传递给它们,并从中返回参数,以便那些人可以轻松地为 VB 添加 COM 支持并尽快发布。但他们并没有考虑从其他语言使用 IDispatch 函数的便利性。从 C/C++ 来看,这是一个非常麻烦的事情。

IExample3App 目录中,有一个 C 语言应用程序示例,它执行与上面的 VBScript 相同的功能。它获取我们的 Ports 集合对象,并使用它来显示所有端口的名称。我不打算讨论 C 应用程序如何获取我们的 IExample3 对象。这与它获取 IExample2 对象的方式完全相同(除了我们 #include IExample3.h,并使用 IExample3 对象的 GUID)。

主要关注点从我们调用我们 IExample3GetPorts 函数以获取我们 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 对象,称为 IEnumVARIANTIEnumVARIANT 的主要目的是存储应用程序“读取”的列表中的当前位置。但考虑到 C/C++ 应用程序通过 Invoke 间接调用我们集合的 Item 函数是多么低效和麻烦,MS 决定在 IEnumVARIANT 中设置一些函数,应用程序可以直接调用这些函数来完成我们之前使用集合的 Item 函数所做的事情……甚至更多。具体来说,IEnumVARIANT 有四个函数(当然,再加上三个 IUnknown 函数),分别命名为 NextSkipResetClone

  • IEnumVARIANTNext 函数使我们集合对象的 Item 函数变得多余。通过一次调用 Next,应用程序可以一次返回多个项的值(通过提供要返回的项数,以及一个用于存储所有值的 VARIANT 数组)。因此,应用程序可以调用 Next 来读取四个项。在下一次调用 Next 时,我们的 IEnumVARIANT 将自动从列表中的第五个项开始读取,并返回应用程序请求的任意数量的值。
  • IEnumVARIANTReset 仅将位置重置回列表的开头。
  • IEnumVARIANTSkip 将位置设置为某个点(即,它类似于在磁盘文件中查找,但在这里它是在我们的列表中设置位置)。
  • IEnumVARIANTClone 分配(并返回给应用程序)另一个 IEnumVARIANT 对象,其位置与被克隆的 IEnumVARIANT 相同。这用于在应用程序想要嵌套循环并且需要记住特定列表中的多个位置的情况下。

微软已经定义了(在其编译器附带的包含文件中)一个 IEnumVARIANT 对象及其 VTable(即,微软已经决定了 VTable 中有哪些函数、这些函数接收什么参数以及它们返回什么)。因此,我们无需执行此操作。但是,与我们迄今为止创建的所有对象一样,我们需要为我们的 IEnumVARIANT 添加一些额外的成员。因此,我们将再次定义一个 MyRealIEnumVariant,其中包含这些额外成员。

但在我们修改 IExample3 源代码之前,让我们再次创建一个名为 IExample4 的新目录。我们将复制源代码到新目录,然后重命名/编辑它们。搜索并替换“IExample3”为“IExample4”。运行 GUIDGEN.EXE 创建新的 GUID,并将它们放入 IExample4.hPortNames.hIExample4.idl。再次,我已经为你完成了这些,并创建了一个包含新文件的 IExample4 目录。

PortNames.c 中,我们将添加我们 MyRealIEnumVariant 对象的定义,编写其所有函数,并静态声明其 VTable。所有这些都从注释开始

//==============================================================
//=================== IEnumVARIANT functions ===================
//==============================================================

事实上,现在你应该很熟悉它的 QueryInterfaceAddRefRelease 函数的作用了。而其他四个函数则相当简单,你可以通过阅读源代码注释来了解这些函数的详细信息。

真正的问题是“应用程序如何获取我的 IEnumVARIANT 对象之一?”。还记得之前我们忽略了集合对象的 _NewEnum 函数,并让它返回 E_NOTIMPL 吗?猜猜怎么着。这就是应用程序调用的函数,用于获取我们的 IEnumVARIANT 对象之一,所以现在我们必须编写一些实际的代码。

换句话说,要获取我们的 IEnumVARIANT 之一,应用程序必须首先获取我们的 IExample4 对象,调用我们的 IExample4GetPorts 函数以获取我们的集合对象,然后调用我们集合的 _NewEnum 函数以获取 IEnumVARIANT。这不是最快的方式,但这就是它的工作方式。一些坏消息:C/C++ 应用程序无法直接调用我们集合的 _NewEnum 函数。就像我们集合的 CountItem 函数一样,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),就像我们 IClassFactoryCreateInstance 分配我们的 IExample4 对象,或者我们 IExample4GetPorts 函数分配我们的集合对象一样。这里没有什么新鲜事。

但请注意,_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)。

在我们的例子中,应用程序将调用我们 IEnumVARIANTQueryInterface,要求我们提供一个 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 相同的事情,但在底层更有效率(因为引擎使用我们的 IEnumVARIANTNext 函数,而不是 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 存储在内部,以便在循环的后续迭代中使用。然后,它调用我们 IEnumVARIANTNext 函数,请求返回一项的值。当然,第一次调用 Next 时,我们返回第一个项的端口名称(即字符串“Port 1”)。VB 引擎将此字符串存入变量“elem”。这一切都在这一行 VBScript 指令中完成。现在,脚本只需显示“elem”的值(即字符串“Port 1”)。在下一次循环迭代中,VB 引擎再次调用我们 IEnumVARIANTNext 函数,请求另一个项的值。这是第二次调用 Next,因此,当然,我们返回第二项的值,即字符串“Port 2”。VB 引擎现在将 elem 变量更新为这个新值。然后脚本显示“Port 2”。这会一直持续下去,直到 VB 引擎调用我们 IEnumVARIANTNext,而我们没有更多项要返回。此时,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 函数更通用的关键是向我们的 MyRealIEnumVariantMyRealICollection 对象添加一个额外的数据成员。我们将向我们的 MyRealICollection 添加一个成员,该成员持有指向它应该操作的 IENUMLIST 的指针。并且,我们将编写一个新的辅助函数来分配一个 MyRealICollection 对象。此辅助函数将接收该 IENUMLIST 指针,并将其存储在我们 MyRealICollection 中添加的这个新数据成员中。我已经编写了一个这样的函数(allocICollection)并将其放在 IEnumVariant.c 中。它接收指向我们希望集合对象操作的任何 IENUMLIST 的指针。

我们还将向我们的 MyRealIEnumVariant 添加一个数据成员。它将执行与我们 MyRealICollection 中添加的新数据成员相同的功能(即,持有指向 IENUMLIST 的指针,我们的 IEnumVARIANT 将对其进行操作)。

其他更改是微不足道的,但最终留在 PortNames.c 中的只是用于创建和删除我们的 PortsList 的少量代码,以及创建专门包装 PortsList 的集合对象。

要创建另一个列表,并添加对集合和 IEnumVARIANT 对象支持,我们只需要创建一个新的源文件,类似于 PortNames.c 中的内容。事实上,让我们来做这件事。

假设我们要创建一个系统中网络卡的列表。并且对于每张网络卡,我们希望提供两类信息:网络卡的名称,以及其 MAC 地址。我们将代码放在 NetCards.cNetCards.h 中。

我们希望每个项返回多条信息。(即,每个 IENUMITEM 将与一张网络卡相关。对于每张网络卡,我们希望脚本知道网卡的名称和 MAC 地址。)最好的方法是创建一个“子对象”,我们将其任意命名为 INetwork 对象。我们将在该对象中放置两个函数:NameAddressName 函数将返回网络卡名称的 BSTRAddress 函数将返回 MAC 地址的 BSTR

我们将为计算机中的每张网络卡创建一个 INetwork 对象。然后,我们将为该网卡在列表中创建一个 IENUMITEM。我们将把 INetwork 指针放入 IENUMITEMVARIANTpunkVal 字段,并将 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 的额外函数(即 NameAddress)能够从 C/C++ 直接调用,我们需要在 IDL 文件中将 VTable 声明为“dual”。我们还必须在 IExample5.h 中包含其 VTable 定义,以便 C/C++ 应用程序确切知道这些额外函数的顺序和参数。我已经将我们 INetwork VTable 的定义添加到了 IExample5.hIExample5.idl 中。请注意,它看起来与我们的 IExample5 对象非常相似。两者都包含 IDispatch 函数。两者都被声明为 dual。它们唯一的区别在于它们的额外函数。但是,与我们的 IEnumVARIANT 对象一样,我们自己的 INetwork 对象本身不需要在我们的 IDL 中声明。只需要声明其 VTable。毕竟,我们的 INetwork 对象将像普通的 IDispatch 对象一样呈现给应用程序,只是它的额外函数在 VTable 中,并且 C/C++ 应用程序可以直接调用它们。

通过给我们的 IENUMLIST 添加一个 count 字段,我们可以确定所有集合和 IEnumVARIANT 对象何时完成对其列表的使用,从而在任何时候删除列表。(即,与之前的示例不同,我们不限于仅在我们的 DLL 终止时删除列表。)事实上,您会注意到,直到应用程序实际请求我们的网络卡集合时,我们才创建我们的 INetwork 对象列表。然后,一旦最后一个使用该列表的集合/IEnumVARIANTRelease()d,我们就删除 INetwork 对象列表。

IExample5 目录中有一个 VBScript 示例,它执行 For Each 循环来访问我们每个 INetwork 对象,然后调用 NameAddress 函数来显示网卡的名称和 MAC 地址。

添加/删除

有时,我们可能希望给予脚本/应用程序能力来添加或删除列表中的项。传统的做法是将 AddRemove 函数放入我们的集合对象。因为每个列表可能包含不同类型的项,需要不同类型的数据,您必须为特定列表定义另一个集合对象。您将定义其 VTable,并将这两个额外函数(AddRemove)放入其中。Add 函数必须编写成这样,以便脚本/应用程序可以传递创建新项所需的任何数据。(并且,该函数可以先搜索列表以查看是否已存在匹配项,从而避免重复项)。Remove 函数应传递一些参数,允许您定位要删除的所需项。

当然,您需要为此新集合的 VTable 生成一个 GUID。并且,您需要通过将 GUID 传递给 GetTypeInfoOfGuid 来创建一个 ITypeInfo,并将其存储在 InvokeGetTypeInfoGetIDsOfNames 使用的全局变量中。

好消息是,您可以使用许多与我们的原始集合(ICollection)和 IEnumVARIANT 相同的函数(在 IEnumVariant.c 中)。因此,需要编写的新代码并不像您预期的那么多。

© . All rights reserved.