COM in plain C, part 8






4.85/5 (23投票s)
2007年1月6日
13分钟阅读

117111

2444
杂项脚本主机详细信息
目录
引言
在前几章中,我们学习了如何创建一个ActiveX脚本宿主。尽管那些章节涵盖了编写脚本宿主的最重要方面,但仍然有一些额外的、更晦涩的功能是您的脚本宿主可能希望利用的。本章将详细介绍其中一些更晦涩的功能。
持久脚本代码
在我们之前的脚本宿主示例中,我们有一个runScript
函数,它打开脚本引擎,运行脚本,然后关闭引擎。通过这种方法,脚本会被加载/解析,运行,然后在(完成运行后)卸载。
但有时您可能希望将一些脚本添加到引擎中,即使这些脚本未在运行,也一直保留在引擎中。也许您希望这些脚本能被您使用同一引擎的IActiveScript运行的任何其他脚本调用。本质上,您可能希望将这些脚本视为一组“基于内存的宏”。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";
以上就是一个名为HelloWorld的VB子过程。它只是显示一个消息框。
接下来,我们将嵌入第二个VB脚本。这个VB脚本将简单地调用第一个脚本的HelloWorld子过程:
wchar_t VBscript[] = L"HelloWorld";
当我们将第二个脚本添加到引擎时,我们不会指定SCRIPTTEXT_ISPERSISTENT标志。
为了使持久脚本生效,我们需要我们的runScript
线程在程序开始时(而不是等到需要运行脚本时才启动线程)就保留VB引擎的IActiveScript。这个线程将一直存活到程序结束。最初,线程将调用CoCreateInstance
获取VB引擎的IActiveScript,然后调用其QueryInterface获取引擎的IActiveScriptParse,然后调用InitNew
初始化引擎,最后调用SetScriptSite
将我们的IActiveScriptSite提供给引擎。这个初始化过程与我们之前的宿主示例相同。
然后runScript将调用ParseScriptText
将我们的“VB宏”添加到引擎。这几乎与前面的示例完全相同,只是我们指定了SCRIPTTEXT_ISPERSISTENT。
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBmacro[0],
0, 0, 0, 0, 0, SCRIPTTEXT_ISPERSISTENT, 0, 0);
添加此脚本后,runScript线程现在等待主线程指示它继续运行第二个脚本(该脚本将调用我们刚刚加载的这个脚本)。线程等待我们创建的事件信号。
主窗口有一个“运行脚本”按钮。当用户点击它时,主线程会设置这个事件信号。
// Let the script thread know that we want it to run a script
SetEvent(NotifySignal[0]);
runThread唤醒并调用ParseScriptText
添加第二个脚本,然后通过调用SetScriptState
将VB引擎状态设置为SCRIPTSTATE_CONNECTED。这将运行第二个脚本,该脚本将调用宏脚本中的HelloWorld子过程以弹出消息框。用户关闭此框后,第一个脚本结束,SetScriptState
返回。
此时,我们不释放IActiveScriptParse和IActiveScript,也不关闭引擎。相反,我们只需再次调用SetScriptState
将VB引擎的状态设置为SCRIPTSTATE_INITIALIZED。这将导致第二个脚本被卸载。但宏脚本不会被卸载,因为它已持久化。
runThread再次进入睡眠状态,等待用户再次点击“运行脚本”按钮。在这种情况下,runThread将重复添加/运行第二个脚本的过程。但请注意,无需重新添加宏脚本。它仍然加载在引擎中。
这是runThread中的“脚本循环”:
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函数。脚本使用该项的名称来调用我们的C函数。
但这并不是命名项的唯一用途。我们还可以将脚本分组到我们创建的命名项下,现在我们将研究这一点。
考虑我们有两个名为File1.c和File2.c的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的命名项。这会在引擎中创建两个“源文件”。然后,我们将调用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)\r\n\
{\r\n\
printf(\"File1.c\");\r\n\
}\r\n\r\n\
static void File1(void)\r\n\
{\r\n\
MyFunction();\r\n\
}";
// Here's the contents of File2.c
wchar_t File1[] = L"static void MyFunction(const char *ptr)\r\n\
{\r\n\
printf(ptr);\r\n\
}\r\n\r\n\
static void File2(void)\r\n\
{\r\n\
MyFunction(\"File1.c\");\r\n\
}";
// 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。以下是我们如何在runScript中执行此操作:
// 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指针。我们从哪里获得它?我们通过调用引擎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);
}
通过上述的微小修改,我们已经为宏脚本使用了命名项。这有什么好处?首先,我们的第二个脚本现在可以在其内部拥有一个HelloWorld子过程,这不会与MyMacro的HelloWorld发生冲突。因此,我们消除了宏脚本和第二个脚本之间任何子过程/函数名称冲突的可能性。此外,如果我们有更多的宏脚本,我们可以将每个脚本放入自己的命名项中。这样,每个宏脚本都可以拥有相似名称的子过程/函数,并且宏脚本之间不会发生冲突。VB引擎知道正在调用哪个子过程/函数,因为调用中使用的对象名称表明哪个命名项包含该子过程/函数。
总之,使用命名项是一种防止向特定引擎添加的脚本代码中可能发生的子过程/函数名称冲突的方法。
调用脚本中的特定函数
在上面的示例中,我们调用了引擎的GetScriptDispatch
来检索特定命名项的IDispatch。我们只是将其返回给引擎。
但我们也可以自己使用该IDispatch来直接调用特定的VB子过程/函数(在特定的命名项中)。要调用子过程/函数,我们调用该IDispatch的GetIDsOfNames
和Invoke
函数。这与我们在IExampleApp3中调用COM对象的某个函数时使用IDispatch的Invoke非常相似。您可能希望再次查阅该示例以刷新您的记忆。假设我们希望直接调用MyMacros命名对象中的HelloWorld子过程。首先,我们需要获取该命名对象的IDispatch,这是通过调用引擎IActiveScript的GetScriptDispatch
来实现的。然后,我们调用该IDispatch的GetIDsOfNames
来获取引擎分配给我们要调用的函数的DISPID(即唯一编号)。最后,我们使用此DISPID和IDispatch的Invoke
来直接调用函数。
// 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。我们如何做到这一点?我们在GetScriptDispatch的名称参数中传递0。这是一个特殊值,它告诉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的值,然后调用main子过程来显示此变量。
互操作不同语言
有可能让用一种语言编写的脚本调用用另一种语言编写的脚本。例如,假设我们有以下显示消息框的VBScript函数:
Sub SayHello
MsgBox "Hello World"
End Sub
假设我们有以下调用上述VBScript函数的JScript函数:
function main() { SayHello(); }
首先,因为我们将使用两种不同语言(JScript和VBScript)的脚本,所以我们需要调用两次CoCreateInstance
;一次获取JScript引擎的IActiveScript,一次获取VBScript引擎的IActiveScript。当然,我们将这两个指针分别存储在两个变量(分别为JActiveScript
和VBActiveScript
)中。
我们还需要获取每个引擎的IActiveScriptParse。并且我们需要调用每个引擎的SetScriptSite
来提供我们的IActivesScriptSite。(我们可以为每个引擎使用单独的IActivesScriptSite,但出于我们的目的,我们将为两个引擎使用同一个,因为我们不会同时运行两种语言的脚本。相反,当JScript引擎调用VBScript函数时,VB引擎才会运行脚本。)
换句话说,runScript
必须执行我们使用脚本引擎所需的所有初始化,但要为每个引擎执行一次。
然后,我们必须调用JScript引擎的ParseScriptText
将上述JScript代码添加到JScript引擎,并调用VBScript引擎的ParseScriptText
将上述VBScript代码添加到VBScript引擎。我们将代码添加到各自引擎的全局命名项中。
为了便于JScript调用VBScript,我们需要在JScript引擎中创建一个命名对象,我们将它与VBScript引擎关联。我们在添加脚本到其引擎之前完成此操作。我们随意给这个项命名为“VB”。
JActiveScript->lpVtbl->AddNamedItem(JActiveScript, L"VB",
SCRIPTITEM_GLOBALMEMBERS|SCRIPTITEM_ISVISIBLE);
让我们看看当JScript引擎运行上面的JScript代码时会发生什么。引擎会查找我们加载到JScript引擎中的所有JScript函数。它发现没有名为“SayHello”的JScript函数。由于我们已经向JScript引擎添加了一些命名项(带有ISVISIBLE
标志),引擎会对自己说:“嗯。也许这个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”的项。换句话说,我们在JScript引擎中使用“VB”项作为“占位符”。JScript引擎不需要知道它的“VB”项实际上将是VBScript引擎的全局命名项。
因此,JScript引擎将调用VB引擎全局命名项IDispatch的GetIDsOfNames
。果然,VB引擎将返回其SayHello函数的DISPID
。当JScript调用那个IDispatch的Invoke时,它最终会调用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能够调用SayHello,就像它是另一个“本地”JScript函数一样。换句话说,这更像是一个名义上的快捷方式,隐藏了命名项的复杂细节。
但是这种便利是有代价的。现在,具有SCRIPTITEM_GLOBALMEMBERS
标志的任何项以及全局命名项之间可能会发生名称冲突。