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

纯 C 语言 COM 组件,第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (95投票s)

2006年4月20日

CPOL

39分钟阅读

viewsIcon

290005

downloadIcon

6241

如何用 C 语言编写一个可供 VBScript、Visual BASIC、jscript 等脚本语言使用的 COM 组件。

目录

引言

在本系列的第一部分中,我们用纯 C 语言创建了一个 COM 组件,将其打包成 DLL,并学习了如何注册它以便任何 C/C++ 应用程序都能使用。我们还创建了一个 C/C++ 头文件 (IExample.h),其中包含了我们的 GUID 和对象 VTable (函数) 的定义。通过包含此 .H 文件,C/C++ 应用程序就知道了我们对象 VTable 的成员名称 (以便调用我们的函数)、要传递的参数、函数的返回值以及我们对象和其 VTable 的 GUID。

虽然 C/C++ 应用程序只需要这些支持,但为了适应 Visual Basic、VBScript、Jscript、Python 等解释型“脚本”语言,我们必须为我们的 IExample DLL 添加额外的支持。本文的重点是如何添加此类支持。添加此支持后,我们将编写一个 VBScript 示例来使用我们更新的 IExample 组件。

为什么脚本语言不能直接使用我们的 DLL?

除非某种语言能够读取我们的 C/C++ 头文件并从中提取信息,否则 Visual Basic、Python 等应用程序根本不知道我们的对象中有哪些函数、这些函数接受什么参数、它们返回什么,以及我们的 GUID 是什么。大多数语言不支持从 C/C++ 头文件中提取信息。我们需要一种方法以一种与语言无关的格式来指定 IExample.h 中包含的所有信息,最好是一种紧凑的“二进制”格式,而不是 IExample.h。稍后我们将看到,我们通过创建所谓的“类型库”来做到这一点。

为了让脚本引擎更容易、更标准化地解析我们“类型库”的内容,我们将向我们的对象添加一些函数,称为 IDispatch 函数。脚本引擎只能通过这些 IDispatch 函数获取类型库信息。只有我们的对象直接访问其自身的类型库。通过不让脚本引擎直接访问我们的类型库,这意味着如果 Microsoft 开发了类型库的后续版本,这些复杂的版本细节将对脚本引擎隐藏。

数据类型也存在另一个问题。考虑 ANSI C 中的字符串。C 字符串是由一系列 8 位字节组成的,以一个 0 字节结尾。但这并非 Pascal 字符串的存储方式。Pascal 字符串以一个字节开头,该字节指示后面有多少字节。换句话说,Pascal 字符串以长度字节开头,然后是其余的字节。没有终止的 0 字节。那么 UNICODE 与 ANSI 呢?在 UNICODE 中,字符串中的每个字符实际上是 2 个字节 (即一个 short)。Visual Basic 字符串是 UNICODE,与 ANSI C 不同。

我们需要解决这些数据类型问题。我们需要一套适用于使用我们对象的任何语言的“通用数据类型”,并仅将这些数据类型用于我们对象函数的参数 (以及我们函数返回的任何数据)。

自动化数据类型

为了支持任何语言 (以及每种语言的扩展,如 UNICODE),Microsoft 定义了一组 COM 对象应使用的通用数据类型。Microsoft 称这些为“自动化数据类型”。(稍后我们将了解“自动化”一词的来源。) 其中一种自动化数据类型是 BSTR。如果 COM 对象接收一个字符串,则该参数可以采用 BSTR 的形式。什么是 BSTR?它是一个指向特殊格式化字符串的指针。每个字符是两个字节 (即一个 short),并且前面有一个无符号长整型,指示有多少个短整型字符,并在末尾有一个 0 短整型。这兼容几乎所有语言/扩展的“字符串”数据类型。但这同时也意味着,C/C++ 应用程序,或者我们自己的 C 对象,有时需要将 C 字符串重新格式化为以长度为前缀的、以 null 结尾的 UNICODE 字符串。

仅为举例说明,我们来看以下 ANSI C 字符串

char MyString[] = "Some text";

首先,我们将字符串分解成单独的字符

char MyString[] = {'S','o','m','e',' ','t','e','x','t', 0};

由于 C 字符串是以 null 结尾的,请注意我们将最后一个 0 字节也包含在数组中。

现在,我们需要将字符串转换为 UNICODE。这意味着每个 char 都变成一个 short。我们将重新定义数组为 short

short MyString[] = {'S','o','m','e',' ','t','e','x','t', 0};

现在,我们需要在数组前面加上数组的字节数。请注意,我们不将终止的零计算为 short。因此,我们有 9 个 short 的长度 * 每个 short 2 个字节 (注意 Intel Little Endian 字节序)

short MyString[] = {18, 0, 'S','o','m','e',' ','t','e','x','t', 0};

BSTR 定义为指向此特殊格式化的 UNICODE 字符串的指针。实际上,它指向 UNICODE 字符串中的第三个 short,因为长度应该放在实际字符串的前面。因此,我们声明一个 BSTR 类型的变量,并将其设置为指向上面的字符串

BSTR  strptr;

strptr = &MyString[2];

现在我们可以将 strptr 传递给期望 BSTR 作为参数的 COM 对象。如果我们的对象函数之一期望 BSTR 参数,那么 strptr 就是将要传递的内容。

但在你认为必须手动重新格式化所有字符串之前,这是不必要的。幸运的是,有一个名为 MultiByteToWideChar 的操作系统函数可以将 ANSI C 字符串转换为 UNICODE,还有 SysAllocStringSysAllocStringLen 等函数,它们会分配一个缓冲区来复制 UNICODE 字符串,前面会有一个无符号长整型,表示该缓冲区的大小。因此,要将我们原始的 char 字符串转换为指向它的 BSTR 指针 (省略错误检查),我们可以这样做

DWORD len;
BSTR  strptr;
char  MyString[] = "Some text";

// Get the length (in wide chars) of the UNICODE buffer we'll need to
// convert MyString to UNICODE.
len = MultiByteToWideChar(CP_ACP, 0, MyString, -1, 0, 0);

// Allocate a UNICODE buffer of the needed length. SysAllocStringLen will
// also allocate room for a terminating 0 short, and the unsigned long count.
// SysAllocStringLen will fill in the unsigned long with the value of
// "len * sizeof(wchar_t)" and then return a pointer to the third short in
// the buffer it allocates. All that is left for us to do is simply stuff a
// UNICODE version of our C string into this buffer.
strptr = SysAllocStringLen(0, len);

// Convert MyString to UNICODE in the buffer allocated by SysAllocStringLen.
MultiByteToWideChar(CP_ACP, 0, MyString, -1, strptr, len);

