`
chinamming
  • 浏览: 141311 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

标准C编写COM(八)COM in plain C,Part8

 
阅读更多

原文:http://www.codeproject.com/Articles/17038/COM-in-plain-C-part-8

下载例程-419Kb

内容

  • 简介
  • 脚本代码持久化
  • 脚本代码和“命名项”
  • 调用脚本中的特定函数
  • 查询/设置脚本中变量的值
  • 查询/设置脚本中变量的值


简介

在前面的章节中,我们学会了如何创建Activex脚本宿主。虽然这些章节覆盖了编写一个脚本宿主的的大部分方面,但是,这里还有其它一些 你的脚本宿主也许想要使用的 更esoteric(深奥,机密)的特性。本章节将会详细的介绍其中一些机密特性。


脚本代码持久化


在先前的脚本宿主示例中,我们用runScript函数打开脚本引擎,运行脚本,然后关闭引擎。这个函数中,脚本引擎被加载/分析,执行,然后释放(执行完成之后)。


但是,也许有时候,你希望添加一些脚本到引擎,并且让它们一直驻留在里面,即使这些脚本没有被执行。可能,你希望这些脚本能够被任何 能够被相同引擎的IActiveScript识别的 其它脚本所调用。事实上,或许你还想把这些脚本作为"ram-based macros"集合。ActiveX脚本引擎让这成为可能。但我们还需要从以下两方面改变我们的方法:

  1. 当我们把脚本作为“宏”添加的时候,我们需要为ParseScriptText函数指定SCRIPTTEXT_ISPERSISTENT标志。这告诉引擎保留内部已分析\加载过的脚本,即使在ParseScriptText返回之后。

  2. 我们不能在所有宏被使用之前释放引擎的IActiveScript对象。如果这样做,那些宏最终会被卸载掉。

最好的做法是 在引擎处于INITIALIZED状态时 添加这些宏,但要在引擎被设置为STARTED或CONNECTED状态之前。ParseScriptText不会尝试运行这些脚本,而是对其语法分析,并在ParseScriptText返回的时候,在内部把这些脚本保存起来。这些脚本会驻留在引擎中,直到我们释放了引擎的IActiveScript对象,即使在这中间,我们调用了其它脚本或者方法。


ScriptHost7目录中,你会找到一个说明这一点的例子。我们添加一个VB脚本给引擎,并指定 SCRIPTTEXT_ISPERSISTENT 标志。为了简单,这个VB脚本被作为全局变量,像下面这样嵌入在我们的EXE中:

  1. wchar_tVBmacro[]=L"SubHelloWorld\r\nMsgBox\"Helloworld\"\r\nEndSub";

上面的代码是一个VB的“Hello World”子程序。它只是的弹出一个消息框。


接下来,我们嵌入第二段VB脚本。这个VB脚本只是调用第一个加载的HelloWorld脚本程序。

  1. wchar_tVBscript[]=L"HelloWorld";

当载入第二段脚本时,我们不指定SCRIPTTEXT_ISPERSISTENT标志。

为了确保持久化的脚本能够工作,需要让runScript线程始终保持VB引擎的IActiveScript对象,直到程序终止。为了完成这一点,我们在程序开始的时候就启动线程(而不是在运行脚本时才启动线程)。这个线程一直保持有效,直到主程序结束。开始,引擎调用CoCreateInstance来获取VB引擎的 IActiveScript,然后调用 IActiveScript的QueryInterface 得到引擎的 IActiveScriptParse对象,再调用InitNew初始化引擎,最后调用SetScriptSite把我们的 IActiceScriptSite传给引擎。初始化部分跟我们前几章的示例一样。


runScript将会调用ParseScriptText来加载我们的“VB 宏”到引擎中。这几乎和示例程序完全一样,除开指定了 SCRIPTTEXT_ISPERSISTENT 标志:

  1. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&VBmacro[0],
  2. 0,0,0,0,0,SCRIPTTEXT_ISPERSISTENT,0,0);

添加这个脚本之后,runScript 线程就等待,直到主线程 设置我们创建的一个事件信号量 唤醒,然后运行第二段脚本(它会调用刚才我们装载的第一段脚本)。

主窗口有一个“Run Script”按钮。当用户点击时,主线程就设置事件信号量。

  1. //Letthescriptthreadknowthatwewantittorunascript
  2. SetEvent(NotifySignal[0]);

运行线程被唤醒后就调用ParseScriptText函数加载第二个脚本,然后通过调用SetScriptState函数,设置VB脚本引擎的状态为SCRIPTSTATE_CONNECTED。 这导致第二段脚本运行, 第二段脚本调用 宏 中的 “Hello World”子程序 弹出一个消息框。当用户关闭消息框之后,第一段脚本运行结束,SetScriptState函数返回。


此时,我们还没有释放(Rlease) IActiveScriptParse和IActiveScript对象,也没有关闭引擎。我们调用SetScriptState把状态设置为SCRIPTSTATE_INITIALIZED。这样第二段脚本就被卸载,但宏脚本不会被卸载,因为它是持久化的。


运行线程又重新休眠了。等待用户再次点击“Run Script”按钮。在这个事件中,运行线程重复 加载/运行 第二段脚本的过程。但注意我们不需要重新加载宏脚本,它一直被保留在引擎中。

这就是运行线程中的“脚本循环”:

  1. for(;;)
  2. {
  3. //Waitformainthreadtosignalustorunascript.
  4. WaitForSingleObject(NotifySignal,INFINITE);
  5. //Havethescriptengineparseoursecondscriptandadditto
  6. //theinternallistofscriptstorun.NOTE:WedoNOTspecify
  7. //SCRIPTTEXT_ISPERSISTENTsothisscriptwillbeunloaded
  8. //whentheenginegoesbacktoINITIALIZEDstate.
  9. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,
  10. &VBscript[0],0,0,0,0,0,0,0,0);
  11. //Runallofthescriptsthatweaddedtotheengine.
  12. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
  13. SCRIPTSTATE_CONNECTED);
  14. //TheabovescripthasendedafterSetScriptStatereturns.Now
  15. //let'ssettheenginestatebacktoinitializedtounloadthis
  16. //script.VBmacro[]remainsstillloaded.
  17. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
  18. SCRIPTSTATE_INITIALIZED);
  19. }

脚本代码和“命名项”


在上面的例子中,我们添加宏脚本给同样的“命名项”作为第二段脚本。(注意:我们没指定特定的命名项,所以引擎用它的缺省的全局项)。但在不同的“命名项”下加载脚本是可以的。


在前面的章节中,你应该记得我们可以 通过创建一个命名项(通过引擎IActiveScript对象的的AddNamedItem函数) 让脚本能够调用我们自己的的C函数。

但这不是命名项的唯一用处。我们还可以把创建的命名项分组,这就是我们现在的示例:

假设我们有2个c源文件分别为File1.cFile2.c。下面是 File1.c的内容:

  1. //File1.c
  2. staticvoidMyFunction(void)
  3. {
  4. printf("File1.c");
  5. }
  6. staticvoidFile1(void)
  7. {
  8. MyFunction();
  9. }

这是 File2.c 的内容:
  1. //File2.c
  2. staticvoidMyFunction(constchar*ptr)
  3. {
  4. printf(ptr);
  5. }
  6. staticvoidFile2(void)
  7. {
  8. MyFunction("File1.c");
  9. }

还有一些东西需需要注意:

  1. 由于static关键字修饰,File1.c中的MyFunction和File2.c中的MyFunction就不一样了。我们可以把两个源文件在一起编译和连接不会有问题(也就是说,不会有命名冲突)。

  2. 由于static关键字修饰,File1.c中的函数不能调用File2.c中的函数,反之亦然。

当我们创建一个命名项时(在我们要加载的脚本代码中),把它看成创建c源文件。为了创建一个命名项,我们调用引擎IActiveScipt的AddNamedItem。假设我们有一个C语言实现的脚本引擎。首先,我们需要调用AddNamedItem两次,第一次,我们创建以File1.c作为名字的命名项;第二次我们创建以File2.c作为名字的命名项。这就在引擎中创建了2个“源文件”,然后我们将调用ParseScriptText把File1.c的内容加载给 File1.c命名项。为此,我们必须把命名项的名字作为第三个参数传给ParseScriptText。然后,我们把File2.c的内容加载给File2.c命名项。以下是我们的具体做法:

  1. //Here'sthecontentsofFile1.c
  2. wchar_tFile1[]=L"staticvoidMyFunction(void)
  3. {
  4. printf(\"File1.c\");
  5. }
  6. staticvoidFile1(void)
  7. {
  8. MyFunction();
  9. }";
  10. //Here'sthecontentsofFile2.c
  11. wchar_tFile1[]=L"staticvoidMyFunction(constchar*ptr)
  12. {
  13. printf(ptr);
  14. }
  15. staticvoidFile2(void)
  16. {
  17. MyFunction(\"File1.c\");
  18. }";
  19. //CreatetheFile1.cnameditem.Error-checkingomitted!
  20. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,"File1.c",0);
  21. //CreatetheFile2.cnameditem.
  22. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,"File2.c",0);
  23. //AddtheFile1.ccontentstotheFile1.cnamedobject
  24. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&File1[0],
  25. "File1.c",0,0,0,0,0,0,0);
  26. //AddtheFile2.ccontentstotheFile2.cnamedobject
  27. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&File2[0],
  28. "File2.c",0,0,0,0,0,0,0);
  29. 现在我们把VB“宏脚本”放到创建的命名项中。随便给这个项取名为:MyMacro。我们的具体做法如下:
  30. //Thenameofthenameditem
  31. wchar_tMyMacroObjectName[]=L"MyMacro";
  32. //CreatetheMyMacronameditem
  33. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,&MyMacroObjectName[0],SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISPERSISTENT);
  34. //AddthecontentsofVBmacrototheMyMacronameditem
  35. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&VBmacro[0],
  36. &MyMacroObjectName[0],0,0,0,0,SCRIPTITEM_ISVISIBLE|SCRIPTTEXT_ISPERSISTENT,
  37. 0,0);

你会注意到,我们传给AddNamedItem的几个标志参数。我们指定了SCRIPTITEM_ISPERSISTENT,因为我们不想让引擎在 被我们重置为INITIALIZED状态时 删除这个命名项(和它的内容)。我们还设置了SCRIPTITEM_ISVISIBLE标志,因为我们想要这个命名项能够被缺省的全局项(即第二段脚本获取添加项的地方)访问。设置SCRIPTITEM_ISVISIBLE标志等价于删除 C语言引擎例子中函数 的static关键字。这会允许一个命名项的函数被其它命名项的函数调用。如果没有SCRIPTITEM_ISVISIBLE, 一个命名项的函数可以自己调用,但不能被其它任何命名项的函数调用。

我们必须修改第二个VB脚本。现在当它调用HelloWorld子程序时,需要引用命名项。在VBscript中,这是通过 把它的名称作为对象使用 来完成的:

  1. wchar_tVBscript[]=L"MyMacro.HelloWorld";

还有一件事情。当我们调用AddNamedItem创建“MyMacro”时,引擎会调用IActiveScriptSite的GetItemInfo,并传递其名字“MyMacro”作为参数。我们需要为这个命名项 获取并返回一个IDispatch指针。这个IDispatch从哪儿来?我们通过 调用引擎IActiveScript对象的GetScriptDispatch函数,传入命名项的名字 来得到它。这就是我们的IActiveScriptSite的GetItemInfo函数:

  1. STDMETHODIMPGetItemInfo(MyRealIActiveScriptSite*this,LPCOLESTR
  2. objectName,DWORDdwReturnMask,IUnknown**objPtr,ITypeInfo**typeInfo)
  3. {
  4. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)*objPtr=0;
  5. if(dwReturnMask&SCRIPTINFO_ITYPEINFO)*typeInfo=0;
  6. //Weassumethatthenameditemtheengineisaskingforisour
  7. //"MyMacro"nameditemwecreated.Weneedtoreturnthe
  8. //IDispatchforthisnameditem.Wheredowegetit?Fromtheengine.
  9. //Specifically,wecalltheengineIActiveScript'sGetScriptDispatch(),
  10. //passingobjectName(whichshouldbe"MyMacro").
  11. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)
  12. return(EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  13. objectName,objPtr));
  14. return(E_FAIL);
  15. }

做了上面的修改,现在宏脚本就有了一个命名项。这有什么好处呢?首先,我们的第二个脚本中就能够也有一个“Hello World”子程序,不会和MyMacro的“Hello World”子程序冲突。所以,我们现在可以排除宏脚本和第二段脚本代码之间 子程序/函数 的命名冲突。此外,如果有更多的宏脚本,我们都可以放到它们各自的命名项中。这样,宏脚本就能有同名的 子程序/函数,在它们之间却不会发生名字冲突。脚本引擎知道那个 子程序/函数 被调用,因为命名项的名称指出了 子程序/函数 所属的命名项。

以下为译者注
*********************************************************
例程和函数的区别
-----------------------------
例程:
	Private Sub abc(a As Integer, b As Integer, c As Integer)
		'your code....
		'c = a + b
	End Sub
函数:
	Private Function c (a As Integer, b As Integer) As Integer
		'your code....
		'c = a + b
	End Function
-----------------------------
*********************************************************

综上所述,使用命名项能够避免 添加子程序/函数,全局变量添加到引擎中时 发生命名冲突。

调用脚本中的特定函数


在上面的例子中,通过调用引擎的GetScriptDispatch,获取特定命名项的IDispatch,我们只是简单的把IDispatch返回给引擎。


但是,除此之外,我们自己还能通过这个IDispatch直接调用 (在特定命名项中的)VB 子例程/函数。为了调用 子例程/函数,我们需要调用IDispatch的GetIDsOfNamesInvoke函数。这和我们在IExampleApp3示例中,当我们使用IDispatch的Invoke来调用com对象中的函数时,做的很类似。你也许应该重新仔细阅读那个例子,以便记起来起来在里面我们是如何做的。 现在,我们要直接调用MyMacros命名项中的HelloWorld子例程。首先我们要通过引擎IActiveScript对象的GetScriptDispatch函数获取命名项的IDispatch。然后调用IDispatch的GetIDsOfNames得到 引擎用于标识我们想要调用的函数的唯一序列号 DISPID。

  1. //NOTE:Error-checkingomitted!
  2. IDispatch*objPtr;
  3. DISPIDdispid;
  4. OLECHAR*funcName;
  5. DISPPARAMSdspp;
  6. VARIANTret;
  7. //GettheIDispatchfor"MyMacro"nameditem
  8. EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  9. "MyMacro",&objPtr);
  10. //NowgettheDISPIDforthe"HelloWorld"sub
  11. funcName=(OLECHAR*)L"HelloWorld";
  12. objPtr->lpVtbl->GetIDsOfNames(objPtr,&IID_NULL,&funcName,1,
  13. LOCALE_USER_DEFAULT,&dispid);
  14. //CallHelloWorld.
  15. //SinceHelloWorldhasnoargspassedtoit,wedon'thavetodo
  16. //anygrotesqueinitializationofDISPPARAMS.
  17. ZeroMemory(&dspp,sizeof(DISPPARAMS));
  18. VariantInit(&ret);
  19. objPtr->lpVtbl->Invoke(objPtr,dispid,&IID_NULL,LOCALE_USER_DEFAULT,
  20. DISPATCH_METHOD,&dspp,&ret,0,0);
  21. VariantClear(&ret);
  22. //ReleasetheIDispatchnowthatwemadethecall
  23. objPtr->lpVtbl->Release(objPtr);

ScriptHost8中,是添加下面的VB脚本(包含main子程序)到vb引擎中的例子:

  1. wchar_tVBscript[]=L"Submain\r\nMsgBox\"Helloworld\"\r\nEndSub";

然后我们直接调用这个 main 程序。你要注意的一件事是我们在调用ParseScriptText时没创建/指定任何特定的命名项。因此这段脚本代码被作为缺省“全局命名项”被加入。所以,我们需要从全局命名项中获取它的IDispatch。我们如何来做呢?我们传一个0给GetScriptDispatch作为名字。这是一个指定的值,告诉GetScriptDispatch返回全局命名项的IDispatch。

查询/设置脚本中变量的值


为了查询或设置某个(在指定命名项中的)变量,我们要做的事情和上面几乎完全一样。唯一不同在于Invoke的调用,当需要获取值时,指定DISPATCH_PROPERTYGET标志;当需要设置值时,我们设置DISPATCH_PROPERTYPUT标志。这就是一个 设置“MyMacro”命名项中变量“MyVariable”的值的 例子:

  1. //NOTE:Error-checkingomitted!
  2. IDispatch*objPtr;
  3. DISPIDdispid,dispPropPut;
  4. OLECHAR*varName;
  5. DISPPARAMSdspp;
  6. VARIANTarg;
  7. //GettheIDispatchfor"MyMacro"nameditem
  8. EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  9. "MyMacro",&objPtr);
  10. //NowgettheDISPIDforthe"MyVariable"variable(ie,property)
  11. varName=(OLECHAR*)L"MyVariable";
  12. objPtr->lpVtbl->GetIDsOfNames(objPtr,&IID_NULL,&varName,1,
  13. LOCALE_USER_DEFAULT,&dispid);
  14. //Setthevalueto10.
  15. VariantInit(&arg);
  16. ZeroMemory(&dspp,sizeof(DISPPARAMS));
  17. dspp.cArgs=dspp.cNamedArgs=1;
  18. dispPropPut=DISPID_PROPERTYPUT;
  19. dspp.rgdispidNamedArgs=&dispPropPut;
  20. dspp.rgvarg=&arg;
  21. arg.vt=VT_I4;
  22. arg.lVal=10;
  23. objPtr->lpVtbl->Invoke(objPtr,dispid,&IID_NULL,LOCALE_USER_DEFAULT,
  24. DISPATCH_PROPERTYPUT,&dspp,0,0,0);
  25. VariantClear(&arg);
  26. //ReleasetheIDispatchnowthatwemadethecall
  27. objPtr->lpVtbl->Release(objPtr);

ScriptHost9就是一个这样的示例,我们先设置MyVariable的值,然后调用mian子程序显示这个变量。

不同语言的交互


一种语言编写的脚本也可以调用另一种语言编写的脚本。例如,假设我们有下面的VB函数,显示一个消息框:

  1. SubSayHello
  2. MsgBox"HelloWorld"
  3. EndSub

那么就假设我们用下面的jscript函数来调用上面的的Vbscript函数:

  1. functionmain()
  2. {
  3. SayHello();
  4. }

首先,由于我们将会使用2种不同语言的脚本,jscript和vbscript,所以我们需要调用2次CoCreateInstance;一次得到jscript引擎的IActiveScript,另一次得到vbscript引擎的IActiveScript。当然,我们要把这2个指针分别保存到2个变量中(分别为:JActiveScriptVBActiveScript )。


我们还需要得到每个引擎的IActiveScriptParse。同时我们还需要调用每个引擎的SetScriptSite,把IActivesScriptSite传给引擎(我们可以为每个引擎分别传递不同的IActivesScriptSite,但是在这里,我们传给每个引擎同样的IActivesScriptSite,因为我们不会同时运行2种语言的脚本,只有在jscript引擎调用vbscript函数时才会用到vb引擎)。


换言之,runScript必须完成 要使用脚本引擎必需初始化的 工作,但是每个引擎只需要初始化一次。


然后,我们需要调用jscript引擎的ParseScriptText添加上面的jscript代码到jscript引擎中,同时需要调用vbscript引擎的ParseScriptText添加上面的vbscript代码到vbscript引擎中。我们会把这些代码分别加到2个引擎的全局命名项中。


为了方便jscript调用vbscript,我们需要在jscript引擎中创建一个命名项用来和vbscript交互。这需要在添加脚本到引擎中之前来做,我们随便给这个命名项取名为“VB”。

  1. JActiveScript->lpVtbl->AddNamedItem(JActiveScript,L"VB",
  2. SCRIPTITEM_GLOBALMEMBERS|SCRIPTITEM_ISVISIBLE);

让我们来看看当jscript引擎运行上面的jscript代码时发生了什么。引擎检查jscript中的我们载入的所有jscript函数,没有发现"SayHello"的jscript函数。因为我们添加了一些命名项到jscript引擎中(设置GetIDsOfNames标志),引擎就搞对自己说:"呃...也许SayHello函数在某个命名项中。我需要得到这个命名项对应的IDispatch,通过调用它的GetIDsOfNames函数查询SayHello的DISPID,如果IDispatch成功的返回了DISPID,我就调用IDispatch的Invoke来调用SayHello函数"。


但是引擎如何得到命名项的IDispatch呢?目前为止,你应该知道通过调用我们IActiveScriptSite的GetItemInfo。这种情况下,jscript引擎会为命名项名字的参数传一个"VB"。这就是看起来有点莫名其妙的地方。当我们的GetItemInfo找到被指定的项时,我们调用的vbscript引擎GetScriptDispatch来得到vbscript引擎的全局命名项,这正是我们要返回给jscript引擎的东西。


是的,你没有看错。当jscript引擎请求“VB”命名项的IDispatch时,我们实际上返回了VBscript引擎的全局命名项的IDispatch。为什么呢?因为我们vbscript的SayHello 函数被添加到vb引擎的全局命名项中,而不是在"VB"命名项中。换句话说,我们在jscript中把"VB"命名项作为一个代理(placeholder)使用。jscript引擎不需要知道"VB"命名项其实返回了vbscript引擎的全局命名项的IDispatch。


那么,jscript引擎会调用vbscript引擎全局命名项的IDsipatch的GetIDsOfNames,当然,VB引擎会返回SayHello函数的DISPID。当jscript调用IDispatch的Inovke时,最终进入vb引擎中让vb引擎运行VB的SayHello函数。


这里就是我们的IActiveScriptSite的GetItemInfo

  1. STDMETHODIMPGetItemInfo(MyRealIActiveScriptSite*this,LPCOLESTR
  2. objectName,DWORDdwReturnMask,IUnknown**objPtr,ITypeInfo**typeInfo)
  3. {
  4. HRESULThr;
  5. hr=E_FAIL;
  6. if(dwReturnMask&SCRIPTINFO_ITYPEINFO)*typeInfo=0;
  7. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)
  8. {
  9. *objPtr=0;
  10. //Iftheengineisaskingforour"VB"nameditemwecreated,
  11. //thenweknowthisistheJScriptenginecalling.Weneedto
  12. //returntheIDispatchforVBScript's"globalnameditem".
  13. if(!lstrcmpW(objectName,L"VB"))
  14. {
  15. hr=VBActiveScript->lpVtbl->GetScriptDispatch(VBActiveScript,
  16. 0,objPtr);
  17. }
  18. }
  19. return(hr);
  20. }

ScriptHost10中,是jscript调用vbscript的例子。

顺便提一句,你也许对SCRIPTITEM_GLOBALMEMBERS标志有点奇怪。先回想一下我们前面处理一个命名项时,脚本必须像一个对象名那样引用项的名字,例如:

VB.SayHello()

当用SCRIPTITEM_GLOBALMEMBERS标志创建项时,指出了对象名是可选的。例如,上面的代码可以工作,或者还可以这样用:

SayHello()

所以我们做的只是让jscript 在调用vb脚本的SayHello函数时 就像在调用另一个 本地(local) jscript函数。换言之,它或多或少是一种 隐藏命名项麻烦细节的 理论意义上的 捷径。

但这个好处是要付出代价的。就像全局项那样,这些使用SCRIPTITEM_GLOBALMEMBERS标记的项之间可能会出现名字冲突。

原文:http://www.codeproject.com/Articles/17038/COM-in-plain-C-part-8

下载例程-419Kb

内容

  • 简介
  • 脚本代码持久化
  • 脚本代码和“命名项”
  • 调用脚本中的特定函数
  • 查询/设置脚本中变量的值
  • 查询/设置脚本中变量的值


简介

在前面的章节中,我们学会了如何创建Activex脚本宿主。虽然这些章节覆盖了编写一个脚本宿主的的大部分方面,但是,这里还有其它一些 你的脚本宿主也许想要使用的 更esoteric(深奥,机密)的特性。本章节将会详细的介绍其中一些机密特性。


脚本代码持久化


在先前的脚本宿主示例中,我们用runScript函数打开脚本引擎,运行脚本,然后关闭引擎。这个函数中,脚本引擎被加载/分析,执行,然后释放(执行完成之后)。


但是,也许有时候,你希望添加一些脚本到引擎,并且让它们一直驻留在里面,即使这些脚本没有被执行。可能,你希望这些脚本能够被任何 能够被相同引擎的IActiveScript识别的 其它脚本所调用。事实上,或许你还想把这些脚本作为"ram-based macros"集合。ActiveX脚本引擎让这成为可能。但我们还需要从以下两方面改变我们的方法:

  1. 当我们把脚本作为“宏”添加的时候,我们需要为ParseScriptText函数指定SCRIPTTEXT_ISPERSISTENT标志。这告诉引擎保留内部已分析\加载过的脚本,即使在ParseScriptText返回之后。

  2. 我们不能在所有宏被使用之前释放引擎的IActiveScript对象。如果这样做,那些宏最终会被卸载掉。

最好的做法是 在引擎处于INITIALIZED状态时 添加这些宏,但要在引擎被设置为STARTED或CONNECTED状态之前。ParseScriptText不会尝试运行这些脚本,而是对其语法分析,并在ParseScriptText返回的时候,在内部把这些脚本保存起来。这些脚本会驻留在引擎中,直到我们释放了引擎的IActiveScript对象,即使在这中间,我们调用了其它脚本或者方法。


ScriptHost7目录中,你会找到一个说明这一点的例子。我们添加一个VB脚本给引擎,并指定 SCRIPTTEXT_ISPERSISTENT 标志。为了简单,这个VB脚本被作为全局变量,像下面这样嵌入在我们的EXE中:

  1. wchar_tVBmacro[]=L"SubHelloWorld\r\nMsgBox\"Helloworld\"\r\nEndSub";

上面的代码是一个VB的“Hello World”子程序。它只是的弹出一个消息框。


接下来,我们嵌入第二段VB脚本。这个VB脚本只是调用第一个加载的HelloWorld脚本程序。

  1. wchar_tVBscript[]=L"HelloWorld";

当载入第二段脚本时,我们不指定SCRIPTTEXT_ISPERSISTENT标志。

为了确保持久化的脚本能够工作,需要让runScript线程始终保持VB引擎的IActiveScript对象,直到程序终止。为了完成这一点,我们在程序开始的时候就启动线程(而不是在运行脚本时才启动线程)。这个线程一直保持有效,直到主程序结束。开始,引擎调用CoCreateInstance来获取VB引擎的 IActiveScript,然后调用 IActiveScript的QueryInterface 得到引擎的 IActiveScriptParse对象,再调用InitNew初始化引擎,最后调用SetScriptSite把我们的 IActiceScriptSite传给引擎。初始化部分跟我们前几章的示例一样。


runScript将会调用ParseScriptText来加载我们的“VB 宏”到引擎中。这几乎和示例程序完全一样,除开指定了 SCRIPTTEXT_ISPERSISTENT 标志:

  1. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&VBmacro[0],
  2. 0,0,0,0,0,SCRIPTTEXT_ISPERSISTENT,0,0);

添加这个脚本之后,runScript 线程就等待,直到主线程 设置我们创建的一个事件信号量 唤醒,然后运行第二段脚本(它会调用刚才我们装载的第一段脚本)。

主窗口有一个“Run Script”按钮。当用户点击时,主线程就设置事件信号量。

  1. //Letthescriptthreadknowthatwewantittorunascript
  2. SetEvent(NotifySignal[0]);

运行线程被唤醒后就调用ParseScriptText函数加载第二个脚本,然后通过调用SetScriptState函数,设置VB脚本引擎的状态为SCRIPTSTATE_CONNECTED。 这导致第二段脚本运行, 第二段脚本调用 宏 中的 “Hello World”子程序 弹出一个消息框。当用户关闭消息框之后,第一段脚本运行结束,SetScriptState函数返回。


此时,我们还没有释放(Rlease) IActiveScriptParse和IActiveScript对象,也没有关闭引擎。我们调用SetScriptState把状态设置为SCRIPTSTATE_INITIALIZED。这样第二段脚本就被卸载,但宏脚本不会被卸载,因为它是持久化的。


运行线程又重新休眠了。等待用户再次点击“Run Script”按钮。在这个事件中,运行线程重复 加载/运行 第二段脚本的过程。但注意我们不需要重新加载宏脚本,它一直被保留在引擎中。

这就是运行线程中的“脚本循环”:

  1. for(;;)
  2. {
  3. //Waitformainthreadtosignalustorunascript.
  4. WaitForSingleObject(NotifySignal,INFINITE);
  5. //Havethescriptengineparseoursecondscriptandadditto
  6. //theinternallistofscriptstorun.NOTE:WedoNOTspecify
  7. //SCRIPTTEXT_ISPERSISTENTsothisscriptwillbeunloaded
  8. //whentheenginegoesbacktoINITIALIZEDstate.
  9. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,
  10. &VBscript[0],0,0,0,0,0,0,0,0);
  11. //Runallofthescriptsthatweaddedtotheengine.
  12. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
  13. SCRIPTSTATE_CONNECTED);
  14. //TheabovescripthasendedafterSetScriptStatereturns.Now
  15. //let'ssettheenginestatebacktoinitializedtounloadthis
  16. //script.VBmacro[]remainsstillloaded.
  17. EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
  18. SCRIPTSTATE_INITIALIZED);
  19. }

脚本代码和“命名项”


在上面的例子中,我们添加宏脚本给同样的“命名项”作为第二段脚本。(注意:我们没指定特定的命名项,所以引擎用它的缺省的全局项)。但在不同的“命名项”下加载脚本是可以的。


在前面的章节中,你应该记得我们可以 通过创建一个命名项(通过引擎IActiveScript对象的的AddNamedItem函数) 让脚本能够调用我们自己的的C函数。

但这不是命名项的唯一用处。我们还可以把创建的命名项分组,这就是我们现在的示例:

假设我们有2个c源文件分别为File1.cFile2.c。下面是 File1.c的内容:

  1. //File1.c
  2. staticvoidMyFunction(void)
  3. {
  4. printf("File1.c");
  5. }
  6. staticvoidFile1(void)
  7. {
  8. MyFunction();
  9. }

这是 File2.c 的内容:
  1. //File2.c
  2. staticvoidMyFunction(constchar*ptr)
  3. {
  4. printf(ptr);
  5. }
  6. staticvoidFile2(void)
  7. {
  8. MyFunction("File1.c");
  9. }

还有一些东西需需要注意:

  1. 由于static关键字修饰,File1.c中的MyFunction和File2.c中的MyFunction就不一样了。我们可以把两个源文件在一起编译和连接不会有问题(也就是说,不会有命名冲突)。

  2. 由于static关键字修饰,File1.c中的函数不能调用File2.c中的函数,反之亦然。

当我们创建一个命名项时(在我们要加载的脚本代码中),把它看成创建c源文件。为了创建一个命名项,我们调用引擎IActiveScipt的AddNamedItem。假设我们有一个C语言实现的脚本引擎。首先,我们需要调用AddNamedItem两次,第一次,我们创建以File1.c作为名字的命名项;第二次我们创建以File2.c作为名字的命名项。这就在引擎中创建了2个“源文件”,然后我们将调用ParseScriptText把File1.c的内容加载给 File1.c命名项。为此,我们必须把命名项的名字作为第三个参数传给ParseScriptText。然后,我们把File2.c的内容加载给File2.c命名项。以下是我们的具体做法:

  1. //Here'sthecontentsofFile1.c
  2. wchar_tFile1[]=L"staticvoidMyFunction(void)
  3. {
  4. printf(\"File1.c\");
  5. }
  6. staticvoidFile1(void)
  7. {
  8. MyFunction();
  9. }";
  10. //Here'sthecontentsofFile2.c
  11. wchar_tFile1[]=L"staticvoidMyFunction(constchar*ptr)
  12. {
  13. printf(ptr);
  14. }
  15. staticvoidFile2(void)
  16. {
  17. MyFunction(\"File1.c\");
  18. }";
  19. //CreatetheFile1.cnameditem.Error-checkingomitted!
  20. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,"File1.c",0);
  21. //CreatetheFile2.cnameditem.
  22. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,"File2.c",0);
  23. //AddtheFile1.ccontentstotheFile1.cnamedobject
  24. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&File1[0],
  25. "File1.c",0,0,0,0,0,0,0);
  26. //AddtheFile2.ccontentstotheFile2.cnamedobject
  27. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&File2[0],
  28. "File2.c",0,0,0,0,0,0,0);
  29. 现在我们把VB“宏脚本”放到创建的命名项中。随便给这个项取名为:MyMacro。我们的具体做法如下:
  30. //Thenameofthenameditem
  31. wchar_tMyMacroObjectName[]=L"MyMacro";
  32. //CreatetheMyMacronameditem
  33. EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript,&MyMacroObjectName[0],SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISPERSISTENT);
  34. //AddthecontentsofVBmacrototheMyMacronameditem
  35. activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,&VBmacro[0],
  36. &MyMacroObjectName[0],0,0,0,0,SCRIPTITEM_ISVISIBLE|SCRIPTTEXT_ISPERSISTENT,
  37. 0,0);

你会注意到,我们传给AddNamedItem的几个标志参数。我们指定了SCRIPTITEM_ISPERSISTENT,因为我们不想让引擎在 被我们重置为INITIALIZED状态时 删除这个命名项(和它的内容)。我们还设置了SCRIPTITEM_ISVISIBLE标志,因为我们想要这个命名项能够被缺省的全局项(即第二段脚本获取添加项的地方)访问。设置SCRIPTITEM_ISVISIBLE标志等价于删除 C语言引擎例子中函数 的static关键字。这会允许一个命名项的函数被其它命名项的函数调用。如果没有SCRIPTITEM_ISVISIBLE, 一个命名项的函数可以自己调用,但不能被其它任何命名项的函数调用。

我们必须修改第二个VB脚本。现在当它调用HelloWorld子程序时,需要引用命名项。在VBscript中,这是通过 把它的名称作为对象使用 来完成的:

  1. wchar_tVBscript[]=L"MyMacro.HelloWorld";

还有一件事情。当我们调用AddNamedItem创建“MyMacro”时,引擎会调用IActiveScriptSite的GetItemInfo,并传递其名字“MyMacro”作为参数。我们需要为这个命名项 获取并返回一个IDispatch指针。这个IDispatch从哪儿来?我们通过 调用引擎IActiveScript对象的GetScriptDispatch函数,传入命名项的名字 来得到它。这就是我们的IActiveScriptSite的GetItemInfo函数:

  1. STDMETHODIMPGetItemInfo(MyRealIActiveScriptSite*this,LPCOLESTR
  2. objectName,DWORDdwReturnMask,IUnknown**objPtr,ITypeInfo**typeInfo)
  3. {
  4. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)*objPtr=0;
  5. if(dwReturnMask&SCRIPTINFO_ITYPEINFO)*typeInfo=0;
  6. //Weassumethatthenameditemtheengineisaskingforisour
  7. //"MyMacro"nameditemwecreated.Weneedtoreturnthe
  8. //IDispatchforthisnameditem.Wheredowegetit?Fromtheengine.
  9. //Specifically,wecalltheengineIActiveScript'sGetScriptDispatch(),
  10. //passingobjectName(whichshouldbe"MyMacro").
  11. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)
  12. return(EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  13. objectName,objPtr));
  14. return(E_FAIL);
  15. }

做了上面的修改,现在宏脚本就有了一个命名项。这有什么好处呢?首先,我们的第二个脚本中就能够也有一个“Hello World”子程序,不会和MyMacro的“Hello World”子程序冲突。所以,我们现在可以排除宏脚本和第二段脚本代码之间 子程序/函数 的命名冲突。此外,如果有更多的宏脚本,我们都可以放到它们各自的命名项中。这样,宏脚本就能有同名的 子程序/函数,在它们之间却不会发生名字冲突。脚本引擎知道那个 子程序/函数 被调用,因为命名项的名称指出了 子程序/函数 所属的命名项。

以下为译者注
*********************************************************
例程和函数的区别
-----------------------------
例程:
	Private Sub abc(a As Integer, b As Integer, c As Integer)
		'your code....
		'c = a + b
	End Sub
函数:
	Private Function c (a As Integer, b As Integer) As Integer
		'your code....
		'c = a + b
	End Function
-----------------------------
*********************************************************

综上所述,使用命名项能够避免 添加子程序/函数,全局变量添加到引擎中时 发生命名冲突。

调用脚本中的特定函数


在上面的例子中,通过调用引擎的GetScriptDispatch,获取特定命名项的IDispatch,我们只是简单的把IDispatch返回给引擎。


但是,除此之外,我们自己还能通过这个IDispatch直接调用 (在特定命名项中的)VB 子例程/函数。为了调用 子例程/函数,我们需要调用IDispatch的GetIDsOfNamesInvoke函数。这和我们在IExampleApp3示例中,当我们使用IDispatch的Invoke来调用com对象中的函数时,做的很类似。你也许应该重新仔细阅读那个例子,以便记起来起来在里面我们是如何做的。 现在,我们要直接调用MyMacros命名项中的HelloWorld子例程。首先我们要通过引擎IActiveScript对象的GetScriptDispatch函数获取命名项的IDispatch。然后调用IDispatch的GetIDsOfNames得到 引擎用于标识我们想要调用的函数的唯一序列号 DISPID。

  1. //NOTE:Error-checkingomitted!
  2. IDispatch*objPtr;
  3. DISPIDdispid;
  4. OLECHAR*funcName;
  5. DISPPARAMSdspp;
  6. VARIANTret;
  7. //GettheIDispatchfor"MyMacro"nameditem
  8. EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  9. "MyMacro",&objPtr);
  10. //NowgettheDISPIDforthe"HelloWorld"sub
  11. funcName=(OLECHAR*)L"HelloWorld";
  12. objPtr->lpVtbl->GetIDsOfNames(objPtr,&IID_NULL,&funcName,1,
  13. LOCALE_USER_DEFAULT,&dispid);
  14. //CallHelloWorld.
  15. //SinceHelloWorldhasnoargspassedtoit,wedon'thavetodo
  16. //anygrotesqueinitializationofDISPPARAMS.
  17. ZeroMemory(&dspp,sizeof(DISPPARAMS));
  18. VariantInit(&ret);
  19. objPtr->lpVtbl->Invoke(objPtr,dispid,&IID_NULL,LOCALE_USER_DEFAULT,
  20. DISPATCH_METHOD,&dspp,&ret,0,0);
  21. VariantClear(&ret);
  22. //ReleasetheIDispatchnowthatwemadethecall
  23. objPtr->lpVtbl->Release(objPtr);

ScriptHost8中,是添加下面的VB脚本(包含main子程序)到vb引擎中的例子:

  1. wchar_tVBscript[]=L"Submain\r\nMsgBox\"Helloworld\"\r\nEndSub";

然后我们直接调用这个 main 程序。你要注意的一件事是我们在调用ParseScriptText时没创建/指定任何特定的命名项。因此这段脚本代码被作为缺省“全局命名项”被加入。所以,我们需要从全局命名项中获取它的IDispatch。我们如何来做呢?我们传一个0给GetScriptDispatch作为名字。这是一个指定的值,告诉GetScriptDispatch返回全局命名项的IDispatch。

查询/设置脚本中变量的值


为了查询或设置某个(在指定命名项中的)变量,我们要做的事情和上面几乎完全一样。唯一不同在于Invoke的调用,当需要获取值时,指定DISPATCH_PROPERTYGET标志;当需要设置值时,我们设置DISPATCH_PROPERTYPUT标志。这就是一个 设置“MyMacro”命名项中变量“MyVariable”的值的 例子:

  1. //NOTE:Error-checkingomitted!
  2. IDispatch*objPtr;
  3. DISPIDdispid,dispPropPut;
  4. OLECHAR*varName;
  5. DISPPARAMSdspp;
  6. VARIANTarg;
  7. //GettheIDispatchfor"MyMacro"nameditem
  8. EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
  9. "MyMacro",&objPtr);
  10. //NowgettheDISPIDforthe"MyVariable"variable(ie,property)
  11. varName=(OLECHAR*)L"MyVariable";
  12. objPtr->lpVtbl->GetIDsOfNames(objPtr,&IID_NULL,&varName,1,
  13. LOCALE_USER_DEFAULT,&dispid);
  14. //Setthevalueto10.
  15. VariantInit(&arg);
  16. ZeroMemory(&dspp,sizeof(DISPPARAMS));
  17. dspp.cArgs=dspp.cNamedArgs=1;
  18. dispPropPut=DISPID_PROPERTYPUT;
  19. dspp.rgdispidNamedArgs=&dispPropPut;
  20. dspp.rgvarg=&arg;
  21. arg.vt=VT_I4;
  22. arg.lVal=10;
  23. objPtr->lpVtbl->Invoke(objPtr,dispid,&IID_NULL,LOCALE_USER_DEFAULT,
  24. DISPATCH_PROPERTYPUT,&dspp,0,0,0);
  25. VariantClear(&arg);
  26. //ReleasetheIDispatchnowthatwemadethecall
  27. objPtr->lpVtbl->Release(objPtr);

ScriptHost9就是一个这样的示例,我们先设置MyVariable的值,然后调用mian子程序显示这个变量。

不同语言的交互


一种语言编写的脚本也可以调用另一种语言编写的脚本。例如,假设我们有下面的VB函数,显示一个消息框:

  1. SubSayHello
  2. MsgBox"HelloWorld"
  3. EndSub

那么就假设我们用下面的jscript函数来调用上面的的Vbscript函数:

  1. functionmain()
  2. {
  3. SayHello();
  4. }

首先,由于我们将会使用2种不同语言的脚本,jscript和vbscript,所以我们需要调用2次CoCreateInstance;一次得到jscript引擎的IActiveScript,另一次得到vbscript引擎的IActiveScript。当然,我们要把这2个指针分别保存到2个变量中(分别为:JActiveScriptVBActiveScript )。


我们还需要得到每个引擎的IActiveScriptParse。同时我们还需要调用每个引擎的SetScriptSite,把IActivesScriptSite传给引擎(我们可以为每个引擎分别传递不同的IActivesScriptSite,但是在这里,我们传给每个引擎同样的IActivesScriptSite,因为我们不会同时运行2种语言的脚本,只有在jscript引擎调用vbscript函数时才会用到vb引擎)。


换言之,runScript必须完成 要使用脚本引擎必需初始化的 工作,但是每个引擎只需要初始化一次。


然后,我们需要调用jscript引擎的ParseScriptText添加上面的jscript代码到jscript引擎中,同时需要调用vbscript引擎的ParseScriptText添加上面的vbscript代码到vbscript引擎中。我们会把这些代码分别加到2个引擎的全局命名项中。


为了方便jscript调用vbscript,我们需要在jscript引擎中创建一个命名项用来和vbscript交互。这需要在添加脚本到引擎中之前来做,我们随便给这个命名项取名为“VB”。

  1. JActiveScript->lpVtbl->AddNamedItem(JActiveScript,L"VB",
  2. SCRIPTITEM_GLOBALMEMBERS|SCRIPTITEM_ISVISIBLE);

让我们来看看当jscript引擎运行上面的jscript代码时发生了什么。引擎检查jscript中的我们载入的所有jscript函数,没有发现"SayHello"的jscript函数。因为我们添加了一些命名项到jscript引擎中(设置GetIDsOfNames标志),引擎就搞对自己说:"呃...也许SayHello函数在某个命名项中。我需要得到这个命名项对应的IDispatch,通过调用它的GetIDsOfNames函数查询SayHello的DISPID,如果IDispatch成功的返回了DISPID,我就调用IDispatch的Invoke来调用SayHello函数"。


但是引擎如何得到命名项的IDispatch呢?目前为止,你应该知道通过调用我们IActiveScriptSite的GetItemInfo。这种情况下,jscript引擎会为命名项名字的参数传一个"VB"。这就是看起来有点莫名其妙的地方。当我们的GetItemInfo找到被指定的项时,我们调用的vbscript引擎GetScriptDispatch来得到vbscript引擎的全局命名项,这正是我们要返回给jscript引擎的东西。


是的,你没有看错。当jscript引擎请求“VB”命名项的IDispatch时,我们实际上返回了VBscript引擎的全局命名项的IDispatch。为什么呢?因为我们vbscript的SayHello 函数被添加到vb引擎的全局命名项中,而不是在"VB"命名项中。换句话说,我们在jscript中把"VB"命名项作为一个代理(placeholder)使用。jscript引擎不需要知道"VB"命名项其实返回了vbscript引擎的全局命名项的IDispatch。


那么,jscript引擎会调用vbscript引擎全局命名项的IDsipatch的GetIDsOfNames,当然,VB引擎会返回SayHello函数的DISPID。当jscript调用IDispatch的Inovke时,最终进入vb引擎中让vb引擎运行VB的SayHello函数。


这里就是我们的IActiveScriptSite的GetItemInfo

  1. STDMETHODIMPGetItemInfo(MyRealIActiveScriptSite*this,LPCOLESTR
  2. objectName,DWORDdwReturnMask,IUnknown**objPtr,ITypeInfo**typeInfo)
  3. {
  4. HRESULThr;
  5. hr=E_FAIL;
  6. if(dwReturnMask&SCRIPTINFO_ITYPEINFO)*typeInfo=0;
  7. if(dwReturnMask&SCRIPTINFO_IUNKNOWN)
  8. {
  9. *objPtr=0;
  10. //Iftheengineisaskingforour"VB"nameditemwecreated,
  11. //thenweknowthisistheJScriptenginecalling.Weneedto
  12. //returntheIDispatchforVBScript's"globalnameditem".
  13. if(!lstrcmpW(objectName,L"VB"))
  14. {
  15. hr=VBActiveScript->lpVtbl->GetScriptDispatch(VBActiveScript,
  16. 0,objPtr);
  17. }
  18. }
  19. return(hr);
  20. }

ScriptHost10中,是jscript调用vbscript的例子。

顺便提一句,你也许对SCRIPTITEM_GLOBALMEMBERS标志有点奇怪。先回想一下我们前面处理一个命名项时,脚本必须像一个对象名那样引用项的名字,例如:

VB.SayHello()

当用SCRIPTITEM_GLOBALMEMBERS标志创建项时,指出了对象名是可选的。例如,上面的代码可以工作,或者还可以这样用:

SayHello()

所以我们做的只是让jscript 在调用vb脚本的SayHello函数时 就像在调用另一个 本地(local) jscript函数。换言之,它或多或少是一种 隐藏命名项麻烦细节的 理论意义上的 捷径。

但这个好处是要付出代价的。就像全局项那样,这些使用SCRIPTITEM_GLOBALMEMBERS标记的项之间可能会出现名字冲突。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics