您的位置:首页 > 其它

译:用标准C编写COM(八) 2010-08-04 15:32:07| 分类: ActiveX Scriptin | 标签: |举报 |字号大

2014-07-02 14:27 405 查看

译:用标准C编写COM(八)

2010-08-04 15:32:07| 分类:

ActiveX Scriptin | 标签:
|举报
|字号大中小 订阅

复杂脚本宿主细节

下载例程-419kb


内容

简介
持久化脚本代码
脚本代码和“命名项”(“Named Items”)
调用脚本中特定函数
查询/设置脚本中的变量
不同语言间的交互


简介

在前面的章节中,我们学习了如何创建一个ActiveX脚本宿主(ActiveX Script Host)。尽管那个章节包含了写一个脚本宿主的最重要的内容,或许你的脚本宿主还会用到另外一些比较深奥的特性。本章会详述这些较深奥的特性。


持久化脚本代码

在我们的前一个脚本宿主例子中,有一个打开脚本引擎、运行脚本和随后关闭引擎的runScript函数。通过这种方式来加载/分析、运行、和随后(在它运行完后)卸载脚本。
但有时你可能想添加些脚本给引擎,然后保留这些添加给引擎的脚本即便在他们没有运行时。或许,你想让用同一引擎的IActiveScript运行的其他脚本随时调用这些脚本。实质上,或许你想像对待“ram-based macros”集那样对待这些脚本。 ActiveX脚本引擎可以做的这一点。但我们必须基于两点来改变我们的方法:

当我们把脚本作为一个“宏”添加时,我们需要指定SCRIPTTEXT_ISPERSISTENT标志给
ParseScriptText。这告诉引擎在脚本引擎里面保留内部已分析、加载过的脚本,即便在ParseScriptText返回后。


我们不能在我们的宏所有对其使用未完前Release引擎的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脚本。这个VB脚本只是调用第一个脚本的HelloWorld例程。

wchar_t VBscript[] = L"HelloWorld";
在我们给引擎添加第二段脚本时,我们没指定SCRIPTTEXT_ISPERSISTENT标志。
为了确保持久化的脚本工作,我们需要让我们的runScript线程保持VB引擎的IActiveScript直到程序终止。为了完成这一点,我们在我们程序开始的部分启动线程(而不是只在运行脚本时才启动线程)。这个线程一直保持活着直到我们的程序结束。最初,引擎调用CoCreateInstance来获取VB引擎的IActiveScriptParse,然后调用InitNew初始化引擎,最后调用SetScriptSite把我们的IActiceScriptSite给引擎。初始化部分跟我们前面的host例子一样。
然后runScript会调用ParseScriptText添加我们的“VB macro”给引擎。这跟前面的例子几乎一样,除了我们指定了SCRIPTTEXT_ISPERSISTENT外:
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &VBmacro[0],
0, 0, 0, 0, 0, SCRIPTTEXT_ISPERSISTENT, 0, 0);

添加完脚本后, runScript线程此时等主线程让它继续,然后运行第二段脚本(它会调用我们刚才加载的脚本)。线程等待我们创建的一个事件信号量。
主窗口有一个“Run Script”按钮。在用户在它上面点击时,主线程设置这个信号量:
// 让脚本线程知道我们要它运行一段脚本
SetEvent(NotifySignal[0]);

runThread醒来调用ParseScriptText添加第二段脚本,然后通过调用SetScriptState来把VB引擎设置为SCRIPTSTATE_CONNECTED状态。这样会运行第二段脚本,它会调用宏脚本中的HelloWorld例程弹出一个消息框。用户取消消息框后,第一段脚本结束,同时SetScriptState返回。
这时,我们不Release IActiveScriptParse和IActiveScript,不要关闭引擎,而是只调用SetScriptState把VB引擎的状态设为SCRIPTSTATE_INITIALIZED。这会导致第二段脚本被卸载。但宏脚本不会被卸载,因为它是持久化的。
runThread回去sleep,等待用户再次点击“Run Script”按钮。在这种情况下,runThread重复添加、运行第二段脚本这一过程。但注意不需要重新添加宏脚本。它一直加载在引擎中。
这是runThread中的“脚本循环”:
for (;;)
{
// 等待主线程通知我们运行脚本
WaitForSingleObject(NotifySignal, INFINITE);


// 脚本引擎分析我们的第二段脚本把它添加到内部脚本运行列表中。注释:我们没有指定
// SCRIPTTEXT_ISPERSISTENT所以脚本会在引擎回到INITIALIZED状态时被卸载。
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,
&VBscript[0], 0, 0, 0, 0, 0, 0, 0, 0);


// 运行我们添加给引擎的所有脚本
EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
SCRIPTSTATE_CONNECTED);


// 上面的脚本在SetScriptState返回后结束。现在我们把引擎的状态设回以初始化来
// 卸载脚本。VBmacro[]依然保持加载状态。
EngineActiveScript->lpVtbl->SetScriptState(EngineActiveScript,
SCRIPTSTATE_INITIALIZED);
}


脚本代码和“命名项Named Items”

在上面的例子中,我们添加宏脚本给同样的“命名项”作为第二段脚本。(注意:我们没指定特定的命名项,所以引擎用它的缺省的全局项)。但在不同的“命名项”下加载脚本是可以的。
在前面的章节中,你应该记得我们通过创建一个命名项(通过引擎IActiveScript的AddNamedItem)让脚本调用我们自己的的C函数。脚本用这个项的名子调用我们的C函数。
但这不是一个命名项的唯一用法。我们也可以对我们创建的命名项进行分类,现在我们就来演示它。
假设我们有两个名为File1.cFile2.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源文件。为了创建一个命名项,我们调用引擎IActiveScript的AddNamedITem。假设我们有一个C语言实现的脚本引擎。首先,我们要调要AddNamedItem两次。第一次,我们创建一个名为File1.c的命名项。第二次我们要创建一个名为File2.c的命名项。这样就在引擎中创建了两个“源文件”。然后我们调用ParseScriptText来添加File1.c的内容给File1.c命名项。为此,我们必须把元素的名字作为第三个参数传给ParseScriptText。然后,我们把File2.c内容添加给File2.c命名项。这是我们的做法:
// 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\
}";


// 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\
}";


// 创建命名项File1.c。忽略错误检查。
EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, "File1.c", 0);

// 创建命名项File2.c
EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, "File2.c", 0);


// 添加File1.c内容到File1.c命名对象中
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &File1[0],
"File1.c", 0, 0, 0, 0, 0, 0, 0);


// 添加File2.c内容到File2.c命名对象中
activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, &File2[0],
"File2.c", 0, 0, 0, 0, 0, 0, 0);

现在我们把我们的VB“宏脚本”放到我们创建的命名项中。我们随意给这个项一个MyMacro名。这是我们在runScript中做的:
// 命名项名子
wchar_t MyMacroObjectName[] = L"MyMacro";


// 创建MyMacro命名项
EngineActiveScript->lpVtbl->AddNamedItem(EngineActiveScript, &MyMacroObjectName[0],
SCRIPTITEM_ISVISIBLE|SCRIPTITEM_ISPERSISTENT);


// 添加VBmacro内容到MyMacro命名项中
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脚本。现在当它调用HellWorld例程时它要引用这个命名项。在VBScript中,这可以通过使用它的名字来完成,就像它是一个对象一样。
wchar_t VBscript[] = L"MyMacro.HelloWorld";

还有些活要干。在我们调用AddNameItem来创建“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;


// 我们假设引擎请求的命名项是我们创建的“MyMacro”命名项。我们要返回这个命名项的IDispatch。
// 我们在哪获得?从引擎。我们调用引擎IActiveScript的GetScriptDispatch(),传入objectName(即“MyMacro”)。

if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
return(EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
objectName, objPtr));

return(E_FAIL);
}

经过上面的细小修改,我们现在已经把我们的宏脚本改造成一个命名项了。这什么好处呢?首先,我们的第二段脚本现在在内部有了一个HellWorld子程序,这与MyMacro的HelloWorld不会冲突。所以,我们现在消除了在我们的宏脚本与第二段脚本之间可能出现的例程、函数名的冲突。此外,如果我们有更多的宏脚本,我们可以把每个都放入它自己的命名项中。这样,每个宏脚本可以有同样的例程/函数名,在这些宏脚本之间不会有冲突。VB引擎知道调用哪个例程/函数,因为调用中对象名表明哪个命名项有这个例程/函数。
总之,用命名项是一个防止你在添加给一个特定引擎脚本代码中可能出现例程/函数冲突的方法。


调用脚本中特定函数

在上面的例子中,我们调用引擎的GetScriptDispatch来重新获取一个指向特定命名项的IDispatch。我们只要把它返回给引擎。
我们也可以用我们自己的IDispatch来直接调用一个特定VB例程/函数(在一个特定的命名项中)。为了调用这个例程/函数,我们调用IDispatch的GetIDsOfNames和Invoke函数。这跟我们在IExampleApp3中用IDispatch的Invoke来调用一个COM对象中的函数时的操作很相似。你可以重新细读那个例子来恢复你的记忆。你可能会说我们想直接调用MyMacros命名对象中的HelloWorld子程序。首先我们需要获取命名对象的IDispatch,我们可以通过调用引擎IActiveScript的GetScriptDispatch来做到。然后我们调用IDispatch的GetIDsOfNames来得到引擎分配给我们想要调用函数的DISPID(也就是唯一数字)。最后我们用这个DISPID通过IDispatch的Invoke来直接调用这个函数。
// 注释:忽略错误检查
IDispatch  *objPtr;
DISPID     dispid;
OLECHAR    *funcName;
DISPPARAMS dspp;
VARIANT    ret;


// 获取“MyMacro”命名项的IDispatch
EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
"MyMacro", &objPtr);


