用标准C编写COM(七)COM in plain C,Part7

来源:互联网 发布:淘客怎么申请淘宝特卖 编辑:程序博客网 时间:2024/06/03 18:33

原文:http://www.codeproject.com/Articles/15037/COM-in-plain-C-Part-7

拥有自定义COM对象的ActiveX脚本宿主允许在你的应用程序中调用C函数。 

下载例程- 380 Kb

内容

  •     简介
  •     声明自定义对象
  •     我们的IDL文件和类型库
  •     向引擎注册我们的COM对象
  •     脚本如何调用我们COM对象的函数
  •     我们的IProvideMultipleClassInfo对象
  •     应用程序和文档对象
  •     C++宿主例程

简介

 

在前一章中,我们学习了如何让我们应用程序运行一段脚本。但为了让脚本能调用我们应用程序中的C函数,最终与我们的应用程序交换数据,我们需要给我们的应用程序添加另一个自定义COM对象。我们的C函数(脚本调用的)会封装在这个COM对象中。你可以返回第二章,为了让脚本调用我们COM对象中的函数,我们需要给它添加一个IDispatch标准函数(也有IUnkown标准函数)。

 

一句话,我们要做的是创建一个就像我们在第二章中那样的我们自定义COM对象。我们定义它包含IUnkown函数,后面是IDipatch函数,接着是我们想让脚本调用的额外函数。为了创建一个类型库(让脚本可以看到我们额外函数名、传递的参数和返回值)我们还得写一个.IDL文件。我们做的是像我们在第二章中做的那样创建我们自定义、脚本可识别的对象。


声明我们的自定义COM对象

 

我们可以给我们自定义COM对象起我们喜欢的名字。我们随便叫它IApp。

 

我们给脚本提供三个可调用的函数,Output、SetTitle和GetTitle。Output函数在我们主窗口的EDIT空间中显示一行文本。我们使用另一个自定义窗口消息(WM_APP+1)通过PostMessage传递这行给我们主窗口进程来显示,就像我们在我们的IActiveScriptSite的OnHandleError函数那样。SetTitle函数会改变在我们主窗口的标题栏中的文本。而GetTitle函数是回去标题栏文本。我们把这三个函数放到我们的IApp对象中,在IUnknown和IDispatch函数之后。

 

在ScriptHost4目录中是我们添加了这个IApp对象的ScriptHost版本。AppObject.c源文件包含了大部分新代码。

 

我们需要给我们的IApp对象一个唯一的GUID,因此我运行GUIDGEN.EXE创建了一个,给它命名为CLSID_IApp。我们还需要给IApp的VTable一个GUID,所以我创建另一个GUID,命名为IDD_IApp。我把这两个GUID放在了Guids.c和Guids.h源文件中。

 

像第二章那样,我们用一个特殊的宏来申明我们的IApp对象的VTable(和对象本身)。这是定义:

[cpp] view plaincopyprint?
  1. #undef  INTERFACE  
  2. #define INTERFACE IApp  
  3. DECLARE_INTERFACE_ (INTERFACE, IDispatch)  
  4.  {  
  5.    // IUnkown函数  
  6.    STDMETHOD  (QueryInterface)(THIS_REFIID, void **) PURE;  
  7.    STDMETHOD_ (ULONG, AddRef)(THIS) PURE;  
  8.    STDMETHOD_ (ULONG, Release)(THIS) PURE;  
  9.    // IDispatch函数  
  10.    STDMETHOD_ (ULONG, GetTypeInfoCount)(THIS_ UINT *) PURE;  
  11.    STDMETHOD_ (ULONG, GetTypeInfo)(THIS_ UINTLCID, ITypeInfo **) PURE;  
  12.    STDMETHOD_ (ULONG, GetIDsOfNames)(THIS_ REFIID, LPOLESTR *, UINT,  
  13.                 LCID, DISPID *) PURE;  
  14.    STDMETHOD_ (ULONG, Invoke)(THIS_ DISPID, REFIID, LCIDWORD,  
  15.                 DISPPARAMS *, VARIANT *,EXCEPINFO *, UINT *) PURE;  
  16.    // 额外函数  
  17.    STDMETHOD  (Output)(THIS_ BSTR)PURE;  
  18.    STDMETHOD  (SetTitle)(THIS_ BSTR)PURE;  
  19.     STDMETHOD (GetTitle)(THIS_ BSTR *) PURE;  
  20. };  

记住上面的宏也会自动定义我们的IApp为包含一个指向上面VTable的指针的lpVtbl成员。但像通常做的那样,我们需要添加额外的、私有成员给我们的对象,因此我们也像这样定义一个MyRealIApp结构:

[cpp] view plaincopyprint?
  1. typedef struct {  
  2.   IApp                      iApp;  
  3.   IProvideMultipleClassInfo classInfo;  
  4.  }MyRealIApp;  

我们的MyRealIApp封装了我们的IApp和一个我们后面会用到的标准子对象IProvideMultipleClassInfo。

 

我们需要给我们的IApp声明全局VTable,同时还有我们的IProvideMultipleClassInfo子对象的VTable:

[cpp] view plaincopyprint?
  1. // 我们的IApp VTable.  
  2. IAppVtbl IAppTable = {  
  3.   QueryInterface,  
  4.   AddRef,  
  5.   Release,  
  6.   GetTypeInfoCount,  
  7.   GetTypeInfo,  
  8.   GetIDsOfNames,  
  9.   Invoke,  
  10.   Output,  
  11.   SetTitle,  
  12.   GetTitle};  
  13. // 我们的IProvideMultipleClassInfo VTable.  
  14. IProvideMultipleClassInfoVtbl IProvideMultipleClassInfoTable = {  
  15.   QueryInterface_CInfo,  
  16.   AddRef_CInfo,  
  17.   Release_CInfo,  
  18.   GetClassInfo_CInfo,  
  19.   GetGUID_CInfo,  
  20.   GetMultiTypeInfoCount_CInfo,  
  21.   GetInfoOfIndex_CInfo};  

对我们而言,我们只需要一个IApp对象。所有运行的脚本共享这一个对象(这样如果我们有多个脚本线程运行,他们存取全局数据,在我们的IApp函数做同步是非常重要的)。那么最容易的方法是把我们的IApp对象声明为全局的,同时在在我们程序开始的地方调用一个函数来初始化它。

 

我们还得加载两个ITyperInfo-一个是IApp的VTable,一个是我们IApp对象本身。同时我们需要存储这两个ITypeInfo。因为每个我们只需要一个,我给他们定义两个全局变量。

[cpp] view plaincopyprint?
  1. // 对我们而言,我们只需要一个IApp对象,因此我们把它申明为全局的  
  2.  MyRealIApp  MyIApp;  
  3. // 我们的IApp对象的ITypeInfo,我们只需要一个因此我们让它是全局的  
  4. ITypeInfo   *IAppObjectTypeInfo;  
  5. //我们的IApp对象的ITypeInfo VTabel,我们只需要一个  
  6. ITypeInfo   *IAppVTableTypeInfo;  
  7. void initMyRealIAppObject(void)  
  8.  {  
  9.    // 初始化子对象的VTable指针  
  10.    MyIApp.iApp.lpVtbl = &IAppTable;  
  11.    MyIApp.classInfo.lpVtbl = &IProvideMultipleClassInfoTable;  
  12.    // 还没有加载IApp的VTable的ITypeInfo,和IApp自身  
  13.    IAppObjectTypeInfo = IAppVTableTypeInfo = 0;  
  14.  }  

我们的IDL文件和类型库

 

我们需要创建一个.IDL文件,这样MIDL.EXE能为我们编译一个类型库。我们的IDL文件定义了我们的IApp VTable和IApp对象自身。这与我们在第二章定义我们的IExample2自定义COM对象时做的本质上是一样的。你可以找到一个ScriptHost.idl源文件来仔细阅读。

 

注意IApp的VTable被声明为双接口。这样我们的IDispatch函数就可以使用标准的COM调用来做这些函数的大部分工作了。这与我们的IExample2的IDisaptch函数做的一样。

 

还要注意的是我们的SetTitle和GetTitle函数被声明为propput和propget,以及同样的DISPID号。这样,这两个函数只要脚本是concerned状态,可以设置和获取Title这个变量的值。这与你在第二章看到的类似。

 

我们要做的与IExample2不同的事是MIDL.EXE为我们创建的实际的类型库(也就是ScriptHost.tlb)。对于IExample2,我们只把类型库作为单独的文件。对于我们的应用程序,我们要把类型库文件嵌入到我们的exe的资源中,用COM函数LoadTypeLib来从我们的资源中释放、加载它。

 

我已经给我们的应用程序的资源文件创建了一个.RC文件。在ScriptHost.rc中,你会看到下面这句:

[cpp] view plaincopyprint?
  1. 1TYPELIB MOVEABLE PURE "debug/ScriptHost.tlb"  

它获得类型库文件(ScriptHost.tlb, MIDL.EXE编译的放在Debug目录中),把它嵌入到我们的exe资源中,给它一个资源ID号1。这个资源的类型是TYPELIB,表明这个资源是一个类型库文件。我们不需要提供我们的类型库文件(ScriptHost.tlb)作为一个单独的文件因为现在它被嵌入到我们的exe中了。

 

这是一个通过GUID创建一个与这个GUID相连的ITypeInfo。这个函数从我们的嵌入的类型库资源中提取信息。例如,为了得到一个我们IApp Vtalbe的ITypeInfo,我们只要传入IApp的VTable的GUID。我们还的传入一个接受返回的ITypeInfo的句柄。

[cpp] view plaincopyprint?
  1. HRESULT getITypeInfoFromExe(const GUID *guid, ITypeInfo **iTypeInfo)  
  2.  {  
  3.    wchar_t    fileName[MAX_PATH];  
  4.    ITypeLib   *typeLib;  
  5.    HRESULT    hr;  
  6.    // 提示错误  
  7.    *iTypeInfo = 0;  
  8.    // Load the type library from our EXE's resources  
  9.    // 从我们的EXE的资源中加载类型库  
  10.    GetModuleFileNameW(0, &fileName[0], MAX_PATH);  
  11.    if (!(hr = LoadTypeLib(&fileName[0], &typeLib)))  
  12.    {    
  13.       // 让微软的GetTypeInfoOfGuid()创建一个通用的ItypeInfo给请求的元素(传入它的GUID)  
  14.       hr = typeLib->lpVtbl->GetTypeInfoOfGuid(typeLib, guid, iTypeInfo);  
  15.       // 我们不再需要这个类型库了  
  16.       typeLib->lpVtbl->Release(typeLib);  
  17.    }  
  18.    return(hr);  
  19.  }  


向引擎注册我们的COM对象

 

为了向引擎注册我们的COM对象,我们必须调用一次引擎IActiveScript的AddNamedItem函数。我们还必须为这个COM对象取一个字符串名,脚本会使用它来调用我们的函数。在我们使用的引擎语言中这个名字应该是一个合法的变量名。一个好的通用方法是全部使用字母作为这个字符串的名字(没有空格),它作为变量名被每种语言支持。

 

我们随便取一个字符串名为“application”。我们需要在我们真正运行脚本前注册我们的COM对象,但要在我们初始化引擎后。因此我们在runScript中调用SetScriptSite后添加对AddNamedItem的调用,但要在我们加载脚本和传递它给ParseScriptText前。这是我们添加的一行:

[cpp] view plaincopyprint?
  1. args->EngineActiveScript->lpVtbl->AddNamedItem(args->EngineActiveScript,  
  2.     "application", SCRIPTITEM_ISVISIBLE|SCRIPTITEM_NOCODE)  