// strptr is now a pointer (BSTR) to an Automation string datatype.

注意: 之后必须有人通过将 strptr 传递给 SysFreeString 来释放 SysAllocStringLen 分配的缓冲区。通常,这个“有人”就是调用 SysAllocStringLen 来分配它的人。

请注意,由于 BSTR 指向无符号长整型大小之后,并且字符串无论如何都是以 null 结尾的,所以你可以将 BSTR 视为一个宽字符 (wchar_t) 数据类型。例如,你可以将其传递给 lstrlenW 来获取宽字符的长度。但也要注意,字符串中可能包含嵌入的 0。如果你编写了一个期望接收人类可读文本字符串的函数,那么 BSTR 指向包含嵌入零的内容的可能性很小。另一方面,如果 BSTR 指向二进制数据,那么这个假设可能不成立。在这种情况下,你可以使用 SysStringLen 来确定宽字符的实际长度 (或者如果你需要字节长度,则使用 SysStringByteLen)。

还有其他的“自动化数据类型”,例如 long (即 32 位值)。因此,我们对象函数不限于只接收自动化字符串指针。

事实上,有些语言支持函数可以接收可以是多种数据类型之一的参数。假设我们有一个 Print() 函数。假设这个 Print 函数接受一个参数,该参数可以是字符串指针 (BSTR),也可以是 long,或者可能是其他各种自动化数据类型,并且它将打印它接收到的任何内容。例如,如果传递一个 BSTR,它将打印该字符串的字符。如果传递一个 long,它将首先执行类似调用

sprintf(myBuffer, "%ld", thePassedLong)

...然后打印 myBuffer 中的结果字符串。

这个 Print 函数需要某种方式来知道它接收的是字符串还是 long。如果我们想将这个 Print 函数放入我们的对象中,我们可以让应用程序传递另一个名为 VARIANT 的自动化数据类型。VARIANT 只是一个封装了另一个自动化数据类型的结构。(这是所有支持 COM 的语言必须在内部理解并能够创建的一种“结构”,仅仅是为了将参数传递给对象的函数,或接收回数据。)

VARIANT 有两个成员。第一个成员 vt 设置为某个值,该值告诉我们 VARIANT 封装的是哪种数据类型。另一个成员是一个联合体,用于存储其他数据类型。

因此,如果我们 BSTR 包装到 VARIANT 结构中,那么我们将 VARIANTvt 成员设置为 VT_BSTR,并将我们的 BSTR (即指向该特殊格式化的 UNICODE 字符串的指针) 存储在 VARIANTbstrVal 成员中。如果我们包装一个 long,那么我们将 VARIANTvt 成员设置为 VT_I4,并将我们的 long 存储在 VARIANTlVal 成员中 (由于联合体的存在,它实际上与 bstrVal 是同一个成员)。

总结,为了支持脚本语言,我们的对象函数必须编写成只接收特定类型的数据,称为自动化数据类型。同样,我们返回的任何数据都必须是自动化数据类型。

如果你查看我们 SetString 函数的定义,你会注意到一个问题。我们将其定义为接收一个 char 指针,如下所示

static HRESULT STDMETHODCALLTYPE SetString(IExample *this, char *str)

这是一个 ANSI C 字符串,它不是自动化数据类型。我们需要用 BSTR 参数替换它。

我们的 GetString 函数对脚本语言来说更麻烦。它不仅被定义为接受 ANSI C 字符串指针,而且调用者还需要提供那个非 UNICODE 缓冲区,我们的函数会修改其内容。这不兼容自动化。将 BSTR 返回给脚本语言时,**我们**必须分配字符串并将其交给脚本引擎。脚本引擎将在完成后调用 SysFreeString 来释放该字符串。

考虑到这些要求,最好的做法是更改我们的 IExample 对象,使其缓冲区成员成为 BSTR。当脚本引擎将 BSTR 传递给我们的 SetString 时,我们将复制该字符串,并将这个新的 BSTR 存储在缓冲区成员中。然后,我们将修改我们的 GetString 函数,使其接受脚本引擎提供的指向 BSTR 的指针 (即句柄)。我们将创建另一个字符串副本并将其返回给脚本引擎 (相信脚本引擎会释放它)。

为了不更改我们原始的源代码,我将 IExample 目录的内容复制到一个名为 IExample2 的新目录中。我将 IExample.c 改为 IExample2.c,将 IExample.h 改为 IExample2.h,依此类推。

IExample2.c 中,让我们将 MyRealIExample 结构重命名为 MyRealIExample2 (以区别于我们原始源代码),并更改缓冲区成员的数据类型。(我们还将成员名称更改为“string”)。这是它的新定义

typedef struct {
   IExample2Vtbl  *lpVtbl;
   DWORD          count;
   BSTR           string;
} MyRealIExample2;

这是我们更新的 SetStringGetString 函数

static HRESULT STDMETHODCALLTYPE SetString(IExample2 

*this, BSTR str)
{
   // Make sure that caller passed a string
   if (!str) return(E_POINTER);

   // First, free any old BSTR we allocated
   if (((MyRealIExample2 *)this)->string)
       SysFreeString(((MyRealIExample2 *)this)->string);

   // Make a copy of the caller's string and save this new BSTR
   if (!(((MyRealIExample2 *)this)->string = 
           SysAllocStringLen(str, SysStringLen(str))))
      return(E_OUTOFMEMORY);

   return(NOERROR);
}

static HRESULT STDMETHODCALLTYPE GetString(IExample2 *this, BSTR *string)
{
   // Make sure that caller passed a handle
   if (!string) return(E_POINTER);

   // Create a copy of our string and return the BSTR in his handle. The
   // caller is responsible for freeing it
   if (!(*string = SysAllocString(((MyRealIExample2 *)this)->string)))
      return(E_OUTOFMEMORY);

   return(NOERROR);
}

另外,我们的 Release 函数必须在释放 MyRealIExample2 之前释放已分配的字符串。

   if (((MyRealIExample2 *)this)->string)
           SysFreeString(((MyRealIExample2 *)this)->string);
   GlobalFree(this);

最后,当我们的 IClassFactory 创建 MyRealIExample2 时,它应该清除字符串成员。

((MyRealIExample2 *)thisobj)->string = 0;

当然,我们需要在 IExample2.h 中更新这些函数定义。

// Extra functions
STDMETHOD  (SetString)   (THIS_ BSTR) PURE;
STDMETHOD  (GetString)   (THIS_ BSTR *) PURE;

现在,我们已经更新了对象函数以使用自动化数据类型。

IDispatch 函数

我们的下一步是向对象添加 IDispatch 函数。有四个 IDispatch 函数,已由 Microsoft 定义。我们必须编写这四个函数,然后将它们的指针添加到对象的 VTable 中。

重要提示: 在我们的 VTable 中,这四个指针必须分别命名为 GetTypeInfoCountGetTypeInfoGetIDsOfNamesInvoke。它们必须按此顺序出现,位于三个 IUnknown 函数 (即 VTable 中的前三个指针) 之后,但在指向我们自己额外函数的任何指针 (即 SetStringGetString) 之前。因此,让我们编辑 IExample2.h,并添加这四个 IDispatch 函数的定义。我们将对象名称从 IExample 更改为 IExample2。另外,请注意,在我们的宏的 DECLARE_INTERFACE_ 行中,我们将 IDispatch 替换了 IUnknown。这表示我们将标准的 IDispatch 函数添加到我们的对象中。

// IExample2's VTable
#undef  INTERFACE
#define INTERFACE IExample2
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;
};

因为我正在创建另一个 COM 组件,所以我运行了 GUIDGEN.EXE 来为我的 IExample2IExample2VTbl 创建新的 GUID,并将它们粘贴到 IExample2.h 中。这使得我们可以在不卸载原始 DLL 的情况下测试这个新 DLL,也不会让新 DLL 与原始 DLL 冲突。否则,如果我们使用相同的 GUID,我们将有两个具有相同 GUID 的已注册 COM 组件 (DLL)。这不好。

我还运行了 GUIDGEN.EXE 第三次来生成第三个 GUID。我们将需要它来生成我们的类型库。我将其添加到 IExample2.h 中,并给它一个 C 变量名 CLSID_TypeLib

现在,我们需要编写这四个 IDispatch 函数。幸运的是,Microsoft 的 OLE32 DLL 包含一些通用的函数,我们可以调用它们来完成这四个函数的大部分工作。我们唯一需要做的真正工作是加载我们的类型库 (我们将在本文稍后创建),并创建一个名为 ITypeInfo 的 COM 对象交给脚本引擎。但即使在那里,Microsoft 也提供了一些函数来完成很多工作,即 LoadRegTypeLib 来加载类型库,以及 GetTypeInfoOfGuid 来创建 ITypeInfo

我们的 GetTypeInfoCount 函数很简单。调用者向我们传递一个 UINT 的句柄。如果存在类型库,我们将其设置为 1,否则设置为 0。我们这样做

ULONG STDMETHODCALLTYPE GetTypeInfoCount(IExample2 *this, UINT *pCount)
{
   *pCount = 1;
   return(S_OK);
}

我们的 GetTypeInfo 函数必须 (向脚本引擎) 返回一个 ITypeInfo 对象的指针,该对象由我们的类型库创建。ITypeInfo 对象做什么?它管理脚本引擎对我们类型库中所有信息的访问。记住,我们说过我们不会让任何人直接加载和访问我们的类型库。相反,我们给脚本引擎一个 ITypeInfo 对象,该对象的函数会解析我们的类型库信息并返回我们 IExample2 对象中某个函数 الخاص信息。例如,一旦我们将 ITypeInfo 给了脚本引擎,该引擎就可以调用我们 ITypeInfoGetFuncDesc 函数来确定我们 GetString 函数有多少个参数,每个参数的类型是什么,以及 GetString 在有返回值时返回什么。

“哦,太好了。又要写更多函数了”,你可能在想。是的,我们可以编写 ITypeInfo 对象所需的所有函数,并将它们放在 IExample2.c 中。然后,IExample2GetTypeInfo 函数可以分配我们的一个 ITypeInfo 对象,初始化它 (即将其 VTable 存储在其中),并将其返回给脚本引擎。

但是,我们不会这样做。我们将省去一些工作。Microsoft 已经编写了一些通用的 ITypeInfo 函数 (包含在 OLE DLL 中),并提供了一个我们可以调用的函数,该函数会为我们分配一个 ITypeInfo,并用指向这些通用函数的 VTable 填充它。然后,我们可以将这个“通用” ITypeInfo 提供给脚本引擎。我们只需要确保我们的 IExample2 对象被定义为所谓的“双重接口”。现在不要担心它是什么。但我们将遵守这个要求。

在我们深入研究 GetTypeInfo 函数之前,我想说一点关于类型库的事情。它可以包含供人类显示的字符串。例如,如果我们愿意,我们可以为我们的 GetString 函数添加一个“文本描述”,内容为:此函数返回 IExample2 对象的字符串。然后,例如,Visual BASIC IDE 可以获取我们的 ITypeInfo 对象,调用其 GetDocumentation 函数来检索 GetString 的此描述,并将其显示给想要调用我们 IExampleGetString 的 VB 程序员。有许多人类语言,所以就像你的 EXE/DLL 的资源可以是不同语言一样,类型库也有一个“语言 ID” (LCID)。本文我们将只创建一个英语类型库,并忽略与加载特定语言类型库相关的任何问题。只需知道脚本引擎可能会向我们的 GetTypeInfo 函数传递一个非英语的 LCID,如果它想要其他语言。

GetTypeInfo 定义如下

ULONG STDMETHODCALLTYPE GetTypeInfo(IExample2 *this, 
      UINT itinfo, LCID lcid, ITypeInfo **pTypeInfo)

itinfo 参数未使用,应为 0。LCID 指定使用哪种语言类型库。我们再次忽略这一点,因为我们只处理英语。最后一个参数是脚本引擎期望我们返回 ITypeInfo 对象指针的地方。

我们只需要一个 ITypeInfo 对象,因为其内容不会改变。因此,我们的 GetTypeInfo 函数将简单地确保我们的类型库已加载,并且我们已获得指向 Microsoft 通用 ITypeInfo 的指针。我们将此指针存储在一个全局变量中。我们确实需要每次脚本引擎调用我们的 GetTypeInfo 时增加 ITypeInfo 的引用计数 (即调用其 AddRef 函数)。(当然,我们期望脚本引擎在完成 ITypeInfo 后调用 ITypeInfoRelease)。所以,这是我们的 GetTypeInfo 函数,以及我们编写的一个辅助函数 (loadMyTypeInfo),它负责加载我们的类型库并获取 Microsoft 通用 ITypeInfo。请注意,我们的辅助函数是如何调用 OLE API LoadRegTypeLib 来完成加载我们类型库的所有工作的。我们只需要传递我为类型库创建的 GUID (并将其放入 IExample2.h,将其命名为 CLSID_TypeLib)。

ULONG STDMETHODCALLTYPE GetTypeInfo(IExample2 *this, 
      UINT itinfo, LCID lcid, ITypeInfo **pTypeInfo)
{
   HRESULT  hr;

   // Assume an error.
   *pTypeInfo = 0;

   if (itinfo) hr = ResultFromScode(DISP_E_BADINDEX);

   // If our ITypeInfo is already created,
   // just increment its ref count.
   else if (MyTypeInfo)
   {
      MyTypeInfo->lpVtbl->AddRef(MyTypeInfo);
      hr = 0;
   }
   else
   {
      // Load our type library and
      // get Microsoft's generic ITypeInfo object.
      hr = loadMyTypeInfo(this);
   }

   if (!hr) *pTypeInfo = MyTypeInfo;

   return(hr);
}

static HRESULT loadMyTypeInfo(void)
{
   HRESULT   hr;
   LPTYPELIB pTypeLib;

   // Load our type library and get a ptr to its TYPELIB. Note: This does an
   // implicit pTypeLib->lpVtbl->AddRef(pTypeLib).
   if (!(hr = LoadRegTypeLib(&CLSID_TypeLib, 1, 0, 0, &pTypeLib)))
   {
      // Get Microsoft's generic ITypeInfo,
      // giving it our loaded type library. We only
      // need one of these, and we'll store
      // it in a global. Tell Microsoft this is for
      // our IExample2's VTable, by passing that VTable's GUID.
      if (!(hr = pTypeLib->lpVtbl->GetTypeInfoOfGuid(
            pTypeLib, &IID_IExample2, &MyTypeInfo)))
      {
         // We no longer need the ptr
         // to the TYPELIB now that we've given it
         // to Microsoft's generic ITypeInfo.
         // Note: The generic ITypeInfo has done
         // a pTypeLib->lpVtbl->AddRef(pTypeLib),
         // so this TYPELIB ain't going away
         // until the generic ITypeInfo does
         // a pTypeLib->lpVtbl->Release too.
         pTypeLib->lpVtbl->Release(pTypeLib);

         // Since caller wants us to return our ITypeInfo pointer,
         // we need to increment its reference count. Caller is
         // expected to Release() it when done.
         MyTypeInfo->lpVtbl->AddRef(MyTypeInfo);
      }
   }

   return(hr);
}

稍后创建类型库时,我们需要为添加到 IExample2 的每个函数 (即 GetStringSetString) 分配一个数字值,Microsoft 称之为 DISPID (Dispatch ID)。这可以是任何我们选择的值,但每个函数都必须有一个唯一的 DISPID。(注意:不要使用 0 或负数值)。我们将任意将 GetString 分配 DISPID 1,将 SetString 分配 DISPID 2。脚本引擎在内部使用此 DISPID 来更快地调用我们的函数,因为通过数字值匹配在某个表中查找函数比比较函数名称字符串更快。

接下来,我们编写 GetIDsOfNames 函数。此函数的作用是允许脚本引擎将我们添加到 IExample2 的某个函数 (即 GetStringSetString) 的名称传递给我们,我们将返回我们为此函数分配的 DISPID。例如,如果引擎传递给我们 “GetString”,那么我们将返回 DISPID 1。

由于我们将在创建类型库时将 IExample2 定义为“双重接口”,因此我们可以调用 OLE 函数 DispGetIDsOfNames,该函数将完成我们 GetIDsOfNames 需要做的所有工作 (即解析类型库中的信息以查找我们分配给特定函数的 DISPID)。我们唯一需要做的就是确保我们获得了 ITypeInfo,因为它需要传递给 DispGetIDsOfNames。所以,这是我们的 GetIDsOfNames

ULONG STDMETHODCALLTYPE GetIDsOfNames(IExample2 *this, 
      REFIID riid, LPOLESTR *rgszNames, UINT cNames, 
      LCID lcid, DISPID *rgdispid)
{
   if (!MyTypeInfo)
   {
      HRESULT hr;

      if ((hr = loadMyTypeInfo())) return(hr);
   }

   // Let OLE32.DLL's DispGetIDsOfNames()
   // do all the real work of using our type
   // library to look up the DISPID
   // of the requested function in our object
   return(DispGetIDsOfNames(MyTypeInfo, rgszNames, 
                            cNames, rgdispid));
}

我们还有最后一个 IDispatch 函数要写 - Invoke。这是脚本引擎调用以 (间接) 调用我们添加到 IExample2 的另一个函数 (即 GetStringSetString) 的函数。脚本引擎传递要调用的函数的 DISPID (即 GetString 为 1,SetString 为 2)。引擎还传递一个 VARIANT 数组,其中填充了要传递给 GetStringSetString 的参数。它还传递一个 VARIANT,我们需要用函数中的任何返回值来填充 (不是 InvokeHRESULT 返回值,而是我们的类型库表明 GetStringSetString 的返回值)。

所以,我们的 Invoke 函数首先查看 DISPID。如果它是 1,我们就知道必须调用 GetString。如果它是 2,我们就知道必须调用 SetString。所以,你立即应该想到一个 *case* 语句。如果向 IExample2 添加了很多函数,那么它可能是一个很大的 case 语句。

接下来,我们需要从 VARIANT 中提取这些参数并调用正确的函数,将参数传递给它。例如,如果脚本正在调用 SetString,我们需要从第一个 VARIANT 中提取 BSTR 并将其传递给 SetString

这里有个问题。脚本引擎可以传递一个 VARIANT,该 VARIANT 封装的可能是我们期望的不是精确数据类型,而是可以强制转换为正确类型的数据类型。我们的 SetString 期望一个 BSTR (字符串指针)。但是,假设脚本引擎想要将我们的字符串成员设置为字符串 "10"。现在,引擎可以通过以下方式设置传递给我们的 VARIANT

VARIANT myArg;

myArg.vt = VT_BSTR;
myArg.bstrVal = SysAllocString(L"10");

好了,没问题。它将一个 BSTR 包装在那个 VARIANT 中传递给我们。

但是引擎也可以像这样传递一个 long

VARIANT myArg;

myArg.vt = VT_I4;
myArg.lVal = 10;

现在,我们的 Invoke 函数必须看到这不是 VT_BSTR,并且在我们调用 SetString 之前自己进行转换 (省略错误检查)

// Determine which function to call based upon the DISPID
// the caller passed us.
switch (dispid)
{
   // GetString()
   case 1:
      // Here we'd call GetString with the appropriate args.
      break;

   // SetString()
   case 2:
      // Did he pass the BSTR that SetString expects?
      if (params->rgdispidNamedArgs[0]->vt != VT_BSTR)
      {
         // Nope. We've got to try to convert whatever he passed
         // into a BSTR.

         // Did he pass a long?
         if (params->rgdispidNamedArgs[0]->vt == VT_I4)
         {
             // Create a BSTR out of that long.
             wchar_t temp[33];

             wsprintW(temp, L"%d", params->rgdispidNamedArgs[0]->lVal);
             params->rgdispidNamedArgs[0]->bstrVal = SysAllocString(temp);

             // NOTE: The engine will free this BSTR when it calls
             // VariantClear() on params->rgdispidNamedArgs[0].
         }
         else
         {
            // Here we need to check/convert even more possible datatypes!
            // If we exhaust the logical possibilites, return DISP_E_TYPEMISMATCH.
         }
     }

     // Call SetString with the BSTR arg.
     return(this->lpVtbl->SetString(this, params->rgdispidNamedArgs[0]->bStrVal));
}

“哦,太好了!考虑到有很多函数,这可能对我来说会变成一项繁重的工作!”,你正在想。是的,它可能会。但是,至少 Microsoft 提供了一堆 OLE API 来将 VARIANT 从一种类型转换为另一种类型。所以,我们可以简单地这样做

// SetString()
case 2:
   if ((hr = VariantChangeType(¶ms->rgdispidNamedArgs[0],
       ¶ms->rgdispidNamedArgs[0], 0, VT_BSTR))) return(hr);
   return(this->lpVtbl->SetString(this, 
          params->rgdispidNamedArgs[0]->bStrVal));

但是,还有更好的。由于我们将 IExample2 定义为双重接口,我们可以调用 Microsoft 函数 DispInvoke 来为我们完成**所有**工作。它使用我们的类型库将 DISPID 与要调用的正确函数匹配,检查/转换参数,调用正确的函数,甚至将返回值消息转换为引擎为此目的传递的 VARIANT。我们唯一需要做的,就像在 GetIDsOfNames 函数中一样,就是确保我们获得了 ITypeInfo,因为它需要传递给 DispInvoke。所以,这是我们的 Invoke

ULONG STDMETHODCALLTYPE Invoke(IExample2 *this, 
      DISPID dispid, REFIID riid, LCID lcid,
      WORD wFlags, DISPPARAMS *params, VARIANT *result, 
      EXCEPINFO *pexcepinfo, UINT *puArgErr)
{
   // We implement only a "default" interface
   if (!IsEqualIID(riid, &IID_NULL))
      return(DISP_E_UNKNOWNINTERFACE);

   // We need our ITypeInfo (to pass to DispInvoke)
   if (!MyTypeInfo)
   {
      HRESULT hr;

      if ((hr = loadMyTypeInfo())) return(hr);
   }

   // Let OLE32.DLL's DispInvoke() do all
   // the real work of calling the appropriate
   // function in our object, and massaging
   // the passed args into the correct format
   return(DispInvoke(this, MyTypeInfo, dispid, wFlags, 
          params, result, pexcepinfo, puArgErr));
}

这样就完成了所有的 IDispatch 函数。

总结,为了支持脚本语言,我们的对象 VTable 必须包含四个标准的 IDispatch 函数。它们必须紧随在最初的三个 IUnknown 函数 (QueryInterfaceAddRefRelease) 之后。为了方便实现它们,我们可以将 VTable 定义为“双重接口” (在我们的类型库中),然后调用一些 Microsoft OLE 函数来完成大部分实际工作。

类型库

我们的下一步是创建类型库。首先,我们需要创建包含我们的对象及其 VTable 定义以及它们的 GUID 的“源”文件,这与我们为 C 编译器创建 IExample.h 类似。但是,这种源文件的格式将与 IExample.h 不同,我们将使用一个名为 MIDL.EXE 的特殊实用程序来编译它 (它随您的编译器一起提供,或者可以在 Microsoft 的 Windows Platform SDK 中找到)。

通常,此源文件的名称以 .IDL 扩展名结尾。如果是这样,当我们将其添加到 Visual C++ 项目时,Visual C++ IDE 将自动调用 MIDL.EXE 对其进行处理,将其转换为一个文件名以 .TLB 扩展名结尾的二进制文件。该 TLB 文件就是我们的类型库。

IExample2 目录中,您会找到文件 IExample2_extra.idl,它是我们类型库的源文件。语法与 C++ 有些相似。以 // 开头的行是注释。并且,使用 C 风格的大括号 {} 来包含特定的“结构”或实体。

IDL 以以下行开始

[uuid(E1124082-5FCD-4a66-82A6-755E4D45A9FC), version(1.0), 

helpstring("IExample2 COM server")]
library IExample2
{

第一行包含在方括号中。它包含三个特定信息位,每个信息位用逗号分隔。第一个信息位是我们类型库的 GUID。请注意,它与 IExample2.hCLSID_TypeLib 指定的 GUID 相同 (即,那个我为我们的类型库生成的额外 GUID)。

接下来是我们的类型库的版本号 (我选择的是 1.0)。

最后是任何帮助描述 (针对我们的类型库),我们希望某个对象浏览器向程序员显示 (例如,Visual BASIC 的对象浏览器可能会这样做)。这里,我标识了我的 COM 组件是什么/做什么。例如,如果你正在创建一个读取 UPC 条形码的 COM 组件,你的帮助字符串可能是“Acme Brand UPC bar code reader”。

下一行必须以 library 关键字开头。之后,你可以给类型库起一个名字。我选择了 IExample2,但它可以是任何你选择的名字。请注意,这与 TLB 文件的实际文件名无关。

最后,我们有一个开括号。此括号与我们 IDL 源中的最终闭括号之间的所有内容都将是我们类型库的一部分。在这里,我们放入了我们 IExample2 对象及其 VTable 的定义。

下一行

importlib("STDOLE2.TLB");

就像 .H 文件中的 #include 语句一样。它包含了 Microsoft 的 STDOLE2.TLB 文件 (随你的编译器一起提供,或在 SDK 中),该文件定义了 IUnknownIDispatch 等基本 COM 对象。我们需要引用它,因为我们的 IExample2 VTable 的开头有 IDispatch 函数 (因此也有 IUnknown 函数)。

接下来是我们 IExample2 的 VTable 定义的开始。它看起来是这样的

[uuid(B6127C55-AC5F-4ba0-AFF6-7220C95EEF4D), dual, 
      oleautomation, hidden, nonextensible]
interface IExample2Vtbl : IDispatch
{

第一行包含在方括号中,它包含我们 IExample2 的 VTable 的 GUID (你会注意到它与 IExample2.h 中标识为 IID_IExample2 的 GUID 相同;必须如此)。

这一行还包含几个描述我们 VTable 的关键字。最重要的关键字是“dual”。这就是我们指示 IExample2 是“双重接口”的方式。这意味着,除了允许某个 C/C++ 应用程序通过 IExample2 的 VTable 直接调用我们的 SetStringGetString 函数 (就像你在第一篇文章的演示 C 应用程序中看到的那样),该 VTable 还在其中包含了四个 IDispatch 函数,脚本引擎 (如 VBScript) 可以使用这些函数来 (间接) 调用 SetStringGetString,通过 Invoke。这就是为什么它被称为“dual”。这个 VTable 对于想要直接调用我们 SetStringGetString 函数的 C/C++ 应用程序以及需要通过我们的 Invoke 函数调用它们的脚本引擎来说都是有效的。

oleautomation 关键字表示我们仅将自动化数据类型用于传递给 SetStringGetString 函数的参数,以及任何返回值。

注意: dual 关键字会自动包含 oleautomation。所以,我们实际上不需要指定 oleautomation。但我还是这样做了。如果你用 oleautomation 关键字标识一个 VTable,然后尝试将非自动化数据类型用作函数参数 (或返回值),MIDL.EXE 将会报错。请注意,较旧版本的 MIDL.EXE 可能会将较新的自动化数据类型标记为错误,因此请确保使用最新 Platform SDK 中的 MIDL.EXE

hidden 关键字的意思是我们不希望我们的 VTable 本身出现在某个对象浏览器中。我们只想显示我们稍后定义的 IExample2 对象。

nonextensible 关键字表示当我们实际分配并向某人提供 IExample2 对象时,我们不会做一些意外的事情,例如向 IExample2 的 VTable 添加额外的函数 (未在类型库中定义)。

下一行必须以 interface 开头,以指示我们正在定义一个 VTable。之后是你决定在此 IDL 源中为此 VTable 提供的名称。我们可以将其称为 IExample2Vtbl,或其他任何名称。我选择了“IExample2Vtbl”。我们还指定了 IDispatch 类型,因为我们的 IExample2 VTable 的开头确实包含一个 IDispatch (当然,之前还有一个 IUnknown,因为 IDispatch 也包含 IUnknown)。

注意: 如果我们的 VTable 没有四个 IDispatch 函数,那么我们将将其定义为 IUnknown 类型。

最后,我们有一个 C 开括号。在此之后,我们列出了 VTable 中的所有函数。它看起来是这样的

[helpstring("Sets the test string.")]
[id(1)] HRESULT SetString([in] BSTR str);
[helpstring("Gets the test string.")]
[id(2)] HRESULT GetString([out, retval] BSTR *strptr);

首先,你会注意到我们没有定义 IUnknown (QueryInterfaceAddRefRelease) 或 IDispatch (GetTypeInfoCountGetTypeInfoGetIDsOfNamesInvoke) 函数。我们不必这样做,因为我们指定 IDispatch 作为我们的 VTable 类型,所以 MIDL.EXE 会自动添加这些函数 (由于我们导入了 STDOLE.TLB,它知道这些函数)。

因此,我们只列出了我们添加到 IExample2 VTable 末尾的函数 (即 SetStringGetString)。这些函数**必须**按它们在 IExample2.h 的 VTable 定义中出现的顺序排列。

我们在每个函数之前都指定了一个帮助字符串。同样,这只是对象浏览器会向程序员显示的文本。

对于每个函数,我们首先指定我们选择赋予它的 DISPID。记住,我们任意决定为 SetString 选择 1,为 GetString 选择 2。

每个函数定义的其余部分看起来与 IExample2.h 中的相同,只有一个例外。在每个参数之前,我们必须指定该参数是否是脚本引擎在传递给我们之前初始化为特定值的 (即 [in] 参数),还是未初始化的参数,引擎期望我们用某个值填充它 (即 [out] 参数)。或者,它甚至可以是一个参数,脚本引擎初始化为某个值,并且还期望我们修改它以获得新值 (即 [in, out] 参数)。或者,也许它可以是一个传递给我们的参数,引擎期望我们的 Invoke 函数将其存储在引擎传递的那个特殊 VARIANT 中,用于返回该值 (即 [out, retval] 参数)。

SetString 期望接收一个引擎已设置为特定值的 BSTR。因此,我们将其标识为 [in]

由于 GetString 期望接收一个 BSTR 指针,我们将用一个我们分配的字符串副本填充它,我们应该将其指定为 [out] 参数。不幸的是,出于我在此不深入解释的原因,Microsoft 不允许简单地使用 [out]。我们必须使用 [in, out][out, retval]。两者之间的区别在于,我们是否希望将分配的 BSTR 作为调用 GetString 的返回值为脚本返回 (即 [out, retval]),还是脚本会传递一个变量名给 GetString 来填充 (即 [in, out])。换句话说,这取决于我们希望脚本如何调用我们的 GetString 函数。我们有两种选择,这里是用 VBScript 说明的选项

newString = GetString()  ' [out, retval]
GetString(newString)     ' [in, out]

前者在我看来更自然,所以我们将继续把传递给 GetString 的参数定义为 [out, retval]

注意: 传递给函数的参数中只能有一个标记为 [out, retval]。其余参数必须是 [in][in, out]

我们以一个闭括号结束我们 VTable 的定义。

现在,我们来定义我们的 IExample2 对象。它看起来是这样的

[uuid(520F4CFD-61C6-4eed-8004-C26D514D3D19), 
      helpstring("IExample2 object."), appobject]
coclass IExample2
{
   [default] interface IExample2Vtbl;
}

第一行指定了我们 IExample2 对象的 GUID。同样,请注意它与 IExample2.h 中定义为 CLSID_IExample2 的 GUID 相同,并且必须如此。

我们指定一个帮助字符串,让对象浏览器知道这个特定对象是什么/做什么。

appobject 关键字表示可以通过 CoGetClassObject 获取我们的 IExample2 对象。

下一行必须以 coclass 开头,后面跟着我们决定在此 IDL 源中称呼我们的对象。

在花括号内,我们放入一行,指示我们的对象以我们在 IDL 文件中标识为“IExample2Vtbl”的 VTable 开头。这一行包含 interface 关键字,后跟我们的 VTable 名称。我们用方括号括起来的 default 关键字作为前缀,以指示这是脚本引擎应该假定为其 IExample2 对象中获取的 VTable。(我们尚未讨论这一点,但对象可能包含指向多个 VTable 的指针。这被称为“多重接口”。但尝试让这样的对象与脚本引擎一起工作会非常麻烦,所以我们将完全避免它。即使 IExample2 中只有一个 VTable,我们仍然需要将其标记为默认 VTable。)

我们用两个闭括号结束,这样 IDL 文件就完成了。你可以使用 MIDL.EXE 编译它 (或者将 IExample2_extra.idl 添加到你的 Visual C++ 项目中,让 IDE 对其运行 MIDL.EXE)。如果没有问题,你应该会得到一个名为 IExample2.tlb 的二进制文件。这就是我们的类型库。

可以将该 .TLB 文件嵌入到我们的 DLL 的资源中,并从那里加载。但我喜欢将 .TLB 文件与 DLL 分开。为什么?因为 C/C++ 应用程序不需要 .TLB,编译过的 Visual BASIC 应用程序也不需要。只有解释型的 Visual BASIC、VBScript 和其他类似的解释型语言才需要 .TLB。(而且你确实需要为这些程序员分发类型库。)

总结,IDL 文件包含我们类型库的“源代码”。我们将我们的对象及其 VTable 的定义放入此文件中,这与我们在 C 头文件中所做的类似,但格式略有不同。它使用 MIDL.EXE 编译,生成 .TLB 文件,这就是我们实际的二进制类型库。此 .TLB 文件可以嵌入到我们的 DLL 资源中,也可以与我们的 DLL 一起分发,以允许脚本引擎使用我们的组件。

注册类型库

我们已经完成了更新的组件。当然,在任何人使用它之前,我们需要安装/注册它。在第一篇文章中,你可能还记得,我们编写了一个实用程序来注册我们的组件。我们需要对该代码进行非常小的改动,因为我们的类型库 (.TLB 文件) 也需要注册。因此,我们将在同一个安装实用程序中进行。Microsoft 提供了一个 API 来完成注册类型库的所有工作:RegisterTypeLib

但在看那个之前,还有另一件事要讨论。Visual BASIC 程序员不喜欢处理像“大数字”这样的复杂事情。GUID 对他们来说太吓人了。因此,Microsoft 决定 COM 开发人员可以为他们的组件关联一个“产品 ID”(ProdID)。而 Visual BASIC 程序员将使用我们的 ProdID 来创建我们的对象,而不是使用我们的 IExample2 GUID。那么 ProdID 是什么?它只是一个你选择的用于标识你的组件的唯一字符串。通常,你会取你的主对象名称 (这里是 IExample2),然后附加一个接口名称,甚至一个版本号,每个都用一个点分隔。我将任意选择 **IExample2.object** 作为我的 ProdID。为了将此 ProdID 与我们的 IExample2 关联起来,我们需要设置一些额外的注册表项。我们将在安装实用程序中也这样做。我们所要做的就是创建一个名为“IExample2.object” (即我们选择的 ProdID) 的注册表项。然后,在该项下创建一个名为“CLSID”的子项,并将其默认值设置为我们的 IExample2 GUID。唯一棘手的部分是 GUID 必须被格式化为人类可读的字符串。

RegIExample2 目录中,有一个更新的安装实用程序,它创建了额外的 ProdID 注册表项,并调用 RegisterTypeLib 来注册我们的类型库。这些添加非常简单,因此你可以查看注释掉的代码来研究细节。

运行此安装实用程序后,脚本引擎现在可以使用我们的组件了。

当然,我们的卸载实用程序需要更新以删除额外的注册表项。而且,Microsoft 提供了 **UnRegisterTypeLib** 来撤销 RegisterTypeLib 的操作。请查看 UnregIExample2 目录中更新的代码。

总结,类型库 (即 TLB 文件) 必须像我们的 IExample 对象本身一样被注册。这很容易通过 RegisterTypeLib 完成。此外,我们应该选择一个 ProdID,它只是一个不吓人的字符串,VB 程序员可以使用它来代替我们对象的 GUID,并设置一些注册表项将此 ProdID 与我们的 GUID 关联起来。

VBScript 示例

让我们来看一个使用 IExample2 的简单的 VBScript 示例。我们将简单地调用 SetStringIExample2 的字符串成员设置为 "Hello World"。然后,我们将调用 GetString 来检索该字符串 (放入另一个 VBScript 变量) 并显示它 (这样我们就可以确定它是否已正确设置/检索)。

set myObj = CreateObject("IExample2.object")
myObj.SetString("Hello world")
copy = myObj.GetString()
MsgBox copy, 0, "GetString return"

首先,VBScript 获取我们的一个 IExample2 对象。脚本调用 VB 引擎的 CreateObject。请注意,已传递了我们的 IExample2 ProdID。CreateObject 首先调用 CLSIDFromProgID 来从 ProdID 中查找 IExample2 的 GUID。然后,CreateObject 调用 CoCreateInstance 来获取指向我们 DLL 分配的 IExample2 的指针。脚本将其存储在变量 myObj 中。

接下来,脚本调用我们的 IExample2SetString,传递字符串 "Hello World"。脚本引擎通过调用 IExample2GetIDsOfNames 来获取 SetString 的 DISPID,然后调用 IExample2Invoke,传递一个包含 "Hello World" 的 BSTRVARIANT

接下来,脚本调用我们的 IExample2GetString。脚本引擎再次调用 GetIDsOfNamesInvoke。脚本将我们 (即 Invoke) 返回的字符串存储在变量 copy 中。

最后,脚本显示 GetString 返回的内容。

但先不要运行这个脚本。我们先要改变一下我们的组件。

属性

考虑以下 C 代码

typedef struct {
   char *  string;
} IExample;

IExample example;

example.string = "Hello World";

如果 VBScript 能够做类似的事情来设置我们 IExample2 的字符串成员,那不是很棒吗?例如,与其调用 SetString,不如让 VBScript 这样做

myObj.string = "Hello World"

但是,尽管看起来脚本直接访问了我们 IExample2 的字符串成员,但这将在内部转化为对我们 SetString 的调用,传递 "Hello World"。

同样,脚本也可以这样做来检索 IExample2 的字符串成员的值 (并将其赋给 copy 变量)

copy = myObj.string

同样,这将在内部转化为对我们 GetString 的调用,返回值赋给 copy

我们能做到吗?是的,我们可以。但我们需要更改 SetStringGetString 的定义,不是在我们的 C 代码或 .H 文件中,而是在我们的 .IDL 文件中。由于 SetString 设置成员的值,我们需要将其标记为“属性设置”函数。由于 GetString 检索成员的值,我们需要将其标记为“属性获取”函数。在 IDL 文件中,我们还将 GetStringSetString 重命名为 VBScript 要使用的成员名称。在这里,我们将选择“string”。因此,这是我们更改 IDL 文件中定义的方式

[id(1), propput] HRESULT string([in] BSTR);
[id(1), propget] HRESULT string([out, retval] BSTR *);

我们所做的只是在 SetString 中添加了“propput” (并将函数名更改为“string”)。我们也向 GetString 添加了“propget”,并将其函数名也改为了“string”。

“等等。怎么能有两个同名的函数?”,你可能会问。我们不能。实际的函数名仍然是 SetStringGetString。毕竟,它们出现在 IExample2.h 的 VTable 定义中,与它们出现在我们的 IDL VTable 定义中的位置相同。只是我们的类型库认为它们的名字都是“string”,并且它知道它们是两个不同的函数,因为一个设置值 (propput),另一个检索值 (propget)。

但是,为了将函数声明为 propget,它必须接受正好两个参数。首先,当然是指向我们对象的指针。第二个参数必须声明为 [in],并且应该是设置成员的值。例如,由于 IExample2 的字符串成员需要设置为 BSTR (指针),我们将参数定义为 BSTR。如果字符串成员定义为 long,我们将参数定义为“long”。只要我们坚持使用自动化数据类型,我们就没问题。

并且,为了将函数声明为 propput,它必须接受正好两个参数。首先,当然是指向我们对象的指针。第二个参数必须声明为 [out, retval],并且它应该是指向我们返回成员值的指针。例如,由于 IExample2 的字符串成员是 BSTR (指针),我们将参数定义为 BSTR * (实际上是一个句柄,因为 BSTR 本身就是一个指针)。如果字符串成员定义为 long,我们将参数定义为“long *”。

我已在 IExample2_extra.idl 中更改了以上两行,并将其重命名为 IExample2.idl。运行 MIDL.EXE 对其进行处理,以生成新的 .TLB 文件。

现在,我们可以重写 VBScript 示例如下

set myObj = CreateObject("IExample2.object")
myObj.string = "Hello World"
MsgBox myObj.string, 0, "GetString return"

总结,如果你想允许脚本设置你对象某个数据成员的值,请添加一个设置该值内容的函数,并在 IDL 文件中将其标记为 propput。在 IDL 文件中,函数名将是成员名。传递给函数的第二个参数应标记为 [in] 并且是所需的值。如果你想允许脚本检索你对象某个数据成员的值,请添加一个检索该值内容的函数,并在 IDL 文件中将其标记为 propget。在 IDL 文件中,函数名将是成员名。传递给函数的第二个参数应标记为 [out, retval] 并且是指向你返回值的指针。

注意: 对于给定的成员,你不必同时提供 propgetpropset。你可以只提供一个。例如,如果我们想使字符串成员成为只读值,那么我们将删除 SetString 函数,并从 IDL 文件中删除 propput 行 (以及从 IExample2.h 的 VTable 定义中删除 SetString)。

更新的 C 应用程序示例

因为我们更改了传递给 SetStringGetString 的数据类型,所以我们必须更新我们的 C/C++ 客户端以传递正确的数据类型。这非常简单,只需在我们需要获取 BSTR 的地方使用 SysAllocString,或在需要释放 BSTR 的地方使用 SysFreeString。更新的 C 示例位于 IExampleApp2 目录中。

EXE 中的 COM 组件

我们也可以不将 COM 组件打包成 DLL,而是将其打包成自己的 EXE。这样做的好处是 EXE 在自己的进程空间中运行,因此,应用程序的任何崩溃都不会导致 COM 组件崩溃 (反之亦然,不一定)。缺点是操作系统必须进行“数据封送”才能在进程边界之间传递数据,因此运行速度可能会变慢。

好消息是,我们的 DLL 中需要更改的内容很少。在 IExampleExe 目录中,你会找到一个用于将我们用于构建 IExample2 的代码构建成 EXE 的项目。我将文件名 IExample2.c 改为 IExampleExe.c,将 IExample2.h 改为 IExampleExe.h,将 IExample2.idl 改为 IExampleExe.idl。我还搜索并替换了所有 IExample2 的实例为 IExampleExe。所以现在,我们的对象被称为 IExampleExe。当然,我替换了 IExampleExe.hIExampleExe.idl 中的 GUID,以便这个 EXE 组件不会与我们的 DLL 组件冲突。由于我们正在创建一个 EXE,我们不再需要 DEF 文件,所以它已从项目中删除。

IExampleExe.c 中,我们必须用 WinMain 函数替换 DllMain 函数。我们的 WinMain 必须像 DllMain 一样进行 IClassFactory 和全局变量的初始化。但是,我们的 WinMain 还必须调用 OLE API CoInitialize 来初始化 COM,然后调用 CoRegisterClassObject 将我们的 EXE 添加到 COM 的运行任务列表中。那是什么?它只是 COM 维护的一个内部列表,列出了所有当前正在运行的 EXE,这些 EXE 是 COM 组件。如果我们的 EXE 不在此列表中,那么任何应用程序都无法使用我们的 COM 组件。而且,除非我们的 EXE 调用 CoRegisterClassObject,否则它不在该列表中。既然是我们的 WinMain 调用 CoRegisterClassObject,那么道理上说,直到我们的 EXE 启动之前,没有任何应用程序可以使用我们的 COM 组件。

在我们的 EXE 添加到运行任务列表后,它只是执行一个消息循环,直到接收到 WM_QUIT 消息。

当我们的 EXE 在内存中运行时,围绕消息循环旋转,应用程序可以调用 CoCreateInstance (或 CoGetClassObject/CreateInstance) 来获取我们的一个 IExampleExe 对象并调用其函数。要调用这些函数,应用程序只需做 IExample2App 演示程序所做的事情即可。操作系统负责处理所有必需的细节。

当我们的 EXE 准备终止时,它调用 CoRevokeClassObject 将自己从 COM 的运行任务列表中移除,然后调用 CoUninitialize 来释放 COM 资源。

这就是差异的全部内容 (尽管我在我们的 IExampleExe 对象和 IClassFactory 对象的 Release 函数中添加了一些代码,以检查在我们所有应用程序都 Release 了我们的对象后是否要卸载 DLL)。

注册 EXE 中的 COM 组件时,与注册 DLL 不同,我们创建一个名为“LocalServer32”的键,而不是“InprocServer32”。在 RegIExampleExe 目录中,有一个注册 IExampleExe 的示例。(未提供卸载实用程序。这是一项简单的练习,留给你自己完成)。

要测试这个新的 EXE 组件,你可以简单地修改 IExample2App.c 以引用 IExampleExe 的 GUID,通过更改为 #include IExampleExe.h

#include "../IExampleExe/IExampleExe.h"

请记住,你必须先安装/注册我们的 COM 组件,然后运行我们的组件 (IExample.exe) 将其加入 COM 的任务列表,**然后**再运行修改后的 IExample2App.exe 测试程序。

接下来呢?

在下一章中,我们将从脚本引擎的角度来看 IDispatch 函数。脚本引擎如何将自己语言中的代码翻译成对我们对象的 IDispatch 函数的调用?

© . All rights reserved.