// 现在获取“HelloWorld”子程序的DISPID
funcName = (OLECHAR *)L"HelloWorld";

objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &funcName, 1, LOCALE_USER_DEFAULT, &dispid);


// 调用HelloWorld。由于HellWorld没有要传给它的参数,我们不必做些怪异的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);


// 现在释放我们调用过的IDispatch
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”变量的例子:
// 注释:忽略错误检查!
IDispatch  *objPtr;
DISPID     dispid, dispPropPut;
OLECHAR    *varName;
DISPPARAMS dspp;
VARIANT    arg;


// 获取“MyMacro”命名项的IDispatch
EngineActiveScript->lpVtbl->GetScriptDispatch(EngineActiveScript,
"MyMacro", &objPtr);


// 现在获取“MyVariable”变量的DISPID(即属性)
varName = (OLECHAR *)L"MyVariable";

objPtr->lpVtbl->GetIDsOfNames(objPtr, &IID_NULL, &varName, 1,
LOCALE_USER_DEFAULT, &dispid);


// 设置值为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);


// 现在释放我们调用过的IDispatch
objPtr->lpVtbl->Release(objPtr);

ScriptHost9目录下是一个我们设置MyVariable值然后调用显示这个变量的main例程的例子。


不同语言的交互

以一种语言写的脚本调用以另一种语言写的脚本是可以的。例如,假设我们有下面的VBScript函数来显示一个消息框:
Sub SayHello
MsgBox "Hello World"
End Sub

同时假设我们有下面的JScript函数来调用上面的VBScirpt函数:
function main()
{
SayHello();
}

首先,因为我们要使用两种不同语言的脚本JScript和VBScript,那么我们要调用CoCreateInstance两次;一次获取JScript引擎的IActiveScript,一次获取VBScript引擎的IActiveScript。当然,我们要把这两个指针存储在两个单独的变量中(分别是
JactiveScript
VBActiveScript
)。
我们还得获取每个引擎的IActiveScriptParse。同时我们需要调用每个引擎的SetScriptSite把我们的IactivesScriptSite传给它。(我们可以[/u]对每个引擎使用单独的IActivesScriptSite,但在这我们对两个引擎使用同一个,因为我们不同时运行两种语言的脚本。当然,VB引擎只在JScript引擎调到VBScript函数时才会运行脚本。)
换句话说,我们得在runScript中做使用一个脚本引擎必须做的所有初始化操作,但对每个引擎只做一次。
然后,我们得调用JScript引擎的ParseScriptText来添加上面的Jscirpt代码给JScript引擎,还得调用VBScript引擎的ParseScriptText添加上面的VBScirpt代码给VBScript引擎。我们必须添加代码给各自引擎的全局命名项。
为使JScritpt调用VBScript更方便,我们需要在关联VBScript引擎的JScript引擎中创建命名对象。我们在把脚本添加给引擎前做这些。我们随意给这个项命名为“VB”。
JActiveScript->lpVtbl->AddNamedItem(JActiveScript, L"VB",
SCRIPTITEM_GLOBALMEMBERS|SCRIPTITEM_ISVISIBLE);

让我们看看JScript引擎运行我们上面的JScript代码会怎样。引擎检查我们加载到JScript引擎中的所有JScript函数。好像没有一个叫“SayHello”的JScript函数。因为我们已经添加了命名项给JScript引擎(用了ISVISIBLE标记),引擎对自己说“嗯,或许这个SyaHello函数在这个命名项中。我得获取这个命名项得IDispatch这样我们可以调用它的GetIDsOfName函数请求一个SayHello的DISPID。如果IDispatch成功给我这个DISPID,那么我们那可以通过调用IDispatch的Invoke来调用SayHello函数
那引擎又如何获得一个命名项的IDispatch呢?到此,你应该知道它调用我们的IActiveScirptSite的GetItemInfo。在本例中,JScript引擎会传入一个“VB”的项名。这好像有点乱。当我们的GetItemInfo检测到它请求一个特定的Item时,我们会调用VBScirpt引擎的GetScirptDispatch来获取VBScirpt引擎的全局命名项。我们会把它返回给JScript引擎。
是的,你理解的没错。当JScript引擎请求“VB”命名项的IDispatch时,我们实际上会返回VBScript引擎的全局命名项的IDisaptch。为什么?因为我们的VBScript的SayHello函数添加到了全局命名项中,没有名为“VB”的项。换句话说,我们在JScript引擎中把 “VB”项作为一个代理(placeholder)使用。JScript引擎不需要知道它的“VB”项本质上是VBScript引擎的全局命名项。
所以,JScript引擎会调用VB引擎的全局命名项的IDispatch的GetIDsOfName,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;


// 如果引擎请求我们创建的“VB”命名项,那么我们知道这是JScript引擎调用的。
// 我们要返回VBScript的全局命名项的IDispatch。
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标记的项之间可能会出现名字冲突。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: