纯 C 实现 COM,第七部分





5.00/5 (14投票s)
一个具有自定义 COM 对象的 ActiveX 脚本宿主。这允许脚本调用您应用程序中的 C 函数。
目录
- 声明我们的自定义 COM 对象
- 我们的 IDL 文件和类型库
- 向引擎注册我们的 COM 对象
- 脚本如何调用我们的 COM 对象函数
- 我们的 IProvideMultipleClassInfo 对象
- 应用程序和文档对象
- 一个 C++ 示例宿主
引言
在前一章中,我们学习了如何从应用程序运行脚本。但是,为了让该脚本能够调用我们应用程序中的 C 函数,并最终与我们的应用程序交换数据,我们需要向应用程序添加另一个自定义 COM 对象。我们将把可由脚本调用的 C 函数包装在这个 COM 对象中。您可能还记得第二章,为了让脚本能够调用我们 COM 对象中的函数,我们需要向其中添加 IDispatch
标准函数(以及 IUnknown
标准函数)。
简而言之,我们将要做的是创建我们自己的自定义 COM 对象,就像我们在第二章中所做的那样。我们将定义它包含 IUnknown
函数,然后是 IDispatch
函数,然后是我们希望脚本能够调用的任何额外函数。此外,我们还将编写一个 .IDL 文件来创建类型库(以便脚本可以发现我们额外函数的名称、传递给它们的参数以及它们的返回值)。我们所做的只是创建我们自己的自定义、可由脚本访问的对象,就像我们在第二章中所做的那样。但我们不会将此对象放在其自己的 DLL 中,而是将其作为我们可执行文件的一部分。
声明我们的自定义 COM 对象
我们可以选择为我们的自定义 COM 对象命名,无论我们喜欢什么。我们暂时将其命名为 IApp
。
让我们提供三个脚本可以调用的函数,分别命名为 Output
、SetTitle
和 GetTitle
。Output
函数将在我们主窗口的 EDIT 控件中显示一行文本。我们将使用另一个自定义窗口消息(WM_APP
+ 1)通过 PostMessage
将该行传递给我们的主窗口过程进行显示——就像我们在 IActiveScriptSite
的 OnHandleError
函数中所做的那样。SetTitle
函数将更改我们主窗口标题栏中的文本。而 GetTitle
函数将检索标题栏文本。我们将把这三个函数放在 IApp
对象中,位于 IUnknown
和 IDispatch
函数之后。
在 ScriptHost4 目录中,有一个我们 Script Host 的版本,它添加了这个 IApp
对象。名为 AppObject.c 的源文件包含大部分新代码。
我们需要为我们的 IApp
对象生成一个唯一的 GUID,所以我运行了 GUIDGEN.EXE 来创建一个,并将其命名为 CLSID_IApp
。我们还需要为 IApp
的 VTable 生成一个 GUID,所以我创建了另一个 GUID 并将其命名为 IDD_IApp
。我已将这两个 GUID 放在名为 Guids.c 和 Guids.h 的源文件中。
您可能还记得第二章,我们使用了一个特殊的宏来声明我们的 IApp
对象的 VTable(以及对象本身)。这是声明:
#undef INTERFACE #define INTERFACE IApp 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 (Output)(THIS_ BSTR) PURE; STDMETHOD (SetTitle)(THIS_ BSTR) PURE; STDMETHOD (GetTitle)(THIS_ BSTR *) PURE; };
请记住,上述宏还会自动定义我们的 IApp
,使其包含一个成员,lpVtbl
,它是指向上述 VTable 的指针。但正如许多情况一样,我们需要向我们的对象添加额外的私有成员,所以我们还将定义一个 MyRealIApp
结构,如下所示:
typedef struct { IApp iApp; IProvideMultipleClassInfo classInfo; } MyRealIApp;
我们的 MyRealIApp
包装了我们的 IApp
,以及一个标准的 COM 子对象 IProvideMultipleClassInfo
,我们稍后将对其进行检查。
我们需要声明 IApp
的全局 VTable,以及我们 IProvideMultipleClassInfo
子对象的 VTable:
// Our IApp VTable. IAppVtbl IAppTable = { QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke, Output, SetTitle, GetTitle}; // Our IProvideMultipleClassInfo VTable. IProvideMultipleClassInfoVtbl IProvideMultipleClassInfoTable = { QueryInterface_CInfo, AddRef_CInfo, Release_CInfo, GetClassInfo_CInfo, GetGUID_CInfo, GetMultiTypeInfoCount_CInfo, GetInfoOfIndex_CInfo};
出于我们的目的,我们只需要一个 IApp
对象。所有运行的脚本都将共享这个对象(因此,如果它们访问任何全局数据且我们有多个脚本线程正在运行,那么在我们的 IApp
函数中采用某种同步机制是很重要的)。最简单的方法是声明我们的 IApp
对象为全局变量,并在程序开始时调用一个函数来初始化它。
我们还将加载两个 ITypeInfo
——一个用于 IApp
的 VTable,另一个用于我们 IApp
对象本身。而且,我们需要将这些 ITypeInfo
存储在某个地方。因为我们每个只需要一个,所以我将为它们声明两个全局变量。
// For our purposes, we need only one IApp object, so // we'll declare it global MyRealIApp MyIApp; // The ITypeInfo for our IApp object. We need only 1 so we make it global ITypeInfo *IAppObjectTypeInfo; // The ITypeInfo for our IApp VTable. We need only one of these ITypeInfo *IAppVTableTypeInfo; void initMyRealIAppObject(void) { // Initialize the sub-object VTable pointers MyIApp.iApp.lpVtbl = &IAppTable; MyIApp.classInfo.lpVtbl = &IProvideMultipleClassInfoTable; // Haven't yet loaded the ITypeInfos for IApp's VTable, nor IApp itself IAppObjectTypeInfo = IAppVTableTypeInfo = 0; }
我们的 IDL 文件和类型库
我们需要创建一个 .IDL 文件,以便 MIDL.EXE 可以为我们编译类型库。我们的 IDL 文件定义了我们的 IApp
VTable 和 IApp
对象本身。这基本上与我们在第二章定义我们的 IExample2
自定义 COM 对象时所做的相同。您可以在名为 ScriptHost.idl 的源文件中找到该文件。
请注意,IApp
的 VTable 被声明为双接口。这将允许我们的 IDispatch
函数使用一些标准的 COM 调用来完成这些函数的大部分工作。同样,这与我们对 IExample2
的 IDispatch
函数所做的一样。
另外,请注意,我们的 SetTitle
和 GetTitle
函数被声明为 propput
和 propget
,并引用相同的 DISPID 号。因此,从脚本的角度来看,这两个函数用于设置和获取名为 Title
的变量的值。同样,您应该从第二章熟悉这一点。
我们将与 IExample2
不同的一点是关于 MIDL.EXE 为我们创建的实际类型库(即 ScriptHost.tlb)。对于 IExample2
,我们只是将类型库保留为一个单独的文件。对于我们的应用程序,我们将把该类型库文件嵌入到我们 EXE 的资源中,并使用 COM 函数 LoadTypeLib
从我们的资源中提取/加载它。
我为我们应用程序的资源创建了一个 .RC 文件。在 ScriptHost.rc 中,您会看到以下语句:
1 TYPELIB MOVEABLE PURE "debug/ScriptHost.tlb"
这会获取类型库文件(ScriptHost.tlb,由 MIDL.EXE 编译并放置在 Debug 目录中),并将其嵌入到我们 EXE 的资源中,为其分配资源 ID 号 1。资源类型是 TYPELIB
,表示该资源是类型库文件。我们不再需要单独分发我们的类型库文件(ScriptHost.tlb),因为它现在已嵌入到我们 EXE 中。
这是一个函数,它接受一个 GUID 并为该 GUID 引用的任何内容创建一个 ITypeInfo
。该函数从我们嵌入的类型库资源中提取信息。例如,要获取我们 IApp
VTable 的 ITypeInfo
,我们只需传递 IApp
VTable 的 GUID。我们还传递一个句柄,用于返回 ITypeInfo
。
HRESULT getITypeInfoFromExe(const GUID *guid, ITypeInfo **iTypeInfo) { wchar_t fileName[MAX_PATH]; ITypeLib *typeLib; HRESULT hr; // Assume an error *iTypeInfo = 0; // Load the type library from our EXE's resources GetModuleFileNameW(0, &fileName[0], MAX_PATH); if (!(hr = LoadTypeLib(&fileName[0], &typeLib))) { // Let Microsoft's GetTypeInfoOfGuid() create a generic ITypeInfo // for the requested item (whose GUID is passed) hr = typeLib->lpVtbl->GetTypeInfoOfGuid(typeLib, guid, iTypeInfo); // We no longer need the type library typeLib->lpVtbl->Release(typeLib); } return(hr); }
向引擎注册我们的 COM 对象
要将我们的 COM 对象注册到引擎,我们必须调用引擎 IActiveScript
的 AddNamedItem
函数一次。我们还必须为该 COM 对象指定一个字符串名称,脚本将使用该名称来调用我们的 COM 函数。此名称应是我们在使用的任何语言引擎中合法的变量名。一种良好的通用方法是使用所有字母字符作为字符串名称(不带空格),这几乎被所有语言支持为变量名。
我们暂时决定使用字符串名称“application”。我们必须在实际运行脚本之前、在初始化引擎之后调用 AddNamedItem
。因此,我们在 runScript
中调用 SetScriptSite
之后、加载脚本并将其传递给 ParseScriptText
之前添加该调用。这是我们要添加的行:
args->EngineActiveScript->lpVtbl->AddNamedItem(args->EngineActiveScript,
"application", SCRIPTITEM_ISVISIBLE|SCRIPTITEM_NOCODE)
第二个参数是我们的字符串名称,在这里是“application”。第三个参数是一些标志。SCRIPTITEM_ISVISIBLE
标志表示脚本可以调用我们对象的函数。(没有此标志,脚本将无法调用我们的任何函数。这可能有一个原因,但在这里不是。)SCRIPTITEM_NOCODE
表示对象函数是我们可执行文件中的 C 代码。有可能向脚本添加一个对象,其中该对象的函数包含在您添加到引擎的另一个脚本中。在这种情况下,您将不使用 SCRIPTITEM_NOCODE
,但在这里我们想要它,因为我们的对象函数确实是我们可执行文件中的 C 函数。
一旦我们调用 AddNamedItem
,脚本引擎就会调用我们的 IActiveScriptSite
的 GetItemInfo
函数来检索指向我们自定义 COM 对象和/或其 ITypeInfo
的指针。因此,现在我们需要用有用的指令替换以前的存根代码。如下所示:
STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo) { HRESULT hr; // Assume failure hr = E_FAIL; if (dwReturnMask & SCRIPTINFO_IUNKNOWN) *objPtr = 0; if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0; // Does the engine want our IApp object (has the string name "application")? if (!lstrcmpiW(objectName, "application")) { // Does the engine want a pointer to our IApp object returned? if (dwReturnMask & SCRIPTINFO_IUNKNOWN) { // Give the engine a pointer to our IApp object. Engine will call // its AddRef() function, and later Release() it when done *objPtr = getAppObject(); } // Does the engine want the ITypeInfo for our IApp object returned? if (dwReturnMask & SCRIPTINFO_ITYPEINFO) { // Make sure we have an ITypeInfo for our IApp object. (The script // engine needs to figure out what args are passed to our IApp's // extra functions, and what those functions return. And for that, // it needs IApp's ITypeInfo). The engine will call its AddRef() // function, and later Release() it when done if ((hr = getAppObjectITypeInfo(typeInfo))) goto bad; } hr = S_OK; } bad: return(hr); }
请注意,引擎将传递我们为对象指定的字符串名称(即“application”),因此我们进行字符串比较以确保引擎确实在请求我们的 IApp
对象。引擎还会传递一些句柄,我们可以在其中返回指向对象和/或其 ITypeInfo
的指针。如果引擎想要指向我们对象的指针,它会传递 SCRIPTINFO_IUNKNOWN
标志。如果它想要我们对象的 ITypeInfo
,它会传递 SCRIPTINFO_ITYPEINFO
标志。请注意,这两个标志可以同时传递。我们只需调用我们的 getAppObject
和/或 getAppObjectITypeInfo
函数(在 AppObject.c 中)来填充引擎的句柄。
脚本如何调用我们的 COM 对象函数
当我们调用 AddNamedItem
时,我们的 COM 对象会被添加到引擎,并暴露给脚本,就好像它是脚本本身创建的对象一样。例如,如果 VBScript 想要调用我们 COM 对象的 Output
函数,它将这样做:
application.Output("Some text")
在 VBScript 中,一个点跟着一个对象名。请注意,脚本使用我们的字符串名称“application”来引用我们的 IApp
对象。VBScript 不需要调用 CreateObject
。因为我们的应用程序已通过 AddNamedItem
注册了我们的对象,所以我们的对象通过使用我们选择的字符串名称即可自动供脚本使用。
如果脚本想要设置我们的 Title 属性(即调用我们的 SetTitle
函数),它可以这样做:
application.Title = "Some text"
如果脚本想要获取我们 Title
属性的值(即调用我们的 GetTitle
函数),它可以这样做:
title = application.Title
在 ScriptHost4 目录中有一个 VBScript 示例,名为 script.vbs,它调用我们的 IApp
函数。
我们的 IProvideMultipleClassInfo 对象
我们的 IProvideMultipleClassInfo
对象函数(在 AppObject.c 中)在引擎需要获取与我们的 IApp
对象相关的 GUID 和/或 ITypeInfo
对象时会被调用。引擎可能会要求提供我们 IApp
[默认] VTable 的 GUID,或者它可能会要求提供我们 IApp
对象拥有的任何 [默认,源] VTable 的 GUID(如果我们有这样的 VTable),或者可能会要求提供我们 IApp
的 ITypeInfo
对象(即,不是其 VTable 的 ITypeInfo
)。
应用程序和文档对象
假设我们有一个文本编辑器应用程序。该应用程序是一个“多文档”应用程序,这意味着用户可以编辑多个不同的文本文件,每个文件都在自己的 MDI 窗口中打开。
在这种情况下,我们的 IApp
对象将或多或少地成为一个“应用程序对象”;也就是说,它将包含控制我们应用程序整体操作的函数。例如,它可能有一个显示或隐藏工具栏或状态栏的函数。我们只需要一个 IApp
对象。
我们将定义第二个自定义对象,我们暂时将其命名为 IDocument
对象。该对象将包含控制一个文本编辑器窗口及其内容的函数。例如,我们可能有一个函数在当前光标位置插入一些文本,另一个函数将光标移动到文档的某个位置。每次我们创建/加载另一个文本文件时,我们都会为这个新文档创建一个新的 IDocument
对象。因此,如果用户打开了多个编辑器窗口,我们将有多个 IDocument
对象(每个窗口一个)。
由于我们将有同一对象的多个实例,因此我们不会调用引擎的 AddNamedItem
来注册我们的 IDocument
对象。(实际上,如果用户尚未打开任何文档,我们可能没有任何 IDocument
对象实例。)那么,脚本如何访问特定的 IDocument
对象(假设脚本希望更改该文档的内容)?通常,我们的 IApp
对象将有一个脚本可以调用的函数,该函数将返回一个 IDocument
对象。也许,脚本会传递它想要的文档的“名称”。或者,我们的 IApp
可能有一个函数,它只是返回当前活动文档的 IDocument
。(这取决于您如何决定实现。)
通常,您会有一个“文档”结构的链表,IApp
可以通过该链表搜索以查找/检索适当的 IDocument
对象。例如,也许我们会为我们的 IApp
定义一个 GetDocument
函数。GetDocument
将接收要获取的 IDocument
的 BSTR
名称作为参数。我们的 IDL 文件定义可能如下所示:
[id(3)] HRESULT GetDocument([in] BSTR name, [out, retval] IDispatch **document);
当然,为了让脚本能够调用我们的 IDocument
函数,我们的 IDocument
必须在其 VTable 中包含标准的 IDispatch
函数。在这种情况下,它可以(也应该,就脚本引擎而言)伪装成一个 IDispatch
。所以我们表明 GetDocument
返回的是什么。
因此,例如,VBScript 可以像这样获取名为 test.txt 的文档:
SET doc = application.GetDocument("test.txt")
在 ScriptHost5 目录中有一个基本的文本文件查看器。它是一个简单的 MDI 应用程序,可以打开多个文本文件进行查看;每个文件都在自己的窗口中。我们有一个 IApp
对象,带有一个额外的函数,名为 CreateDocument
。此函数接收要加载的文本文件的 BSTR
文件名作为参数。它创建一个新的 MDI 子窗口,并在窗口中加载/显示该文本文件。(实际上,我们只是创建一个空白文档。)CreateDocument
还为该窗口创建一个新的 IDocument
对象,并将其返回给脚本。
我们的 IDocument
对象包含一个名为 WriteText
的函数。此函数接收一个 BSTR
文本作为参数,该文本将替换窗口中的任何其他文本。
在 ScriptHost5 目录中有一个名为 test.vbs 的 VBScript 文件。它只是调用我们的 IApp
的 CreateDocument
两次,以创建两个 IDocument
对象,并将它们命名为“Document 1”和“Document 2”。它调用每个 IDocument
的 WriteText
函数。对于“Document 1”,它设置文本“This is document 1.”。对于“Document 2”,它设置文本“This is document 2.”。
请注意,为了简单起见,ScriptHost5 被硬编码为加载和运行 text.vbs。
从这个例子中,您应该会注意到我们的 IApp
对象是如何控制我们查看器的整体操作的,而 IDocument
则控制每个单独的文档。请注意我们为定义我们的两个自定义对象而对 ScriptHost.idl 所做的添加(并请注意,两者都有双 VTable,因此我们使用标准的 COM 函数来完成大部分 IDispatch
工作)。同时请注意,我们需要在 IDL 文件中为 IDocument
的 VTable 提供另一个 GUID。但我们不直接在 IDL 文件中定义 IDocument
对象。我们不需要,因为脚本永远不会通过 CoCreateInstance
创建这些对象,而是间接获取我们应用程序本身创建的一个对象。
一个 C++ 示例宿主
在 ScriptHost6 目录中,有一个 ScriptHost5 的 C++ 版本。您会在 AppObject.h 中注意到,我们将 MyRealIApp
和 MyRealIDocument
声明为基于我们的 IApp
和 IDocument
的类。然后,COM 函数成为各自类的成员。请注意,在 C 示例中,我们的 MyRealApp
的函数将 MyRealApp
的指针作为第一个参数,而在 C++ 版本中则省略了这一点。那是因为它成为隐藏的“this
”指针。而且,我们不需要声明和初始化 VTable 的指针。C++ 编译器会为我们完成所有这些工作。
此外,当我们调用 COM 函数时,我们省略 ->lpVtbl
,并且不将对象指针作为第一个参数传递。
另外,在 IActiveScriptSite.h 中,我们将 MyRealIActiveScriptSite
声明为一个基于 IActiveScriptSite
和 IActiveScriptSiteWindow
的类。请注意,我们不再需要为每个子对象单独的 QueryInterface
、AddRef
和 Release
。我们只声明基对象的 QueryInterface
、AddRef
和 Release
,然后 C++ 编译器会自动生成这些函数,并进行正确的委托,用于其他子对象。