原文:http://www.codeproject.com/Articles/17038/COM-in-plain-C-part-8
内容
- 简介
- 脚本代码持久化
- 脚本代码和“命名项”
- 调用脚本中的特定函数
- 查询/设置脚本中变量的值
- 查询/设置脚本中变量的值
简介
在前面的章节中,我们学会了如何创建Activex脚本宿主。虽然这些章节覆盖了编写一个脚本宿主的的大部分方面,但是,这里还有其它一些 你的脚本宿主也许想要使用的 更esoteric(深奥,机密)的特性。本章节将会详细的介绍其中一些机密特性。
脚本代码持久化
在先前的脚本宿主示例中,我们用runScript
函数打开脚本引擎,运行脚本,然后关闭引擎。这个函数中,脚本引擎被加载/分析,执行,然后释放(执行完成之后)。
但是,也许有时候,你希望添加一些脚本到引擎,并且让它们一直驻留在里面,即使这些脚本没有被执行。可能,你希望这些脚本能够被任何 能够被相同引擎的IActiveScript识别的 其它脚本所调用。事实上,或许你还想把这些脚本作为"ram-based macros"集合。ActiveX脚本引擎让这成为可能。但我们还需要从以下两方面改变我们的方法:
- 当我们把脚本作为“宏”添加的时候,我们需要为
ParseScriptText
函数指定SCRIPTTEXT_ISPERSISTENT标志。这告诉引擎保留内部已分析\加载过的脚本,即使在ParseScriptText返回之后。 - 我们不能在所有宏被使用之前释放引擎的IActiveScript对象。如果这样做,那些宏最终会被卸载掉。
最好的做法是 在引擎处于INITIALIZED状态时 添加这些宏,但要在引擎被设置为STARTED或CONNECTED状态之前。ParseScriptText
不会尝试运行这些脚本,而是对其语法分析,并在ParseScriptText
返回的时候,在内部把这些脚本保存起来。这些脚本会驻留在引擎中,直到我们释放了引擎的IActiveScript对象,即使在这中间,我们调用了其它脚本或者方法。
在ScriptHost7目录中,你会找到一个说明这一点的例子。我们添加一个VB脚本给引擎,并指定 SCRIPTTEXT_ISPERSISTENT 标志。为了简单,这个VB脚本被作为全局变量,像下面这样嵌入在我们的EXE中:
- wchar_t VBmacro[] = L"Sub HelloWorld\r\nMsgBox \"Hello world\"\r\nEnd Sub";
上面的代码是一个VB的“Hello World”子程序。它只是的弹出一个消息框。
接下来,我们嵌入第二段VB脚本。这个VB脚本只是调用第一个加载的HelloWorld脚本程序。
- wchar_t VBscript[] = L"HelloWorld";
当载入第二段脚本时,我们不指定SCRIPTTEXT_ISPERSISTENT标志。
为了确保持久化的脚本能够工作,需要让runScript线程始终保持VB引擎的IActiveScript对象,直到程序终止。为了完成这一点,我们在程序开始的时候就启动线程(而不是在运行脚本时才启动线程)。这个线程一直保持有效,直到主程序结束。开始,引擎调用CoCreateInstance来获取VB引擎的 IActiveScript,然后调用 IActiveScript的QueryInterface 得到引擎的 IActiveScriptParse对象,再调用InitNew初始化引擎,最后调用SetScriptSite把我们的 IActiceScriptSite传给引擎。初始化部分跟我们前几章的示例一样。
runScript将会调用ParseScriptText
来加载我们的“VB 宏”到引擎中。这几乎和示例程序完全一样,除开指定了 SCRIPTTEXT_ISPERSISTENT 标志:
- activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBmacro[0],
- 0, 0, 0, 0, 0, SCRIPTTEXT_ISPERSISTENT, 0, 0);
添加这个脚本之后,runScript 线程就等待,直到主线程 设置我们创建的一个事件信号量 唤醒,然后运行第二段脚本(它会调用刚才我们装载的第一段脚本)。
主窗口有一个“Run Script”按钮。当用户点击时,主线程就设置事件信号量。
- // Let the script thread know that we want it to run a script
- SetEvent(NotifySignal[0]);
运行线程被唤醒后就调用ParseScriptText
函数加载第二个脚本,然后通过调用SetScriptState
函数,设置VB脚本引擎的状态为SCRIPTSTATE_CONNECTED。 这导致第二段脚本运行, 第二段脚本调用 宏 中的 “Hello World”子程序 弹出一个消息框。当用户关闭消息框之后,第一段脚本运行结束,SetScriptState
函数返回。
此时,我们还没有释放(Rlease) IActiveScriptParse和IActiveScript对象,也没有关闭引擎。我们调用SetScriptState
把状态设置为SCRIPTSTATE_INITIALIZED。这样第二段脚本就被卸载,但宏脚本不会被卸载,因为它是持久化的。
运行线程又重新休眠了。等待用户再次点击“Run Script”按钮。在这个事件中,运行线程重复 加载/运行 第二段脚本的过程。但注意我们不需要重新加载宏脚本,它一直被保留在引擎中。
这就是运行线程中的“脚本循环”:
- for (;;)
- {
- // Wait for main thread to signal us to run a script.
- WaitForSingleObject(NotifySignal, INFINITE);
- // Have the script engine parse our second script and add it to
- // the internal list of scripts to run. NOTE: We do NOT specify
- // SCRIPTTEXT_ISPERSISTENT so this script will be unloaded
- // when the engine goes back to INITIALIZED state.
- activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,
- &VBscript[0], 0, 0, 0, 0, 0, 0, 0, 0);
- // Run all of the scripts that we added to the engine.
- EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
- SCRIPTSTATE_CONNECTED);
- // The above script has ended after SetScriptState returns. Now
- // let's set the engine state back to initialized to unload this
- // script. VBmacro[] remains still loaded.
- EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
- SCRIPTSTATE_INITIALIZED);
- }
脚本代码和“命名项”
在上面的例子中,我们添加宏脚本给同样的“命名项”作为第二段脚本。(注意:我们没指定特定的命名项,所以引擎用它的缺省的全局项)。但在不同的“命名项”下加载脚本是可以的。
在前面的章节中,你应该记得我们可以 通过创建一个命名项(通过引擎IActiveScript对象的的AddNamedItem函数) 让脚本能够调用我们自己的的C函数。
但这不是命名项的唯一用处。我们还可以把创建的命名项分组,这就是我们现在的示例:
假设我们有2个c源文件分别为File1.c 和 File2.c。下面是 File1.c的内容:
- // File1.c
- static void MyFunction(void)
- {
- printf("File1.c");
- }
- static void File1(void)
- {
- MyFunction();
- }
这是 File2.c 的内容:
- // File2.c
- static void MyFunction(const char *ptr)
- {
- printf(ptr);
- }
- static void File2(void)
- {
- MyFunction("File1.c");
- }
还有一些东西需需要注意:
- 由于static关键字修饰,File1.c中的
MyFunction
和File2.c中的MyFunction
就不一样了。我们可以把两个源文件在一起编译和连接不会有问题(也就是说,不会有命名冲突)。 - 由于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命名项。以下是我们的具体做法:
- // Here's the contents of File1.c
- wchar_t File1[] = L"static void MyFunction(void)
- {
- printf(\"File1.c\");
- }
- static void File1(void)
- {
- MyFunction();
- }";
- // Here's the contents of File2.c
- wchar_t File1[] = L"static void MyFunction(const char *ptr)
- {
- printf(ptr);
- }
- static void File2(void)
- {
- MyFunction(\"File1.c\");
- }";
- // Create the File1.c named item. Error-checking omitted!
- EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, "File1.c", 0);
- // Create the File2.c named item.
- EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, "File2.c", 0);
- // Add the File1.c contents to the File1.c named object
- activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &File1[0],
- "File1.c", 0, 0, 0, 0, 0, 0, 0);
- // Add the File2.c contents to the File2.c named object
- activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &File2[0],
- "File2.c", 0, 0, 0, 0, 0, 0, 0);
- 现在我们把VB“宏脚本”放到创建的命名项中。随便给这个项取名为:MyMacro。我们的具体做法如下:
- // The name of the named item
- wchar_t MyMacroObjectName[] = L"MyMacro";
- // Create the MyMacro named item
- EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, &MyMacroObjectName[0], SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISPERSISTENT);
- // Add the contents of VBmacro to the MyMacro named item
- activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBmacro[0],
- &MyMacroObjectName[0], 0, 0, 0, 0, SCRIPTITEM_ISVISIBLE|SCRIPTTEXT_ISPERSISTENT,
- 0, 0);
你会注意到,我们传给AddNamedItem的几个标志参数。我们指定了SCRIPTITEM_ISPERSISTENT,因为我们不想让引擎在 被我们重置为INITIALIZED状态时 删除这个命名项(和它的内容)。我们还设置了SCRIPTITEM_ISVISIBLE标志,因为我们想要这个命名项能够被缺省的全局项(即第二段脚本获取添加项的地方)访问。设置SCRIPTITEM_ISVISIBLE标志等价于删除 C语言引擎例子中函数 的static关键字。这会允许一个命名项的函数被其它命名项的函数调用。如果没有SCRIPTITEM_ISVISIBLE, 一个命名项的函数可以自己调用,但不能被其它任何命名项的函数调用。
我们必须修改第二个VB脚本。现在当它调用HelloWorld子程序时,需要引用命名项。在VBscript中,这是通过 把它的名称作为对象使用 来完成的:
- wchar_t VBscript[] = L"MyMacro.HelloWorld";
还有一件事情。当我们调用AddNamedItem创建“MyMacro”时,引擎会调用IActiveScriptSite的GetItemInfo
,并传递其名字“MyMacro”作为参数。我们需要为这个命名项 获取并返回一个IDispatch指针。这个IDispatch从哪儿来?我们通过 调用引擎IActiveScript对象的GetScriptDispatch
函数,传入命名项的名字 来得到它。这就是我们的IActiveScriptSite的GetItemInfo
函数:
- STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR
- objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo)
- {
- if (dwReturnMask & SCRIPTINFO_IUNKNOWN) *objPtr = 0;
- if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0;
- // We assume that the named item the engine is asking for is our
- // "MyMacro" named item we created. We need to return the
- // IDispatch for this named item. Where do we get it? From the engine.
- // Specifically, we call the engine IActiveScript's GetScriptDispatch(),
- // passing objectName (which should be "MyMacro").
- if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
- return(EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
- objectName, objPtr));
- return(E_FAIL);
- }
做了上面的修改,现在宏脚本就有了一个命名项。这有什么好处呢?首先,我们的第二个脚本中就能够也有一个“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的GetIDsOfNames
和Invoke
函数。这和我们在IExampleApp3示例中,当我们使用IDispatch的Invoke来调用com对象中的函数时,做的很类似。你也许应该重新仔细阅读那个例子,以便记起来起来在里面我们是如何做的。 现在,我们要直接调用MyMacros命名项中的HelloWorld子例程。首先我们要通过引擎IActiveScript对象的GetScriptDispatch
函数获取命名项的IDispatch。然后调用IDispatch的GetIDsOfNames
得到 引擎用于标识我们想要调用的函数的唯一序列号 DISPID。
- // NOTE: Error-checking omitted!
- IDispatch *objPtr;
- DISPID dispid;
- OLECHAR *funcName;
- DISPPARAMS dspp;
- VARIANT ret;
- // Get the IDispatch for "MyMacro" named item
- EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
- "MyMacro", &objPtr);
- // Now get the DISPID for the "HelloWorld" sub
- funcName = (OLECHAR *)L"HelloWorld";
- objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &funcName, 1,
- LOCALE_USER_DEFAULT, &dispid);
- // Call HelloWorld.
- // Since HelloWorld has no args passed to it, we don't have to do
- // any grotesque initialization of DISPPARAMS.
- ZeroMemory(&dspp, sizeof(DISPPARAMS));
- VariantInit(&ret);
- objPtr->lpVtbl->Invoke(objPtr, dispid, &IID_NULL, LOCALE_USER_DEFAULT,
- DISPATCH_METHOD, &dspp, &ret, 0, 0);
- VariantClear(&ret);
- // Release the IDispatch now that we made the call
- objPtr->lpVtbl->Release(objPtr);
在ScriptHost8中,是添加下面的VB脚本(包含main子程序)到vb引擎中的例子:
- wchar_t VBscript[] = L"Sub main\r\nMsgBox \"Hello world\"\r\nEnd Sub";
然后我们直接调用这个 main 程序。你要注意的一件事是我们在调用ParseScriptText时没创建/指定任何特定的命名项。因此这段脚本代码被作为缺省“全局命名项”被加入。所以,我们需要从全局命名项中获取它的IDispatch。我们如何来做呢?我们传一个0给GetScriptDispatch作为名字。这是一个指定的值,告诉GetScriptDispatch返回全局命名项的IDispatch。
查询/设置脚本中变量的值
为了查询或设置某个(在指定命名项中的)变量,我们要做的事情和上面几乎完全一样。唯一不同在于Invoke的调用,当需要获取值时,指定DISPATCH_PROPERTYGET标志;当需要设置值时,我们设置DISPATCH_PROPERTYPUT标志。这就是一个 设置“MyMacro”命名项中变量“MyVariable”的值的 例子:
- // NOTE: Error-checking omitted!
- IDispatch *objPtr;
- DISPID dispid, dispPropPut;
- OLECHAR *varName;
- DISPPARAMS dspp;
- VARIANT arg;
- // Get the IDispatch for "MyMacro" named item
- EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
- "MyMacro", &objPtr);
- // Now get the DISPID for the "MyVariable" variable (ie, property)
- varName = (OLECHAR *)L"MyVariable";
- objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &varName, 1,
- LOCALE_USER_DEFAULT, &dispid);
- // Set the value to 10.
- VariantInit(&arg);
- ZeroMemory(&dspp, sizeof(DISPPARAMS));
- dspp.cArgs = dspp.cNamedArgs = 1;
- dispPropPut = DISPID_PROPERTYPUT;
- dspp.rgdispidNamedArgs = &dispPropPut;
- dspp.rgvarg = &arg;
- arg.vt = VT_I4;
- arg.lVal = 10;
- objPtr->lpVtbl->Invoke(objPtr, dispid, &IID_NULL, LOCALE_USER_DEFAULT,
- DISPATCH_PROPERTYPUT, &dspp, 0, 0, 0);
- VariantClear(&arg);
- // Release the IDispatch now that we made the call
- objPtr->lpVtbl->Release(objPtr);
在ScriptHost9就是一个这样的示例,我们先设置MyVariable的值,然后调用mian子程序显示这个变量。
不同语言的交互
一种语言编写的脚本也可以调用另一种语言编写的脚本。例如,假设我们有下面的VB函数,显示一个消息框:
- Sub SayHello
- MsgBox "Hello World"
- End Sub
那么就假设我们用下面的jscript函数来调用上面的的Vbscript函数:
- function main()
- {
- SayHello();
- }
首先,由于我们将会使用2种不同语言的脚本,jscript和vbscript,所以我们需要调用2次CoCreateInstance
;一次得到jscript引擎的IActiveScript,另一次得到vbscript引擎的IActiveScript。当然,我们要把这2个指针分别保存到2个变量中(分别为:JActiveScript
和VBActiveScript
)。
我们还需要得到每个引擎的IActiveScriptParse。同时我们还需要调用每个引擎的SetScriptSite
,把IActivesScriptSite传给引擎(我们可以为每个引擎分别传递不同的IActivesScriptSite,但是在这里,我们传给每个引擎同样的IActivesScriptSite,因为我们不会同时运行2种语言的脚本,只有在jscript引擎调用vbscript函数时才会用到vb引擎)。
换言之,runScript
必须完成 要使用脚本引擎必需初始化的 工作,但是每个引擎只需要初始化一次。
然后,我们需要调用jscript引擎的ParseScriptText
添加上面的jscript代码到jscript引擎中,同时需要调用vbscript引擎的ParseScriptText
添加上面的vbscript代码到vbscript引擎中。我们会把这些代码分别加到2个引擎的全局命名项中。
为了方便jscript调用vbscript,我们需要在jscript引擎中创建一个命名项用来和vbscript交互。这需要在添加脚本到引擎中之前来做,我们随便给这个命名项取名为“VB”。
- JActiveScript->lpVtbl->AddNamedItem(JActiveScript, L"VB",
- 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
:
- STDMETHODIMP GetItemInfo(MyRealIActiveScriptSite *this, LPCOLESTR
- objectName, DWORD dwReturnMask, IUnknown **objPtr, ITypeInfo **typeInfo)
- {
- HRESULT hr;
- hr = E_FAIL;
- if (dwReturnMask & SCRIPTINFO_ITYPEINFO) *typeInfo = 0;
- if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
- {
- *objPtr = 0;
- // If the engine is asking for our "VB" named item we created,
- // then we know this is the JScript engine calling. We need to
- // return the IDispatch for VBScript's "global named item".
- if (!lstrcmpW(objectName, L"VB"))
- {
- hr = VBActiveScript->lpVtbl->GetScriptDispatch(VBActiveScript,
- 0, objPtr);
- }
- }
- return(hr);
- }
ScriptHost10中,是jscript调用vbscript的例子。
顺便提一句,你也许对SCRIPTITEM_GLOBALMEMBERS标志有点奇怪。先回想一下我们前面处理一个命名项时,脚本必须像一个对象名那样引用项的名字,例如:
VB.SayHello()
当用SCRIPTITEM_GLOBALMEMBERS标志创建项时,指出了对象名是可选的。例如,上面的代码可以工作,或者还可以这样用:
SayHello()
所以我们做的只是让jscript 在调用vb脚本的SayHello函数时 就像在调用另一个 本地(local) jscript函数。换言之,它或多或少是一种 隐藏命名项麻烦细节的 理论意义上的 捷径。
但这个好处是要付出代价的。就像全局项那样,这些使用SCRIPTITEM_GLOBALMEMBERS标记的项之间可能会出现名字冲突。