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






4.96/5 (95投票s)
如何用 C 语言编写一个可供 VBScript、Visual BASIC、jscript 等脚本语言使用的 COM 组件。
目录
- 引言
- 为什么脚本语言不能直接使用我们的 DLL?
- 自动化数据类型 (例如 BSTR, VARIANT)
- IDispatch 函数
- 类型库
- 注册类型库
- VBScript 示例
- 属性
- 更新的 C 应用程序示例
- EXE 中的 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,还有 SysAllocString
和 SysAllocStringLen
等函数,它们会分配一个缓冲区来复制 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
结构中,那么我们将 VARIANT
的 vt
成员设置为 VT_BSTR
,并将我们的 BSTR
(即指向该特殊格式化的 UNICODE 字符串的指针) 存储在 VARIANT
的 bstrVal
成员中。如果我们包装一个 long
,那么我们将 VARIANT
的 vt
成员设置为 VT_I4
,并将我们的 long
存储在 VARIANT
的 lVal
成员中 (由于联合体的存在,它实际上与 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;
这是我们更新的 SetString
和 GetString
函数
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 中,这四个指针必须分别命名为 GetTypeInfoCount
、GetTypeInfo
、GetIDsOfNames
和 Invoke
。它们必须按此顺序出现,位于三个 IUnknown
函数 (即 VTable 中的前三个指针) 之后,但在指向我们自己额外函数的任何指针 (即 SetString
和 GetString
) 之前。因此,让我们编辑 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 来为我的 IExample2
和 IExample2VTbl
创建新的 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
给了脚本引擎,该引擎就可以调用我们 ITypeInfo
的 GetFuncDesc
函数来确定我们 GetString
函数有多少个参数,每个参数的类型是什么,以及 GetString
在有返回值时返回什么。
“哦,太好了。又要写更多函数了”,你可能在想。是的,我们可以编写 ITypeInfo
对象所需的所有函数,并将它们放在 IExample2.c 中。然后,IExample2
的 GetTypeInfo
函数可以分配我们的一个 ITypeInfo
对象,初始化它 (即将其 VTable 存储在其中),并将其返回给脚本引擎。
但是,我们不会这样做。我们将省去一些工作。Microsoft 已经编写了一些通用的 ITypeInfo
函数 (包含在 OLE DLL 中),并提供了一个我们可以调用的函数,该函数会为我们分配一个 ITypeInfo
,并用指向这些通用函数的 VTable 填充它。然后,我们可以将这个“通用” ITypeInfo
提供给脚本引擎。我们只需要确保我们的 IExample2
对象被定义为所谓的“双重接口”。现在不要担心它是什么。但我们将遵守这个要求。
在我们深入研究 GetTypeInfo
函数之前,我想说一点关于类型库的事情。它可以包含供人类显示的字符串。例如,如果我们愿意,我们可以为我们的 GetString
函数添加一个“文本描述”,内容为:此函数返回 IExample2 对象的字符串。然后,例如,Visual BASIC IDE 可以获取我们的 ITypeInfo
对象,调用其 GetDocumentation
函数来检索 GetString
的此描述,并将其显示给想要调用我们 IExample
的 GetString
的 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
后调用 ITypeInfo
的 Release
)。所以,这是我们的 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
的每个函数 (即 GetString
和 SetString
) 分配一个数字值,Microsoft 称之为 DISPID (Dispatch ID)。这可以是任何我们选择的值,但每个函数都必须有一个唯一的 DISPID。(注意:不要使用 0 或负数值)。我们将任意将 GetString
分配 DISPID 1,将 SetString
分配 DISPID 2。脚本引擎在内部使用此 DISPID 来更快地调用我们的函数,因为通过数字值匹配在某个表中查找函数比比较函数名称字符串更快。
接下来,我们编写 GetIDsOfNames
函数。此函数的作用是允许脚本引擎将我们添加到 IExample2
的某个函数 (即 GetString
或 SetString
) 的名称传递给我们,我们将返回我们为此函数分配的 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
的另一个函数 (即 GetString
或 SetString
) 的函数。脚本引擎传递要调用的函数的 DISPID (即 GetString
为 1,SetString
为 2)。引擎还传递一个 VARIANT
数组,其中填充了要传递给 GetString
或 SetString
的参数。它还传递一个 VARIANT
,我们需要用函数中的任何返回值来填充 (不是 Invoke
的 HRESULT
返回值,而是我们的类型库表明 GetString
或 SetString
的返回值)。
所以,我们的 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
函数 (QueryInterface
、AddRef
和Release
) 之后。为了方便实现它们,我们可以将 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.h 中 CLSID_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 中),该文件定义了 IUnknown
和 IDispatch
等基本 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 直接调用我们的 SetString
和 GetString
函数 (就像你在第一篇文章的演示 C 应用程序中看到的那样),该 VTable 还在其中包含了四个 IDispatch
函数,脚本引擎 (如 VBScript) 可以使用这些函数来 (间接) 调用 SetString
和 GetString
,通过 Invoke
。这就是为什么它被称为“dual
”。这个 VTable 对于想要直接调用我们 SetString
和 GetString
函数的 C/C++ 应用程序以及需要通过我们的 Invoke
函数调用它们的脚本引擎来说都是有效的。
oleautomation
关键字表示我们仅将自动化数据类型用于传递给 SetString
和 GetString
函数的参数,以及任何返回值。
注意: 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
(QueryInterface
、AddRef
和 Release
) 或 IDispatch
(GetTypeInfoCount
、GetTypeInfo
、GetIDsOfNames
和 Invoke
) 函数。我们不必这样做,因为我们指定 IDispatch
作为我们的 VTable 类型,所以 MIDL.EXE 会自动添加这些函数 (由于我们导入了 STDOLE.TLB,它知道这些函数)。
因此,我们只列出了我们添加到 IExample2
VTable 末尾的函数 (即 SetString
和 GetString
)。这些函数**必须**按它们在 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 示例。我们将简单地调用 SetString
将 IExample2
的字符串成员设置为 "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
中。
接下来,脚本调用我们的 IExample2
的 SetString
,传递字符串 "Hello World"。脚本引擎通过调用 IExample2
的 GetIDsOfNames
来获取 SetString
的 DISPID,然后调用 IExample2
的 Invoke
,传递一个包含 "Hello World" 的 BSTR
的 VARIANT
。
接下来,脚本调用我们的 IExample2
的 GetString
。脚本引擎再次调用 GetIDsOfNames
和 Invoke
。脚本将我们 (即 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
。
我们能做到吗?是的,我们可以。但我们需要更改 SetString
和 GetString
的定义,不是在我们的 C 代码或 .H 文件中,而是在我们的 .IDL 文件中。由于 SetString
设置成员的值,我们需要将其标记为“属性设置”函数。由于 GetString
检索成员的值,我们需要将其标记为“属性获取”函数。在 IDL 文件中,我们还将 GetString
和 SetString
重命名为 VBScript 要使用的成员名称。在这里,我们将选择“string”。因此,这是我们更改 IDL 文件中定义的方式
[id(1), propput] HRESULT string([in] BSTR); [id(1), propget] HRESULT string([out, retval] BSTR *);
我们所做的只是在 SetString
中添加了“propput
” (并将函数名更改为“string”)。我们也向 GetString
添加了“propget
”,并将其函数名也改为了“string”。
“等等。怎么能有两个同名的函数?”,你可能会问。我们不能。实际的函数名仍然是 SetString
和 GetString
。毕竟,它们出现在 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]
并且是指向你返回值的指针。
注意: 对于给定的成员,你不必同时提供 propget
和 propset
。你可以只提供一个。例如,如果我们想使字符串成员成为只读值,那么我们将删除 SetString
函数,并从 IDL 文件中删除 propput
行 (以及从 IExample2.h 的 VTable 定义中删除 SetString
)。
更新的 C 应用程序示例
因为我们更改了传递给 SetString
和 GetString
的数据类型,所以我们必须更新我们的 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.h 和 IExampleExe.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
函数的调用?