第二个参数是我们的字符串名,在这是“application”。第三个参数是标示。SCRIPTITEM_ISVISIBLE标示意味着脚本可以调用我们对象的函数。(没有这个标示,脚本不会调用我们的任何函数。为什么你需要这样是有原因的,但在这先不说)。SCRIPTITEM_NOCODE 意味着在我们执行程序中对象的函数是C代码。事实上添加给脚本的对象的函数被包含在你添加到的引擎的另一个脚本中是有可能的。在一般情况下,你不要用SCRIPTITEM_NOCODE ,但在这我们要它,因为在我们的执行程序中我们对象的函数的确是C函数。

 

只要我们调用了AddNamedItem,脚本引擎会调用我们IActiveScriptSite的GetItemInfo函数来获取一个指向我们自定义COM对象的指针,或者是它的ITypeInfo。因此我们现在需要用有效的指令来替换以前的存根代码。在这是这样:

[cpp] view plaincopyprint?
  1. STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR  
  2.    objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo)  
  3.  {  
  4.    HRESULT   hr;  
  5.    // 提示失败  
  6.    hr = E_FAIL;  
  7.    if (dwReturnMask & SCRIPTINFO_IUNKNOWN) *objPtr = 0;  
  8.    if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0;  
  9.    // 引擎要的是我们的IApp对象嘛(有字符串名“application”)?  
  10.    if (!lstrcmpiW(objectName, "application"))  
  11.    {  
  12.       // 引擎要的是一个指向我们IApp对象返回的指针嘛?  
  13.       if (dwReturnMask & SCRIPTINFO_IUNKNOWN)  
  14.       {  
  15.          // 给引擎一个指向我们IApp对象的指针。引擎会调用它的AddRef()函数,  
  16.          // 同时在使用完后Release()它。  
  17.          *objPtr = getAppObject();  
  18.       }  
  19.       // 引擎要我们的IApp对象返回的ITypeInfo嘛?  
  20.       if (dwReturnMask & SCRIPTINFO_ITYPEINFO)  
  21.       {  
  22.          // 确保我们拥有一个IApp对象的ITypeInfo。(脚本引擎需要知道传给我们IApp  
  23.          // 的额外函数的参数的意义,和这些函数的返回值。因此,它需要IApp的ITypeInfo)。  
  24.          // 引擎会调用它的AddRef()函数,在随后使用完后Release()它。  
  25.          if ((hr = getAppObjectITypeInfo(typeInfo))) goto bad;  
  26.       }  
  27.       hr = S_OK;  
  28.    }  
  29. bad:  
  30.    return(hr);  
  31.  }  

注意引擎会传入我们给出对象的字符串名(也就是“application”),因此我们得做字符串比较来确保引擎请求的正是我们的IApp对象。引擎还会传入用于我们返回给它的一个指向这个对象的句柄,或者它的ITypeInfo。如果引擎要一个指向我们对象的指针,它传入一个SCRIPTINFO_IUNKNOWN标示。如果它需要的是我们对象的ITypeInfo,那么它传入SCRIPTINFO_ITYPEINFO标示。注意两个标示可以同时传入。我们只调用我们的getAppObject或getAppObjectITypeInfo函数(在AppObject.c中)来填充引擎的句柄。


脚本如何调用我们COM对象的函数

 

当我们调用AddNamedItem时,我们COM对象被添加给引擎、暴露给脚本就像脚本自己创建的对象一样。例如,如果VBScript要调用我们的COM对象的Output函数,它可以这么做:

[vb] view plaincopyprint?
  1. application.Output("Some text")  

在VBScript中,一个对象名后紧跟一个点。注意脚本使用我们“application”字符串名来定位我们的IApp对象。VBScript不需要调用CreateObject。因为我们的应用程序通过AddNamedItem注册了我们的对象,我们的对象会对只通过使用我们选定的字符串名的脚本自动生效。

 

如果脚本要设置我们的Title属性(也就是,调用我们的SetTitle函数),它可以像下面这样做:

[vb] view plaincopyprint?
  1. application.Title = "Some text"  

如果脚本要获得我们Title属性值(也就是调用我们的GetTitle函数),它可以像下面这样做:

[cpp] view plaincopyprint?
  1. title = application.Title  

有一个调用我们IApp函数script.vbs(在ScriptHost4目录中)VBScript例程。


我们的IProvideMultipleClassInfo对象

 

引擎在需要获得与我们IApp对象有关的GUID或者ITypeInfo对象时它调用我们的IProvideMultipleClassInfo对象函数(在AppObject.c中)。引擎可以请求我们IApp[default]的VTable的GUID,或者可以请求我们IApp对象拥有的任何[default,source] VTable(如果我们有这样的VTable的话)的GUID,或者可以请求我们IApp的ITypeInfo对象(也就是说,不是一个它的VTable的ITypeInfo)。


应用程序和文档对象

 

假设我们有一个文本编辑器应用程序。这个应用程序是一个“多文档”应用程序,意味着用户可以编辑几个不同的文本文件,每个在它自己的MDI窗口中打开。

 

在这种情况下,我们的IApp对象在至少得是一个“application对象”;即它包含控制我们应用程序的所有操作的函数。例如,它可能有一个显示或隐藏工具栏或状态栏的函数。我们只需要一个IApp对象。

 

我们定义第二个自定义对象,我们随便叫它IDocument对象。这个对象有控制一个文本编辑器窗口以及它的内容的函数。例如,或许我们有一个在当前光标位置插入文本的函数,和移动光标到文档中指定位置的另一个函数。每次我们创建、加载不同的文本文件时,我们创建不同的IDocument对象给这个新的文档。因此,用户打开了几个编辑器窗口,我们就的有几个IDocument对象(每个窗口一个)。

 

因为我们要同一个对象的几个实例,我们不能调用引擎的AddNamedItem来注册我们的IDocumnet对象。(的确,如果用户还没有打开任何文档,我们不可以有IDocument对象的任何实例)。那么脚本如何获得存取一个特定的IDocument对象呢(假设脚本希望改变文档的内容)?通常,我们的IApp对象会有脚本可以调用返回一个IDocument对象的函数。或许脚本会出入它想要的文档的“名字”。或者我们的IApp有一个直接返回当前活动文档的IDocument的函数。(这要看你怎么做了)。

 

通常,你要有一个“documnent”结构的链表,IApp可以通过它查找直到发现、获得相应的IDocument对象。例如,或许我们给我们的IApp定义一个GetDocument的函数。GetDocument接受传入的想要获得的IDocument的BSTR名字。我们的IDL文件可以定义成这样:

[cpp] view plaincopyprint?
  1. [id(3)] HRESULT GetDocument([in] BSTR name, [out, retval] IDispatch**document);  

当然,为了让脚本调用我们的IDocumnent的函数,我们的IDocument必须在它的VTable中有标准的IDispatch函数。在本例中,它可以(就脚本引擎而言应该这样)伪装一个IDispatch。因此这就是我们要GetDocument返回的。

 

那么例如,一个VBScript可以这样获得一个text.txt的文档:

[cpp] view plaincopyprint?
  1. 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强行加载和运行test.vbs。

 

对于这个例程,你会注意到我们的IApp对象如何控制我们的阅读器所有操作,而IDocument控制每个单独的文档。注意我们向ScriptHost.idl中添加两个自定义对象(同时注意两个都是双VTable,因此我们用标准COM函数来做IDispatch的大部分工作)。还的注意在我们的IDL中我们需要另一个GUID给IDocument的VTable。但我们没在IDL文件中定义IDocument对象本身。我们不需要因为脚本从不通过CoCreateInstance创建他们,但会间接获取我们应用程序自己创建的一个IDocument。


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++编译器会自动为其他子对象生成这些函数的正确的委托(delegation)。

原创粉丝点击