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

用标准C编写COM(六)COM in plain C,Part6

 
阅读更多

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

如何用C编写ActiveX Script Host。

下载例程-305Kb

内容

  • 简介
  • 选择、打开引擎
  • 我们的IActiveScriptSite对象
  • VBScript例程
  • 初始化引擎
  • 向引擎添加脚本
  • 运行脚本
  • 关闭引擎
  • 加载脚本
  • 枚举已安装引擎
  • 在其他线程运行脚本
  • 结论

简介

当创建一个应用程序时,提供给用户一个他可以通过其写脚本来控制你的应用程序的操作的“宏语言”是比较好的。例如,如果你创建了一个电子邮件程序,或许你想让用户通过写脚本就可以发送电子邮件到指定的地址。为了实现它,可能你的宏语言要提供一个SendMail函数来让脚本调用,传入一个电子邮件地址和正文部分,(使用双引号),语法看起来这样:

  1. SendMail("somebody@somewhere.com","hello")

有了宏语言,用户可以通过写脚本来自动执行重复操作(因此这个“automation”术语常用于描述控制应用程序的脚本),或许甚至可以给你的应用程序添加新函数(如果你的宏语言是足够强大、灵活的话)。

微软给它的许多产品比如Word、Excel等添加了宏语言。事实上,MS使用一个Visual Basic的简单变种作为宏语言的基础。而MS把这个简版的Visual Basic解释器(没有例如把脚本编译成一个可执行文件的特性,其他高级特性也没有)放在一个DLL中。然后MS把特定的COM对象放到Word或Excel可以获取、使用来告诉解释器运行VB脚本的DLL中,从而与应用程序自己的函数交互。例如,有一个特定的IActiveScript的COM对象。MS给他们新的、放在DLL中的简版VB解释器(有COM接口)命名为VBScript。同时这个有特定COM对象集的DLL也被称为ActiveX Script Engine。

当时MS认为让用户可选择宏语言比较好。例如,有些用户可能用类Java(java-like)语言而不是VBScript来写他们的脚本。因此MS又创建了一个包含实现简单Java的解释器DLL。这个解释器叫javascript。Javascript DLL包含跟VBScript DLL同样的COM对象集。MS设计了这些COM对象,因此它们通常是“语言中性(language neutral)”的。换句话说,Excel可以给javascript DLL一段javascript文件让其运行,同样的方式Excel也可以给VBscript DLL一段VBScript文件来运行。所以现在用户可以在两个ActiveX脚本引擎中选择使用。随后,其他第三方可以用这些COM对象集把他们的解释器封装成一个DLL,现在你会发现多种其他比如Python、Perl等语言的ActiveX脚本引擎。其中一些可以被任何支持ActiveX脚本引擎的应用程序使用。

使用ActiveX脚本引擎的应用程序被成为ActiveX脚本宿主(ActiveX Script Host)。

为了应用程序能与引擎交互,应用程序(EXE)必须有它自己的、命名为IActiveScriptSite的特定的COM对象。应用程序调用引擎的一个COM对象函数给引擎一个指向应用程序的IActiveScriptSite COM对象的指针。然后,引擎和应用程序可以通过他们的COM对象的函数来通讯和互相配合运行脚本。

本文会详细说明如何写一个Active脚本宿主-也就是如何写一个可以加载一个ActiveX脚本引擎(dll)的应用程序(exe),调用引擎的COM对象来运行包含在引擎语言指令的脚本(文本文件)。在我们的例程中,我们用VBScript引擎,因此我们例程脚本文件会包含VBScript指令。但我们可以很容易地使用其他引擎、用其他引擎的语言来改写我们的脚本。

总之,ActiveX脚本引擎是一个包含微软定义的标准COM对象的解释器。它可以被知道如何使用这些COM对象的应用程序(也就是可执行程序)使用。这样的应用程序叫ActiveX脚本宿主。一个写的没问题的宿主应该能使用任何交互式引擎。

选择、打开引擎

每个ActiveX脚本引擎必须有它自己唯一的GUID。因此,如果你知道你希望使用那种引擎,你可以传引擎的GUID给CoCreateInstance函数来打开引擎,得到它的IActiveScript对象(就像在第一章中,我们写的传入我们的IExample对象的GUID给CoCreateInstance获取一个IExample对象的应用程序那样)。你应该能在和引擎的“开发包”一起的包含文件中找到引擎的GUID。

一个ActiveX引擎也可以把自己与特殊扩展名的文件关联起来,就像应用程序可以设置文件关联那样。引擎的安装程序会安装一个与文件扩展名关联的注册表键值。例如,VBScript引擎把自己与扩展名为.vbs的文件关联起来。你的应用程序可以通过查找注册表中的文件关联的方法来得到引擎的GUID。(那么,一旦你有了这个GUID,你就可以调用CoCreateInstance了)。

这个函数接受一个你希望得到与之关联引擎的GUID的文件扩展名、一个取得GUID的足够大的缓冲区。这个函数查找相应的注册表键值来找到引擎的GUID,把它拷贝到缓冲区中:

  1. HRESULTgetEngineGuid(LPCTSTRextension,GUID*guidBuffer)
  2. {
  3. wchar_tbuffer[100];
  4. HKEYhk;
  5. DWORDsize;
  6. HKEYsubKey;
  7. DWORDtype;
  8. //查看这个文件扩展名是否与一个ActiveX脚本引擎关联
  9. if(!RegOpenKeyEx(HKEY_CLASSES_ROOT,extension,0,KEY_QUERY_VALUE|KEY_READ,&hk))
  10. {
  11. type=REG_SZ;
  12. size=sizeof(buffer);
  13. size=RegQueryValueEx(hk,0,0,&type,(LPBYTE)&buffer[0],&size);
  14. RegCloseKey(hk);
  15. if(!size)
  16. {
  17. //引擎设置了关联。我们把这个语言字符串放到buffer[]中。现在我们用它来查找
  18. //引擎的GUID
  19. //打开HKEY_CLASSES_ROOT\{LanguageName}
  20. again:size=sizeof(buffer);
  21. if(!RegOpenKeyEx(HKEY_CLASSES_ROOT,(LPCTSTR)&buffer[0],0,KEY_QUERY_VALUE|KEY_READ,&hk))
  22. {
  23. //通过查询CLSID值读GUID(以字符串格式)到buffer[]中
  24. if(!RegOpenKeyEx(hk,"CLSID",0,KEY_QUERY_VALUE|KEY_READ,&subKey))
  25. {
  26. size=RegQueryValueExW(subKey,0,0,&type,(LPBYTE)&buffer[0],&size);
  27. RegCloseKey(subKey);
  28. }
  29. elseif(extension)
  30. {
  31. //如果有错误。看在下面是否包含一个“ScriptEngine”键
  32. //真正的语言名
  33. if(!RegOpenKeyEx(hk,"ScriptEngine",0,KEY_QUERY_VALUE|KEY_READ,&subKey))
  34. {
  35. size=RegQueryValueEx(subKey,0,0,&type,(LPBYTE)&buffer[0],&size);
  36. RegCloseKey(subKey);
  37. if(!size)
  38. {
  39. RegCloseKey(hk);
  40. extension=0;
  41. gotoagain;
  42. }
  43. }
  44. }
  45. }
  46. RegCloseKey(hk);
  47. if(!size)
  48. {
  49. //转换GUID字符串为GUID并把它放在调用者的guidBuffer中
  50. if((size=CLSIDFromString(&buffer[0],guidBuffer)))
  51. MessageBox(0,"Can'tconvertengineGUID","Error",MB_OK|MB_ICONEXCLAMATION);
  52. return(size);
  53. }
  54. }
  55. }
  56. MessageBox(0,"Can'tgetengineGUIDfromregistry","Error",MB_OK|MB_ICONEXCLAMATION);
  57. return(E_FAIL);
  58. }

因此,查找VBScript的GUID,我们可以调用getEngineGuid,像这样传入关联的扩展名“.vbs”:

  1. GUIDguidBuffer;
  2. //查找使用.VBS文件扩展名的脚本引擎。
  3. //注意:微软的VBScript引擎在注册表中为这个扩展名设置了一个关联
  4. getEngineGuid(".vbs",&guidBuffer);

现在,我们可以调用CoCreatInstance来加载、打开VBScript引擎、得到它的IActiveScript对象(到我们命名为activeScript变量中)。注意微软已经在Platform SDK的一个叫activscp.h的包含文件中用IID_IActiveScript定义了IActiveScript对象的GUID,。

  1. #include<window.h>
  2. #include<objbase.h>
  3. #include<activscp.h>
  4. IActiveScript*activeScript;
  5. CoCreateInstance(&guidBuffer,0,CLSCTX_ALL,&IID_IActiveScript,(void**)&activeScript);

我们还需要获得引擎的另一个叫IActiveScriptParse的COM对象。它是IActiveScript对象的一个子对象,所以我们可以把IActiveScriptParse的GUID传给IActiveScript的QueryInterface函数。微软用IID_IActiveScriptParse定义了IActiveScriptParse的GUID。在这我们把获得的这个对象放到我们的activeScriptParse变量中:

  1. IActiveScriptParse*activeScriptParse;
  2. activeScript->lpVtbl->QueryInterface(activeScript,&IID_IActiveScriptParse,(void**)&activeScriptParse);

总之,每个ActiveX脚本引擎有自己的唯一的GUID。一个宿主可以像访问其他COM组件的方式-通过传入唯一的GUID给CoCreateInstance来打开一个引擎(获取引擎的IActiveScript和IActiveScriptParse对象)。此外,引擎可以与特定的扩展名文件关联,这样应该的GUID可以通过查询文件扩展名的注册表键值来“查出”。


我们的IActiveScriptSite对象

我们需要提供我们自己的叫IActiveScriptSite的COM对象。微软也为我们定义了它的GUID和VTable(也就是一个IActiveScriptSiteVtbl结构)。我们要做的是为它写函数。当然,IActiveScriptSite的VTable以QueryInterface、AddRef和Release函数开始。它还包含8个函数GetLCID、GetItemInfo、GetDocVersionString、OnScriptTerminate、OnStateChange、OnScriptError、OnEnterScript和OnLeaveScript。当引擎要通知我们一些事情时它会调用这些函数。例如,当脚本中的函数被调用时我们的OnEnterScript函数就会被调用。当如果脚本本身有错误时我们的OnScriptError会被调用。其他函数为我们给引擎提供信息。例如,引擎调用我们的GetLCID来向我们询问引擎显示的对话框使用的哪种语言LCID。

目前,大部分我们的IActiveScriptSite函数是除了返回S_OK外没做任何事的存根程序。

我们还得提供我们IActiveScriptSite的另一个子对象。这个子对象被称作IActiveScriptSiteWindow。引擎会使用这个子对象与我们打开的任何应用程序窗口交互。这是个可选的对象。我们不必提供它,但如果我们的应用程序打开它自己的窗口,那么提供这个有用的对象。

因为我们需要一个IActiveScriptSiteWindow子对象,我们定义一个MyRealIActiveScriptSite结构来封装我们的IActiveScriptSite和IActiveScriptSiteWidnow:

  1. typedefstruct{
  2. IActiveScriptSitesite;//IActiveScriptSite必须是基对象。
  3. IActiveScriptSiteWindowsiteWnd;//IActiveScriptSite的IActiveScriptSiteWindow子对象。
  4. }MyRealIActiveScriptSite;

对我们来说,我们只需要一个IActiveScriptSite(和它的IActiveScriptSiteWindow),所以最容易的方式就是把它声明为全局,同时VTable也声明为全局的。

  1. //我们的IActiveScriptSiteVTable
  2. IActiveScriptSiteVtblSiteTable={
  3. QueryInterface,
  4. AddRef,
  5. Release,
  6. GetLCID,
  7. GetItemInfo,
  8. GetDocVersionString,
  9. OnScriptTerminate,
  10. OnStateChange,
  11. OnScriptError,
  12. OnEnterScript,
  13. OnLeaveScript};
  14. //IActiveScriptSiteWindowVTable.
  15. IActiveScriptSiteWindowVtblSiteWindowTable={
  16. siteWnd_QueryInterface,
  17. siteWnd_AddRef,
  18. siteWnd_Release,
  19. GetSiteWindow,
  20. EnableModeless};
  21. //这是我们的IActiveScript和它的IActiveScriptSite子对象,
  22. //封装在我们的MyRealIActiveScriptSite结构中。
  23. MyRealIActiveScriptSiteMyActiveScriptSite;

当然,我们需要在程序开始时初始化它的VTable指针。

  1. //初始化我们的IactiveScritpSite和IActiveScriptSiteWindow的lpVtbl成员
  2. MyActiveScriptSite.site.lpVtbl=&SiteTable;
  3. MyActiveScriptSite.siteWnd.lpVtbl=&SiteWindowTable;

在ScriptHost目录中是一个简单的ActiveX脚本宿主例程。IActiveScriptSite.c包含我们的IActiveScriptSite和IActiveScriptSiteWindow对象(封装在我们自己MyRealIActiveScriptSite结构中)的VTable和函数。如前所述,这个例程中的大多数函数是没做任何事的桩程序。唯一重要的事OnScriptError函数。如果脚本中有语法错误(也就是脚本本身书写、格式不正确)或脚本中有运行时错误(例如,引擎在执行脚本时内存越界),引擎会调用我们的OnScriptError函数。

引擎传入一个它自己的叫IActiveScriptError的COM对象,这个对象拥有我们可以调用获取错误信息的函数,例如发生错误在脚本中行号、错误描述的文本消息。(注意:行号是从0开始,因此脚本中第一行的行号是0)。

我们要做的是调用IActiveScriptError的函数来获取信息,重新格式化它,在消息框中显示给用户。

  1. STDMETHODIMPOnScriptError(MyRealIActiveScriptSite*this,IActiveScriptError*scriptError)
  2. {
  3. ULONGlineNumber;
  4. BSTRdesc;
  5. EXCEPINFOei;
  6. OLECHARwszOutput[1024];
  7. //调用GetSourcePosition()来获得发生的错误在脚本中的行号
  8. scriptError->lpVtbl->GetSourcePosition(scriptError,0,&lineNumber,0);
  9. //调用GetSourceLineText()来获得脚本中有错误的行内容
  10. desc=0;
  11. scriptError->lpVtbl->GetSourceLineText(scriptError,&desc);
  12. //调用GetExceptionInfo()来得到更多的信息到我们的ExcEPINFO结构中。
  13. ZeroMemory(&ei,sizeof(EXCEPINFO));
  14. scriptError->lpVtbl->GetExceptionInfo(scriptError,&ei);
  15. //格式化我们要显示给用户的消息
  16. wsprintfW(&wszOutput[0],L"%s\nLine%u:%s\n%s",ei.bstrSource,
  17. lineNumber+1,ei.bstrDescription,desc?desc:"");
  18. //释放我们从IactiveScriptError函数得到的东东
  19. SysFreeString(desc);
  20. SysFreeString(ei.bstrSource);
  21. SysFreeString(ei.bstrDescription);
  22. SysFreeString(ei.bstrHelpFile);
  23. //显示消息
  24. MessageBoxW(0,&wszOutput[0],"Error",MB_SETFOREGROUND|MB_OK|MB_ICONEXCLAMATION);
  25. return(S_OK);
  26. }

注意IActiveScriptError对像只在OnScriptError函数生命期中有效。换句话说,当我们的OnScriptError返回后,指定的IActiveScriptError对象消失了(除非我们明确对它进行AddRef)。

总之,脚本宿主必须提供一个叫IActiveScriptSite的标准COM对象。它也可以提供一个可选的IActiveScriptSite的子对象IActiveScriptSiteWidnow。在最小实现中,函数可以是一个什么也不做的简单桩函数。但,OnScriptError函数通常用于通知用户脚本中错误。

VBScript例程

我们来运行下面的VBScript,它简单显示一个“Hello world”文本的消息框。

  1. MsgBox"Helloworld"

为了容易,我们简单把这个脚本当做一个字符串直接放到我们执行程序中,作为全局数据声明为这样:

  1. wchar_tVBscript[]=L"MsgBox\"Helloworld\"";

有一个重要的事情要注意。我把这个字符串声明为一个宽字符(UNICODE)数据类型,初始化它。(也就是说,wchart_t数据类型表明是宽字符,字符串修饰符L也同样表明是宽字符)。所有脚本引擎函数都接受宽字符字符串。所以,当我们把我们的脚本给VBScript引擎允许时,它必须时UNICODE格式,尽管我们可执行程序本身内部不使用UNICODE。

初始化引擎

在我们运行我们的脚本前,我们首先必须像早些时候所示打开引擎、获得它的IActiveScript对象(通过CoCreateInstance)和它的IActiveScriptParse子对象。

当引擎第一个被打开时,它是在未初始化状态。在我们给引擎运行脚本前,我们必须初始化引擎(只一次)。只要调用一下引擎IActiveScriptParse的Init函数就可以了。

此外,我们需要给引擎一个指向我们IActiveScriptSite对象的指针。同样,我们只需要做一次。只要调用引一下擎IActiveScript的SetScriptSite函数,传入一个指向我们的IActiveScriptSite(它嵌入在我们的MyRealIActiveScriptSite开始位置,因此只要一个简单的强转)的指针就可以了。

在引擎打开后这两个调用我们只需要做一次就够了。

  1. //让引擎做些它需要做的初始化
  2. activeScriptParse->lpVtbl->InitNew(activeScriptParse);
  3. //把我们的IActiveScriptSite对象给引擎。
  4. activeScript->lpVtbl->SetScriptSite(activeScript,
  5. (IActiveScriptSite*)&MyActiveScriptSite);

在做了上面的两个调用后,引擎会自动切换到已初始化状态。现在我们可以添加脚本给引擎了。

注意:引擎的SetScriptSite函数会调用我们的IActiveScriptSite的QueryInterface来要我们返回几个子对象,例如,或许我们会被要求返回一个指向我们的IActiveScriptSiteWindow子对象的指针。如果我们需要对我们自己的COM对象做些预初始化操作,我们应该在调用SetScriptSite前做。

总之,在运行脚本前,宿主必须分别调用引擎的Init和SetScirptSite函数来初始化引擎,把指向宿主的IActiveScriptSite对象的指针给引擎。这只需要在引擎打开后做一次。


添加脚本给引擎

为了运行脚本,我们首先需要把脚本给引擎。我们通过传一个包含脚本的内存缓冲区给引擎IAcitveScriptParse的ParseScriptText函数来完成它。记住脚本必须是宽字符格式。它也必须以nul结束。由于我们的VBScript已经在一个内存缓冲区中了(也就是它在我们的执行程序中是一个全局变量,声明为wchar_t、nul结束),我们要做的是像这样把这个全局变量的地址传进去:

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

ParseScriptText有许多其他参数,但在这对于我们而言,我们可以把它们都设置为0。

那么当我们调用ParseScirptText会发生什么呢?首先,引擎对脚本做语法检查确保它是一个书写正确的脚本。在这,VB引擎确保我们的脚本包含合法的VB指令。如果有语法错误,引擎的ParseScriptTect会调用我们的IActiveScirptSite的OnHandleError。引擎在内部不会添加这个脚本,(在我们的OnHandleError函数返回后)ParseScriptText会返回一个错误(非0)值。

如果脚本语法正确,那么引擎对我们的脚本做一个它自己的拷贝,或许重新格式化它为它自己内部结构,准备运行脚本。但在这时不会运行脚本,因为引擎一致停留在已初始化状态。引擎不会运行任何我们添加给引擎的脚本直到我们把引擎置为start或connected状态。

如果一切正常,ParseScriptText返回0表示成功。引擎现在拥有了我们脚本的它自己内部格式版本,准备运行。(在这时,如果我们的包含脚本的缓冲区动态分配的,现在如果我们想的话可以释放它)

总之,为了执行脚本,宿主必须首先传给引擎的ParseScriptText函数一个包含脚本(宽字符格式,nul结束)的内存缓冲区。这会引起引擎为准备运行脚本时做一个脚本的他自己的拷贝。但引擎在已初始化状态下脚本不会被运行。

运行脚本

为了运行我们的VBScirpt,我们只需要把引擎的状态切换开始start或connected 状态。我们稍后再讨论这两个状态有什么不同,但现在我们只切换到connected状态。我们通过调用引擎的SetScriptState传入希望的状态来改变它的状态,在这是SCRIPTSTATE_CONNECTED状态(定义在MS的activscp.h包含文件中)。

  1. activeScript->lpVtbl->SetScriptState(activeScript,SCRIPTSTATE_CONNECTED);

只要我们一调用,引擎就开始执行脚本中的所有立即指令(immediate instructions)。立即指令是什么?这是语言相关的。在VBScript中,当前指令是指在脚本开始部分不在子例程、函数中的指令。由于我们的例程脚本只包含一条指令。这恰好符合这个描述,这条指令立即执行。我们会看到弹出一个有“Hello World”字符串的消息框。

SetScriptState直到所有那些立即指令都执行完后才返回。在本例中,直到我们解除消息框它才返回。由于这是我们VBScript中仅有的当前指令,SetScriptState返回了。这时,我们不在使用脚本和引擎了,因此我们可以关闭引擎了。

关闭引擎

为了关闭引擎,我们只要这样调用它的IActiveScript的Close函数就可以了:

  1. activeScript->lpVtbl->Close(activeScript);

这会引起引擎停止运行的脚本,释放不再需要的内部资源,切换到closed 状态。引擎会调用我们的IActiveScriptSite的Release函数,释放他从我们这获得的东西(例如释放我们脚本的拷贝)。

当Close返回后,我们然后像这样调用引擎IActiveScriptParse和IActiveScript的Release函数:

  1. activeScript->lpVtbl->Release(activeScript);
  2. activeScript->lpVtbl->Release(activeScript);

我们现在用完了引擎。

加载脚本

当然,如果不让用户写他自己的运行脚本,我们的脚本语言对它而言没什么用。因此我们不是把一个VBScript硬代码写入到我们的执行程序中,我们提供给用户一个文件对话框让他可以选择一个硬盘上的VBScirpt。那么,我们会把脚本加载到内存缓冲区中,保证脚本是宽字符格式和nul结束,把这个内存缓冲区传给ParseScriptText。

我不费神讨论如何给用提供一个文件对话框让其选择它的文件名。

当用户选择了文件名后,我们会把它传给一个返回包含这段脚本格式为宽字符nul结束的内存缓冲区的loadUnicodeScript函数。

  1. OLECHAR*loadUnicodeScript(LPCTSTRfn)
  2. {
  3. OLECHAR*script;
  4. HANDLEhfile;
  5. //假设有错误
  6. script=0;
  7. //打开文件
  8. if((hfile=CreateFile(fn,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,
  9. FILE_ATTRIBUTE_NORMAL,0))!=INVALID_HANDLE_VALUE)
  10. {
  11. DWORDfilesize;
  12. char*psz;
  13. //获取一块用于读文件的nul结束的缓冲区
  14. filesize=GetFileSize(hfile,0);
  15. if((psz=(char*)GlobalAlloc(GMEM_FIXED,filesize+1)))
  16. {
  17. DWORDread;
  18. //读文件
  19. ReadFile(hfile,psz,filesize,&read,0);
  20. //获取一块用于转换为UNICODE的加上一个额外nul结束的wchar_t缓冲区
  21. if((script=(OLECHAR*)GlobalAlloc(GMEM_FIXED,(filesize+1)
  22. *sizeof(OLECHAR))))
  23. {
  24. //转换成UNICODE、nul结束
  25. MultiByteToWideChar(CP_ACP,0,psz,filesize,script,filesize+1);
  26. script[filesize]=0;
  27. }
  28. else
  29. display_sys_error(0);
  30. GlobalFree(psz);
  31. }
  32. else
  33. display_sys_error(0);
  34. CloseHandle(hfile);
  35. }
  36. else
  37. display_sys_error(0);
  38. return(script);
  39. }
  40. voiddisplay_sys_error(DWORDerr)
  41. {
  42. TCHARbuffer[160];
  43. if(!err)err=GetLastError();
  44. buffer[0]=0;
  45. FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,0,err,
  46. MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),&buffer[0],160,0);
  47. MessageBox(0,&buffer[0],"Error",MB_OK);
  48. }

注意:loadUnicodeScript假设磁盘上的文件不是unicode格式。如果碰巧你加载的磁盘文件已经是unicode格式,那么你不用再转换他。在本例中,loadUnicodeScript应该修改成检查文件中的“标示”。考虑其他不同文本文件编码文档的更多信息。

我们可以对我们的代码稍做修改来运行脚本。我们只要调用loadUnicodeScript来加载磁盘脚本到内存缓冲区中,把这个缓冲区传给ParseScriptText。然后,我们可以释放这个缓冲区,把引擎的状态改成connected来运行脚本。

  1. LPOLESTRstr;
  2. //从磁盘加载脚本
  3. str=loadUnicodeScript(fn);
  4. //让脚本引擎分析它,在内部准备运行它。
  5. hr=activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,str,
  6. 0,0,0,0,0,0,0,0);
  7. //我们不再需要加载的脚本了。
  8. GlobalFree(str);
  9. //执行脚本的立即指令
  10. activeScript->lpVtbl->SetScriptState(activeScript,SCRIPTSTATE_CONNECTED);

枚举已安装的引擎

当用户选择运行脚本时,我们不能假定它一定就是一个VBScript。或许它是JScript,或者与Python引擎关联的脚本,等等。

我们要做的是得到他选择的文件名,分离出文件的扩展名,把扩展名传给getEngineGuid,它会给我们相应的我们要打开的引擎的GUID。

但如果文件没有扩展名,或者它的扩展名没有与之关联的已安装的脚本引擎会怎样?在本例中,我们需要提供给用户一个已安装ActiveX脚本引擎的列表,让他手动选择他需要的引擎。然后我们得到选择的引擎的GUID来打开它。

微软的OLE函数提供了一个我们可以用其获取已安装的引擎和他们的GUID的COM对象。这个我们需要获得的COM对象有一个指定的名字ICatInformation,我们让ICatInformation对象来列出脚本引擎。我们可以通过调用CoCreateInstance来得到这个对象。我们然后调用它的EnumClassesOfCategories来得到一个用其Next函数枚举脚本引擎GUID的子对象。此外我们可以调用ProgIDFromCLSID来得到每个引擎的名字(由引擎的安装程序注册的)。

在ScriptHost2目录是一个有窗口的(GUI)显示一个有“Run script”菜单项窗口的C应用程序。当用户选择菜单项时,显示文件对话框得到要运行的脚本的名字。当脚本名选好后,应用程序剥离出扩展名,用这个扩展名来查找关联的引擎的GUID。如果找不到这个引擎,那么应用程序显示一个列举出已安装引擎的对话框让用户选择他要使用的引擎。

源文件ChooseEngine.c包含显示已安装引擎、得到选择引擎的GUID的代码。


在另一个线程运行脚本

我们的GUI应用程序存在一个问题。脚本与我们的用户界面运行在同一个线程中。缺点是,如果脚本做一个无限循环,我们会一直陷入SetScriptState调用中,用户没有办法中断脚本。事实上,当脚本运行时,用户界面总挂在那,这样用户甚至不能移动我们应用程序的窗口。

由于这个原因,最好是启动一个单独的线程来运行脚本。不过有一点要特别注意。大部分引擎的COM函数只能被调用了SetScriptSite的线程调用。这样,我们需要让我们的“脚本线程”在运行脚本中做安装、初始化和清除相关工作。另一个要注意的是我们的IActiveScriptSite函数会在我们的脚本线程中被调用,这样如果我们有些数据同时被我们的IActiveScriptSite函数和UI线程函数访问,我们需要做同步,例如在访问这些数据地方做一个临界区。

在ScriptHost3目录下是一个在第二个线程中运行脚本的ScriptHost2的修改版本。说白了,我们要做的是把我们的runScript函数变成另一个线程的入口点。不需要太多的修改,因为runScript已经做了脚本线程要做的初始化和清除工作。主要的修改涉及线程的初始化和清理。首先,Windows操作系统规定一个线程只能接受一个单一的参数(我们自己选择的)。但我们的runScript需要两个参数:一个文件名和一个GUID。我们需要定义一个新的、单独的封装了两者的结构。我们叫它MYARGS结构,这样定义它:

  1. typedefstruct{
  2. IActiveScript*EngineActiveScript;
  3. HANDLEThreadHandle;
  4. TCHARFilename[MAX_PATH];
  5. GUIDGuid;
  6. }MYARGS;

然后,我们传一个指向我们的MYARGS的指针给runScript。

MYARGS有两个额外函数。ThreadHandle存储一个指向脚本线程的句柄。我们还让脚本线程在我们的MYARGS中存储脚本的IActiveScript对象指针。这样做的原因是主线程也可以在后面访问它。

由于我们一次只启动一个脚本,我们申明一个MYARGS全局变量:

  1. MYARGSMyArgs;

我们的主线程在应用程序开始时初始化它的ThreadHandle成员为0。我们用这个成员来判断脚本线程是否在运行。当ThreadHandle是0时,那么脚本线程没有运行。当非0时,它是一个指向脚本线程的句柄。

runScript需要在线程开始时调用CoInitialize一次。每个线程负责初始化自己的COM库。当然,runScript必须在结束时调用CoUninitialize。此外,我们将改变我们的主进程中调用的CoInitialize为CoInitializeEx,传递一个COINIT_MULTITHREADED值。这样确保如果我们的主线程调用了IActiveScript函数,而引擎不会在我们的脚本线程中阻止我们的主线程和强制执行这个函数。这点对于我们的主线程通过InterruptScriptThread来终止脚本线程非常重要。我们不要信任脚本线程终止自己,如果它“挂了”它做不到这一点。

注意:为了让编译器识别CoInitializeEx,你必须#define _WIN32_WINNT标示为0x0400(或更高),同时你必须在#include objbase.h前做。

当我们的主(UI)线程处理IDM_FILE_RUNSCRIPT消息时,它用要运行的脚本文件名和要使用的引擎的GUID来填充MYARG的Filename和Guid字段。然后我们的主线程像下面这样通过传入我们的MYARGS来调用CreateThread创建、启动脚本线程,如下:

  1. MyArgs.ThreadHandle=CreateThread(0,0,runScript,&MyArgs,0,&wParam);

注意:如果你的脚本线程或IActiveScriptSIte函数调用了C语言函数,那么应该用beginthread。同时检查你的C/C++ “Code Generation”设置确保你使用的是多线程C运行时库。

注意我们把线程的句柄保存在MYARGS的ThreadHandle中。如果脚本线程启动没问题,现在是个非零值。当我们的脚本线程终止时,应该重置ThreadHandle为0。

关于如果脚本线程在运行脚本时出了问题和如果我们的主线程要终止脚本线程该怎么办这两个问题要讨论一下。

为使我们主线程更容易、干净地终止脚本,我们的脚本线程(和我们的IActiveScriptSite函数)应该避免做些会引起线程“暂停”或“等待”的事情。一个例子是调用MessageBox。MessageBox引起线程等待直到用户解除这个消息框。另一个潜在的问题是调用SendMessage。它要等待窗口过程全部处理完消息后返回。这样如果窗口过程线程做些导致暂停或等待的事,那么线程调用SendMessage也注定要暂停和等待。

在runScript中,我们调用我们的display_COM_error函数,它反过来调用MessageBox。这样不好,我们应该只把错误消息传给UI线程,然后让我们的主线程来显示错误消息框。为了做到这一点,我们使用PostMessage。我们使用WM_APP(也就是我们自定义消息号)作为消息号。我们用WPARAM参数传递错误字符串的地址。如果我们传的WPARAM参数是0,那么这意味着LPARAM参数是一个我们应该传给display_sys_error让其获取一个错误消息来显示的错误号。我们用LPARAM参数传递一个HRESULT的错误号。如果我们传入的HRESULT是0,这意味着错误字符串是一个用GlobalAlloc()分配的宽字符字符串。我们主线程需要用MessageBoxW来显示它,同时必须随后对它GlobalFree。

这样例如在runScript中,我们可以把下面的错误处理

  1. if((hr=activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
  2. display_COM_error("Can'tinitializeengine:%08X",hr);

改成

  1. if((hr=activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
  2. PostMessage(MainWindow,WM_APP,(WPARAM)"Can'tinitializeengine:%08X",hr);

我们需要对loadUnicodeScript做点修改,让它不调用display_sys_error,而是调用PostMessage来传错误消息给主线程来显示。

在我们的脚本线程中有一个潜在调用MessageBox的地方,就是在我们IActiveScriptSite中的OnScriptError。我们应该重写它,让它通过GlobalAlloc()分配一个错误消息然后通过PostMessage()把它传给主线程来显示。你可以仔细阅读更新后IActiveScriptSite.c中代码。

我们需要像这样在我们的主窗口过程中添加WM_APP的处理代码:

  1. caseWM_APP:
  2. {
  3. //如果我们的脚本线程要我们显示一个错误消息它会发送一个WM_APP
  4. //wParam=指向要显示的字符串指针。如果是0,那么lParam是一个要传给
  5. //display_sys_error()的错误号
  6. //lParam=HRESULT。如果是0,那么wParam是一个已分配的WCHAR字符串,我们必须对
  7. //它调用GlobalFree()。
  8. if(!wParam)
  9. display_sys_error((DWORD)lParam);
  10. elseif(!lParam)
  11. {
  12. MessageBoxW(hwnd,(constWCHAR*)wParam,"Error",MB_OK|MB_ICONEXCLAMATION);
  13. GlobalFree((void*)wParam);
  14. }
  15. else
  16. display_COM_error((LPCTSTR)wParam,(HRESULT)lParam);
  17. return(0);
  18. }

注意:你可以用RegisterWindowMessage来得到你的自定义消息号,而不是用WM_APP。但对我们而言,WM_APP足够了。

只剩下一件事了-如何在主线程中终止脚本。我们在WM_CLOSE处理中做这件事,那么如果用户在脚本运行中关闭我们的窗口,我们强制终止脚本。引擎的IActiveScript的InterruptScriptThread函数是几个可以被线程调用的函数之一。我们传入一个SCRIPTTHEADID_ALL值,这意味着终止我们给引擎的所有正运行的脚本。(也就是,如果我们创建了几个线程,每个同时运行它自己的VBScript,这会导致VB引擎终止所有的脚本线程)。依次地,如果我们只要终止一个指定的脚本线程,我们可以传入线程的ID。

  1. caseWM_CLOSE:
  2. {
  3. //脚本在运行嘛?
  4. if(MyArgs.ThreadHandle)
  5. {
  6. //通过调用InterruptScriptThread来终止脚本
  7. MyArgs.EngineActiveScript->lpVtbl->InterruptScriptThread(
  8. MyArgs.EngineActiveScript,SCRIPTTHREADID_ALL,0,0);

当InterruptScriptThread返回时,不意味着线程已经终止了。只意味着引擎已经把运行的脚本标记为终止状态。我们依然要“等”线程停止。我们要测试到ThreadHandle是0。(记得上面脚本线程终止时会置0)。但还有一个问题。如果脚本线程不知何故“sleeping”或在等自己得某个东西,例如在调用MessageBox中,那么引擎永远也没有结束它的机会。我们要小心避免调用这样的我们自己的函数,但注意VBScript的msgbox函数也可会调用它。

为了绕过这个问题,我们可以增加一个计数,同时在两次增加这个计数之间使用Sleep()。当这个计数“时间到了”,那么我们假设脚本已经被锁了,我们通过调用TerminateThread强行终止它。

  1. wParam=0;
  2. while(MyArgs.ThreadHandle&&++wParam<25)Sleep(100);
  3. if(MyArgs.ThreadHandle)TerminateThread(MyArgs.ThreadHandle,0);

总之,脚本应该在一个单独的线程中运行而不是在主UI中。脚本线程必须CoInitialize自己。多数引擎的COM函数只能被脚本线程调用。我们的IActiveScriptSite的函数也在脚本线程中被调用。脚本线程应避免做些导致“等待”或“暂停”的操作。UI线程可以通过InterruptScriptThread强制中止脚本,但如果需要的话还需要做“超时”来强行终止脚本。

结论

这一章示范了如何用ActiveX脚本引擎运行脚本。但它只会对运行脚本有帮助,我们还没看到脚本如何与我们应用程序中的函数直接交互,交换数据。为此,我们需要给我们的应用程序添加另一个COM对象。这会是下一章的重点。

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

如何用C编写ActiveX Script Host。

下载例程-305Kb

内容

  • 简介
  • 选择、打开引擎
  • 我们的IActiveScriptSite对象
  • VBScript例程
  • 初始化引擎
  • 向引擎添加脚本
  • 运行脚本
  • 关闭引擎
  • 加载脚本
  • 枚举已安装引擎
  • 在其他线程运行脚本
  • 结论

简介

当创建一个应用程序时,提供给用户一个他可以通过其写脚本来控制你的应用程序的操作的“宏语言”是比较好的。例如,如果你创建了一个电子邮件程序,或许你想让用户通过写脚本就可以发送电子邮件到指定的地址。为了实现它,可能你的宏语言要提供一个SendMail函数来让脚本调用,传入一个电子邮件地址和正文部分,(使用双引号),语法看起来这样:

  1. SendMail("somebody@somewhere.com","hello")

有了宏语言,用户可以通过写脚本来自动执行重复操作(因此这个“automation”术语常用于描述控制应用程序的脚本),或许甚至可以给你的应用程序添加新函数(如果你的宏语言是足够强大、灵活的话)。

微软给它的许多产品比如Word、Excel等添加了宏语言。事实上,MS使用一个Visual Basic的简单变种作为宏语言的基础。而MS把这个简版的Visual Basic解释器(没有例如把脚本编译成一个可执行文件的特性,其他高级特性也没有)放在一个DLL中。然后MS把特定的COM对象放到Word或Excel可以获取、使用来告诉解释器运行VB脚本的DLL中,从而与应用程序自己的函数交互。例如,有一个特定的IActiveScript的COM对象。MS给他们新的、放在DLL中的简版VB解释器(有COM接口)命名为VBScript。同时这个有特定COM对象集的DLL也被称为ActiveX Script Engine。

当时MS认为让用户可选择宏语言比较好。例如,有些用户可能用类Java(java-like)语言而不是VBScript来写他们的脚本。因此MS又创建了一个包含实现简单Java的解释器DLL。这个解释器叫javascript。Javascript DLL包含跟VBScript DLL同样的COM对象集。MS设计了这些COM对象,因此它们通常是“语言中性(language neutral)”的。换句话说,Excel可以给javascript DLL一段javascript文件让其运行,同样的方式Excel也可以给VBscript DLL一段VBScript文件来运行。所以现在用户可以在两个ActiveX脚本引擎中选择使用。随后,其他第三方可以用这些COM对象集把他们的解释器封装成一个DLL,现在你会发现多种其他比如Python、Perl等语言的ActiveX脚本引擎。其中一些可以被任何支持ActiveX脚本引擎的应用程序使用。

使用ActiveX脚本引擎的应用程序被成为ActiveX脚本宿主(ActiveX Script Host)。

为了应用程序能与引擎交互,应用程序(EXE)必须有它自己的、命名为IActiveScriptSite的特定的COM对象。应用程序调用引擎的一个COM对象函数给引擎一个指向应用程序的IActiveScriptSite COM对象的指针。然后,引擎和应用程序可以通过他们的COM对象的函数来通讯和互相配合运行脚本。

本文会详细说明如何写一个Active脚本宿主-也就是如何写一个可以加载一个ActiveX脚本引擎(dll)的应用程序(exe),调用引擎的COM对象来运行包含在引擎语言指令的脚本(文本文件)。在我们的例程中,我们用VBScript引擎,因此我们例程脚本文件会包含VBScript指令。但我们可以很容易地使用其他引擎、用其他引擎的语言来改写我们的脚本。

总之,ActiveX脚本引擎是一个包含微软定义的标准COM对象的解释器。它可以被知道如何使用这些COM对象的应用程序(也就是可执行程序)使用。这样的应用程序叫ActiveX脚本宿主。一个写的没问题的宿主应该能使用任何交互式引擎。

选择、打开引擎

每个ActiveX脚本引擎必须有它自己唯一的GUID。因此,如果你知道你希望使用那种引擎,你可以传引擎的GUID给CoCreateInstance函数来打开引擎,得到它的IActiveScript对象(就像在第一章中,我们写的传入我们的IExample对象的GUID给CoCreateInstance获取一个IExample对象的应用程序那样)。你应该能在和引擎的“开发包”一起的包含文件中找到引擎的GUID。

一个ActiveX引擎也可以把自己与特殊扩展名的文件关联起来,就像应用程序可以设置文件关联那样。引擎的安装程序会安装一个与文件扩展名关联的注册表键值。例如,VBScript引擎把自己与扩展名为.vbs的文件关联起来。你的应用程序可以通过查找注册表中的文件关联的方法来得到引擎的GUID。(那么,一旦你有了这个GUID,你就可以调用CoCreateInstance了)。

这个函数接受一个你希望得到与之关联引擎的GUID的文件扩展名、一个取得GUID的足够大的缓冲区。这个函数查找相应的注册表键值来找到引擎的GUID,把它拷贝到缓冲区中:

  1. HRESULTgetEngineGuid(LPCTSTRextension,GUID*guidBuffer)
  2. {
  3. wchar_tbuffer[100];
  4. HKEYhk;
  5. DWORDsize;
  6. HKEYsubKey;
  7. DWORDtype;
  8. //查看这个文件扩展名是否与一个ActiveX脚本引擎关联
  9. if(!RegOpenKeyEx(HKEY_CLASSES_ROOT,extension,0,KEY_QUERY_VALUE|KEY_READ,&hk))
  10. {
  11. type=REG_SZ;
  12. size=sizeof(buffer);
  13. size=RegQueryValueEx(hk,0,0,&type,(LPBYTE)&buffer[0],&size);
  14. RegCloseKey(hk);
  15. if(!size)
  16. {
  17. //引擎设置了关联。我们把这个语言字符串放到buffer[]中。现在我们用它来查找
  18. //引擎的GUID
  19. //打开HKEY_CLASSES_ROOT\{LanguageName}
  20. again:size=sizeof(buffer);
  21. if(!RegOpenKeyEx(HKEY_CLASSES_ROOT,(LPCTSTR)&buffer[0],0,KEY_QUERY_VALUE|KEY_READ,&hk))
  22. {
  23. //通过查询CLSID值读GUID(以字符串格式)到buffer[]中
  24. if(!RegOpenKeyEx(hk,"CLSID",0,KEY_QUERY_VALUE|KEY_READ,&subKey))
  25. {
  26. size=RegQueryValueExW(subKey,0,0,&type,(LPBYTE)&buffer[0],&size);
  27. RegCloseKey(subKey);
  28. }
  29. elseif(extension)
  30. {
  31. //如果有错误。看在下面是否包含一个“ScriptEngine”键
  32. //真正的语言名
  33. if(!RegOpenKeyEx(hk,"ScriptEngine",0,KEY_QUERY_VALUE|KEY_READ,&subKey))
  34. {
  35. size=RegQueryValueEx(subKey,0,0,&type,(LPBYTE)&buffer[0],&size);
  36. RegCloseKey(subKey);
  37. if(!size)
  38. {
  39. RegCloseKey(hk);
  40. extension=0;
  41. gotoagain;
  42. }
  43. }
  44. }
  45. }
  46. RegCloseKey(hk);
  47. if(!size)
  48. {
  49. //转换GUID字符串为GUID并把它放在调用者的guidBuffer中
  50. if((size=CLSIDFromString(&buffer[0],guidBuffer)))
  51. MessageBox(0,"Can'tconvertengineGUID","Error",MB_OK|MB_ICONEXCLAMATION);
  52. return(size);
  53. }
  54. }
  55. }
  56. MessageBox(0,"Can'tgetengineGUIDfromregistry","Error",MB_OK|MB_ICONEXCLAMATION);
  57. return(E_FAIL);
  58. }

因此,查找VBScript的GUID,我们可以调用getEngineGuid,像这样传入关联的扩展名“.vbs”:

  1. GUIDguidBuffer;
  2. //查找使用.VBS文件扩展名的脚本引擎。
  3. //注意:微软的VBScript引擎在注册表中为这个扩展名设置了一个关联
  4. getEngineGuid(".vbs",&guidBuffer);

现在,我们可以调用CoCreatInstance来加载、打开VBScript引擎、得到它的IActiveScript对象(到我们命名为activeScript变量中)。注意微软已经在Platform SDK的一个叫activscp.h的包含文件中用IID_IActiveScript定义了IActiveScript对象的GUID,。

  1. #include<window.h>
  2. #include<objbase.h>
  3. #include<activscp.h>
  4. IActiveScript*activeScript;
  5. CoCreateInstance(&guidBuffer,0,CLSCTX_ALL,&IID_IActiveScript,(void**)&activeScript);

我们还需要获得引擎的另一个叫IActiveScriptParse的COM对象。它是IActiveScript对象的一个子对象,所以我们可以把IActiveScriptParse的GUID传给IActiveScript的QueryInterface函数。微软用IID_IActiveScriptParse定义了IActiveScriptParse的GUID。在这我们把获得的这个对象放到我们的activeScriptParse变量中:

  1. IActiveScriptParse*activeScriptParse;
  2. activeScript->lpVtbl->QueryInterface(activeScript,&IID_IActiveScriptParse,(void**)&activeScriptParse);

总之,每个ActiveX脚本引擎有自己的唯一的GUID。一个宿主可以像访问其他COM组件的方式-通过传入唯一的GUID给CoCreateInstance来打开一个引擎(获取引擎的IActiveScript和IActiveScriptParse对象)。此外,引擎可以与特定的扩展名文件关联,这样应该的GUID可以通过查询文件扩展名的注册表键值来“查出”。


我们的IActiveScriptSite对象

我们需要提供我们自己的叫IActiveScriptSite的COM对象。微软也为我们定义了它的GUID和VTable(也就是一个IActiveScriptSiteVtbl结构)。我们要做的是为它写函数。当然,IActiveScriptSite的VTable以QueryInterface、AddRef和Release函数开始。它还包含8个函数GetLCID、GetItemInfo、GetDocVersionString、OnScriptTerminate、OnStateChange、OnScriptError、OnEnterScript和OnLeaveScript。当引擎要通知我们一些事情时它会调用这些函数。例如,当脚本中的函数被调用时我们的OnEnterScript函数就会被调用。当如果脚本本身有错误时我们的OnScriptError会被调用。其他函数为我们给引擎提供信息。例如,引擎调用我们的GetLCID来向我们询问引擎显示的对话框使用的哪种语言LCID。

目前,大部分我们的IActiveScriptSite函数是除了返回S_OK外没做任何事的存根程序。

我们还得提供我们IActiveScriptSite的另一个子对象。这个子对象被称作IActiveScriptSiteWindow。引擎会使用这个子对象与我们打开的任何应用程序窗口交互。这是个可选的对象。我们不必提供它,但如果我们的应用程序打开它自己的窗口,那么提供这个有用的对象。

因为我们需要一个IActiveScriptSiteWindow子对象,我们定义一个MyRealIActiveScriptSite结构来封装我们的IActiveScriptSite和IActiveScriptSiteWidnow:

  1. typedefstruct{
  2. IActiveScriptSitesite;//IActiveScriptSite必须是基对象。
  3. IActiveScriptSiteWindowsiteWnd;//IActiveScriptSite的IActiveScriptSiteWindow子对象。
  4. }MyRealIActiveScriptSite;

对我们来说,我们只需要一个IActiveScriptSite(和它的IActiveScriptSiteWindow),所以最容易的方式就是把它声明为全局,同时VTable也声明为全局的。

  1. //我们的IActiveScriptSiteVTable
  2. IActiveScriptSiteVtblSiteTable={
  3. QueryInterface,
  4. AddRef,
  5. Release,
  6. GetLCID,
  7. GetItemInfo,
  8. GetDocVersionString,
  9. OnScriptTerminate,
  10. OnStateChange,
  11. OnScriptError,
  12. OnEnterScript,
  13. OnLeaveScript};
  14. //IActiveScriptSiteWindowVTable.
  15. IActiveScriptSiteWindowVtblSiteWindowTable={
  16. siteWnd_QueryInterface,
  17. siteWnd_AddRef,
  18. siteWnd_Release,
  19. GetSiteWindow,
  20. EnableModeless};
  21. //这是我们的IActiveScript和它的IActiveScriptSite子对象,
  22. //封装在我们的MyRealIActiveScriptSite结构中。
  23. MyRealIActiveScriptSiteMyActiveScriptSite;

当然,我们需要在程序开始时初始化它的VTable指针。

  1. //初始化我们的IactiveScritpSite和IActiveScriptSiteWindow的lpVtbl成员
  2. MyActiveScriptSite.site.lpVtbl=&SiteTable;
  3. MyActiveScriptSite.siteWnd.lpVtbl=&SiteWindowTable;

在ScriptHost目录中是一个简单的ActiveX脚本宿主例程。IActiveScriptSite.c包含我们的IActiveScriptSite和IActiveScriptSiteWindow对象(封装在我们自己MyRealIActiveScriptSite结构中)的VTable和函数。如前所述,这个例程中的大多数函数是没做任何事的桩程序。唯一重要的事OnScriptError函数。如果脚本中有语法错误(也就是脚本本身书写、格式不正确)或脚本中有运行时错误(例如,引擎在执行脚本时内存越界),引擎会调用我们的OnScriptError函数。

引擎传入一个它自己的叫IActiveScriptError的COM对象,这个对象拥有我们可以调用获取错误信息的函数,例如发生错误在脚本中行号、错误描述的文本消息。(注意:行号是从0开始,因此脚本中第一行的行号是0)。

我们要做的是调用IActiveScriptError的函数来获取信息,重新格式化它,在消息框中显示给用户。

  1. STDMETHODIMPOnScriptError(MyRealIActiveScriptSite*this,IActiveScriptError*scriptError)
  2. {
  3. ULONGlineNumber;
  4. BSTRdesc;
  5. EXCEPINFOei;
  6. OLECHARwszOutput[1024];
  7. //调用GetSourcePosition()来获得发生的错误在脚本中的行号
  8. scriptError->lpVtbl->GetSourcePosition(scriptError,0,&lineNumber,0);
  9. //调用GetSourceLineText()来获得脚本中有错误的行内容
  10. desc=0;
  11. scriptError->lpVtbl->GetSourceLineText(scriptError,&desc);
  12. //调用GetExceptionInfo()来得到更多的信息到我们的ExcEPINFO结构中。
  13. ZeroMemory(&ei,sizeof(EXCEPINFO));
  14. scriptError->lpVtbl->GetExceptionInfo(scriptError,&ei);
  15. //格式化我们要显示给用户的消息
  16. wsprintfW(&wszOutput[0],L"%s\nLine%u:%s\n%s",ei.bstrSource,
  17. lineNumber+1,ei.bstrDescription,desc?desc:"");
  18. //释放我们从IactiveScriptError函数得到的东东
  19. SysFreeString(desc);
  20. SysFreeString(ei.bstrSource);
  21. SysFreeString(ei.bstrDescription);
  22. SysFreeString(ei.bstrHelpFile);
  23. //显示消息
  24. MessageBoxW(0,&wszOutput[0],"Error",MB_SETFOREGROUND|MB_OK|MB_ICONEXCLAMATION);
  25. return(S_OK);
  26. }

注意IActiveScriptError对像只在OnScriptError函数生命期中有效。换句话说,当我们的OnScriptError返回后,指定的IActiveScriptError对象消失了(除非我们明确对它进行AddRef)。

总之,脚本宿主必须提供一个叫IActiveScriptSite的标准COM对象。它也可以提供一个可选的IActiveScriptSite的子对象IActiveScriptSiteWidnow。在最小实现中,函数可以是一个什么也不做的简单桩函数。但,OnScriptError函数通常用于通知用户脚本中错误。

VBScript例程

我们来运行下面的VBScript,它简单显示一个“Hello world”文本的消息框。

  1. MsgBox"Helloworld"

为了容易,我们简单把这个脚本当做一个字符串直接放到我们执行程序中,作为全局数据声明为这样:

  1. wchar_tVBscript[]=L"MsgBox\"Helloworld\"";

有一个重要的事情要注意。我把这个字符串声明为一个宽字符(UNICODE)数据类型,初始化它。(也就是说,wchart_t数据类型表明是宽字符,字符串修饰符L也同样表明是宽字符)。所有脚本引擎函数都接受宽字符字符串。所以,当我们把我们的脚本给VBScript引擎允许时,它必须时UNICODE格式,尽管我们可执行程序本身内部不使用UNICODE。

初始化引擎

在我们运行我们的脚本前,我们首先必须像早些时候所示打开引擎、获得它的IActiveScript对象(通过CoCreateInstance)和它的IActiveScriptParse子对象。

当引擎第一个被打开时,它是在未初始化状态。在我们给引擎运行脚本前,我们必须初始化引擎(只一次)。只要调用一下引擎IActiveScriptParse的Init函数就可以了。

此外,我们需要给引擎一个指向我们IActiveScriptSite对象的指针。同样,我们只需要做一次。只要调用引一下擎IActiveScript的SetScriptSite函数,传入一个指向我们的IActiveScriptSite(它嵌入在我们的MyRealIActiveScriptSite开始位置,因此只要一个简单的强转)的指针就可以了。

在引擎打开后这两个调用我们只需要做一次就够了。

  1. //让引擎做些它需要做的初始化
  2. activeScriptParse->lpVtbl->InitNew(activeScriptParse);
  3. //把我们的IActiveScriptSite对象给引擎。
  4. activeScript->lpVtbl->SetScriptSite(activeScript,
  5. (IActiveScriptSite*)&MyActiveScriptSite);

在做了上面的两个调用后,引擎会自动切换到已初始化状态。现在我们可以添加脚本给引擎了。

注意:引擎的SetScriptSite函数会调用我们的IActiveScriptSite的QueryInterface来要我们返回几个子对象,例如,或许我们会被要求返回一个指向我们的IActiveScriptSiteWindow子对象的指针。如果我们需要对我们自己的COM对象做些预初始化操作,我们应该在调用SetScriptSite前做。

总之,在运行脚本前,宿主必须分别调用引擎的Init和SetScirptSite函数来初始化引擎,把指向宿主的IActiveScriptSite对象的指针给引擎。这只需要在引擎打开后做一次。


添加脚本给引擎

为了运行脚本,我们首先需要把脚本给引擎。我们通过传一个包含脚本的内存缓冲区给引擎IAcitveScriptParse的ParseScriptText函数来完成它。记住脚本必须是宽字符格式。它也必须以nul结束。由于我们的VBScript已经在一个内存缓冲区中了(也就是它在我们的执行程序中是一个全局变量,声明为wchar_t、nul结束),我们要做的是像这样把这个全局变量的地址传进去:

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

ParseScriptText有许多其他参数,但在这对于我们而言,我们可以把它们都设置为0。

那么当我们调用ParseScirptText会发生什么呢?首先,引擎对脚本做语法检查确保它是一个书写正确的脚本。在这,VB引擎确保我们的脚本包含合法的VB指令。如果有语法错误,引擎的ParseScriptTect会调用我们的IActiveScirptSite的OnHandleError。引擎在内部不会添加这个脚本,(在我们的OnHandleError函数返回后)ParseScriptText会返回一个错误(非0)值。

如果脚本语法正确,那么引擎对我们的脚本做一个它自己的拷贝,或许重新格式化它为它自己内部结构,准备运行脚本。但在这时不会运行脚本,因为引擎一致停留在已初始化状态。引擎不会运行任何我们添加给引擎的脚本直到我们把引擎置为start或connected状态。

如果一切正常,ParseScriptText返回0表示成功。引擎现在拥有了我们脚本的它自己内部格式版本,准备运行。(在这时,如果我们的包含脚本的缓冲区动态分配的,现在如果我们想的话可以释放它)

总之,为了执行脚本,宿主必须首先传给引擎的ParseScriptText函数一个包含脚本(宽字符格式,nul结束)的内存缓冲区。这会引起引擎为准备运行脚本时做一个脚本的他自己的拷贝。但引擎在已初始化状态下脚本不会被运行。

运行脚本

为了运行我们的VBScirpt,我们只需要把引擎的状态切换开始start或connected 状态。我们稍后再讨论这两个状态有什么不同,但现在我们只切换到connected状态。我们通过调用引擎的SetScriptState传入希望的状态来改变它的状态,在这是SCRIPTSTATE_CONNECTED状态(定义在MS的activscp.h包含文件中)。

  1. activeScript->lpVtbl->SetScriptState(activeScript,SCRIPTSTATE_CONNECTED);

只要我们一调用,引擎就开始执行脚本中的所有立即指令(immediate instructions)。立即指令是什么?这是语言相关的。在VBScript中,当前指令是指在脚本开始部分不在子例程、函数中的指令。由于我们的例程脚本只包含一条指令。这恰好符合这个描述,这条指令立即执行。我们会看到弹出一个有“Hello World”字符串的消息框。

SetScriptState直到所有那些立即指令都执行完后才返回。在本例中,直到我们解除消息框它才返回。由于这是我们VBScript中仅有的当前指令,SetScriptState返回了。这时,我们不在使用脚本和引擎了,因此我们可以关闭引擎了。

关闭引擎

为了关闭引擎,我们只要这样调用它的IActiveScript的Close函数就可以了:

  1. activeScript->lpVtbl->Close(activeScript);

这会引起引擎停止运行的脚本,释放不再需要的内部资源,切换到closed 状态。引擎会调用我们的IActiveScriptSite的Release函数,释放他从我们这获得的东西(例如释放我们脚本的拷贝)。

当Close返回后,我们然后像这样调用引擎IActiveScriptParse和IActiveScript的Release函数:

  1. activeScript->lpVtbl->Release(activeScript);
  2. activeScript->lpVtbl->Release(activeScript);

我们现在用完了引擎。

加载脚本

当然,如果不让用户写他自己的运行脚本,我们的脚本语言对它而言没什么用。因此我们不是把一个VBScript硬代码写入到我们的执行程序中,我们提供给用户一个文件对话框让他可以选择一个硬盘上的VBScirpt。那么,我们会把脚本加载到内存缓冲区中,保证脚本是宽字符格式和nul结束,把这个内存缓冲区传给ParseScriptText。

我不费神讨论如何给用提供一个文件对话框让其选择它的文件名。

当用户选择了文件名后,我们会把它传给一个返回包含这段脚本格式为宽字符nul结束的内存缓冲区的loadUnicodeScript函数。

  1. OLECHAR*loadUnicodeScript(LPCTSTRfn)
  2. {
  3. OLECHAR*script;
  4. HANDLEhfile;
  5. //假设有错误
  6. script=0;
  7. //打开文件
  8. if((hfile=CreateFile(fn,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,
  9. FILE_ATTRIBUTE_NORMAL,0))!=INVALID_HANDLE_VALUE)
  10. {
  11. DWORDfilesize;
  12. char*psz;
  13. //获取一块用于读文件的nul结束的缓冲区
  14. filesize=GetFileSize(hfile,0);
  15. if((psz=(char*)GlobalAlloc(GMEM_FIXED,filesize+1)))
  16. {
  17. DWORDread;
  18. //读文件
  19. ReadFile(hfile,psz,filesize,&read,0);
  20. //获取一块用于转换为UNICODE的加上一个额外nul结束的wchar_t缓冲区
  21. if((script=(OLECHAR*)GlobalAlloc(GMEM_FIXED,(filesize+1)
  22. *sizeof(OLECHAR))))
  23. {
  24. //转换成UNICODE、nul结束
  25. MultiByteToWideChar(CP_ACP,0,psz,filesize,script,filesize+1);
  26. script[filesize]=0;
  27. }
  28. else
  29. display_sys_error(0);
  30. GlobalFree(psz);
  31. }
  32. else
  33. display_sys_error(0);
  34. CloseHandle(hfile);
  35. }
  36. else
  37. display_sys_error(0);
  38. return(script);
  39. }
  40. voiddisplay_sys_error(DWORDerr)
  41. {
  42. TCHARbuffer[160];
  43. if(!err)err=GetLastError();
  44. buffer[0]=0;
  45. FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,0,err,
  46. MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),&buffer[0],160,0);
  47. MessageBox(0,&buffer[0],"Error",MB_OK);
  48. }

注意:loadUnicodeScript假设磁盘上的文件不是unicode格式。如果碰巧你加载的磁盘文件已经是unicode格式,那么你不用再转换他。在本例中,loadUnicodeScript应该修改成检查文件中的“标示”。考虑其他不同文本文件编码文档的更多信息。

我们可以对我们的代码稍做修改来运行脚本。我们只要调用loadUnicodeScript来加载磁盘脚本到内存缓冲区中,把这个缓冲区传给ParseScriptText。然后,我们可以释放这个缓冲区,把引擎的状态改成connected来运行脚本。

  1. LPOLESTRstr;
  2. //从磁盘加载脚本
  3. str=loadUnicodeScript(fn);
  4. //让脚本引擎分析它,在内部准备运行它。
  5. hr=activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse,str,
  6. 0,0,0,0,0,0,0,0);
  7. //我们不再需要加载的脚本了。
  8. GlobalFree(str);
  9. //执行脚本的立即指令
  10. activeScript->lpVtbl->SetScriptState(activeScript,SCRIPTSTATE_CONNECTED);

枚举已安装的引擎

当用户选择运行脚本时,我们不能假定它一定就是一个VBScript。或许它是JScript,或者与Python引擎关联的脚本,等等。

我们要做的是得到他选择的文件名,分离出文件的扩展名,把扩展名传给getEngineGuid,它会给我们相应的我们要打开的引擎的GUID。

但如果文件没有扩展名,或者它的扩展名没有与之关联的已安装的脚本引擎会怎样?在本例中,我们需要提供给用户一个已安装ActiveX脚本引擎的列表,让他手动选择他需要的引擎。然后我们得到选择的引擎的GUID来打开它。

微软的OLE函数提供了一个我们可以用其获取已安装的引擎和他们的GUID的COM对象。这个我们需要获得的COM对象有一个指定的名字ICatInformation,我们让ICatInformation对象来列出脚本引擎。我们可以通过调用CoCreateInstance来得到这个对象。我们然后调用它的EnumClassesOfCategories来得到一个用其Next函数枚举脚本引擎GUID的子对象。此外我们可以调用ProgIDFromCLSID来得到每个引擎的名字(由引擎的安装程序注册的)。

在ScriptHost2目录是一个有窗口的(GUI)显示一个有“Run script”菜单项窗口的C应用程序。当用户选择菜单项时,显示文件对话框得到要运行的脚本的名字。当脚本名选好后,应用程序剥离出扩展名,用这个扩展名来查找关联的引擎的GUID。如果找不到这个引擎,那么应用程序显示一个列举出已安装引擎的对话框让用户选择他要使用的引擎。

源文件ChooseEngine.c包含显示已安装引擎、得到选择引擎的GUID的代码。


在另一个线程运行脚本

我们的GUI应用程序存在一个问题。脚本与我们的用户界面运行在同一个线程中。缺点是,如果脚本做一个无限循环,我们会一直陷入SetScriptState调用中,用户没有办法中断脚本。事实上,当脚本运行时,用户界面总挂在那,这样用户甚至不能移动我们应用程序的窗口。

由于这个原因,最好是启动一个单独的线程来运行脚本。不过有一点要特别注意。大部分引擎的COM函数只能被调用了SetScriptSite的线程调用。这样,我们需要让我们的“脚本线程”在运行脚本中做安装、初始化和清除相关工作。另一个要注意的是我们的IActiveScriptSite函数会在我们的脚本线程中被调用,这样如果我们有些数据同时被我们的IActiveScriptSite函数和UI线程函数访问,我们需要做同步,例如在访问这些数据地方做一个临界区。

在ScriptHost3目录下是一个在第二个线程中运行脚本的ScriptHost2的修改版本。说白了,我们要做的是把我们的runScript函数变成另一个线程的入口点。不需要太多的修改,因为runScript已经做了脚本线程要做的初始化和清除工作。主要的修改涉及线程的初始化和清理。首先,Windows操作系统规定一个线程只能接受一个单一的参数(我们自己选择的)。但我们的runScript需要两个参数:一个文件名和一个GUID。我们需要定义一个新的、单独的封装了两者的结构。我们叫它MYARGS结构,这样定义它:

  1. typedefstruct{
  2. IActiveScript*EngineActiveScript;
  3. HANDLEThreadHandle;
  4. TCHARFilename[MAX_PATH];
  5. GUIDGuid;
  6. }MYARGS;

然后,我们传一个指向我们的MYARGS的指针给runScript。

MYARGS有两个额外函数。ThreadHandle存储一个指向脚本线程的句柄。我们还让脚本线程在我们的MYARGS中存储脚本的IActiveScript对象指针。这样做的原因是主线程也可以在后面访问它。

由于我们一次只启动一个脚本,我们申明一个MYARGS全局变量:

  1. MYARGSMyArgs;

我们的主线程在应用程序开始时初始化它的ThreadHandle成员为0。我们用这个成员来判断脚本线程是否在运行。当ThreadHandle是0时,那么脚本线程没有运行。当非0时,它是一个指向脚本线程的句柄。

runScript需要在线程开始时调用CoInitialize一次。每个线程负责初始化自己的COM库。当然,runScript必须在结束时调用CoUninitialize。此外,我们将改变我们的主进程中调用的CoInitialize为CoInitializeEx,传递一个COINIT_MULTITHREADED值。这样确保如果我们的主线程调用了IActiveScript函数,而引擎不会在我们的脚本线程中阻止我们的主线程和强制执行这个函数。这点对于我们的主线程通过InterruptScriptThread来终止脚本线程非常重要。我们不要信任脚本线程终止自己,如果它“挂了”它做不到这一点。

注意:为了让编译器识别CoInitializeEx,你必须#define _WIN32_WINNT标示为0x0400(或更高),同时你必须在#include objbase.h前做。

当我们的主(UI)线程处理IDM_FILE_RUNSCRIPT消息时,它用要运行的脚本文件名和要使用的引擎的GUID来填充MYARG的Filename和Guid字段。然后我们的主线程像下面这样通过传入我们的MYARGS来调用CreateThread创建、启动脚本线程,如下:

  1. MyArgs.ThreadHandle=CreateThread(0,0,runScript,&MyArgs,0,&wParam);

注意:如果你的脚本线程或IActiveScriptSIte函数调用了C语言函数,那么应该用beginthread。同时检查你的C/C++ “Code Generation”设置确保你使用的是多线程C运行时库。

注意我们把线程的句柄保存在MYARGS的ThreadHandle中。如果脚本线程启动没问题,现在是个非零值。当我们的脚本线程终止时,应该重置ThreadHandle为0。

关于如果脚本线程在运行脚本时出了问题和如果我们的主线程要终止脚本线程该怎么办这两个问题要讨论一下。

为使我们主线程更容易、干净地终止脚本,我们的脚本线程(和我们的IActiveScriptSite函数)应该避免做些会引起线程“暂停”或“等待”的事情。一个例子是调用MessageBox。MessageBox引起线程等待直到用户解除这个消息框。另一个潜在的问题是调用SendMessage。它要等待窗口过程全部处理完消息后返回。这样如果窗口过程线程做些导致暂停或等待的事,那么线程调用SendMessage也注定要暂停和等待。

在runScript中,我们调用我们的display_COM_error函数,它反过来调用MessageBox。这样不好,我们应该只把错误消息传给UI线程,然后让我们的主线程来显示错误消息框。为了做到这一点,我们使用PostMessage。我们使用WM_APP(也就是我们自定义消息号)作为消息号。我们用WPARAM参数传递错误字符串的地址。如果我们传的WPARAM参数是0,那么这意味着LPARAM参数是一个我们应该传给display_sys_error让其获取一个错误消息来显示的错误号。我们用LPARAM参数传递一个HRESULT的错误号。如果我们传入的HRESULT是0,这意味着错误字符串是一个用GlobalAlloc()分配的宽字符字符串。我们主线程需要用MessageBoxW来显示它,同时必须随后对它GlobalFree。

这样例如在runScript中,我们可以把下面的错误处理

  1. if((hr=activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
  2. display_COM_error("Can'tinitializeengine:%08X",hr);

改成

  1. if((hr=activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
  2. PostMessage(MainWindow,WM_APP,(WPARAM)"Can'tinitializeengine:%08X",hr);

我们需要对loadUnicodeScript做点修改,让它不调用display_sys_error,而是调用PostMessage来传错误消息给主线程来显示。

在我们的脚本线程中有一个潜在调用MessageBox的地方,就是在我们IActiveScriptSite中的OnScriptError。我们应该重写它,让它通过GlobalAlloc()分配一个错误消息然后通过PostMessage()把它传给主线程来显示。你可以仔细阅读更新后IActiveScriptSite.c中代码。

我们需要像这样在我们的主窗口过程中添加WM_APP的处理代码:

  1. caseWM_APP:
  2. {
  3. //如果我们的脚本线程要我们显示一个错误消息它会发送一个WM_APP
  4. //wParam=指向要显示的字符串指针。如果是0,那么lParam是一个要传给
  5. //display_sys_error()的错误号
  6. //lParam=HRESULT。如果是0,那么wParam是一个已分配的WCHAR字符串,我们必须对
  7. //它调用GlobalFree()。
  8. if(!wParam)
  9. display_sys_error((DWORD)lParam);
  10. elseif(!lParam)
  11. {
  12. MessageBoxW(hwnd,(constWCHAR*)wParam,"Error",MB_OK|MB_ICONEXCLAMATION);
  13. GlobalFree((void*)wParam);
  14. }
  15. else
  16. display_COM_error((LPCTSTR)wParam,(HRESULT)lParam);
  17. return(0);
  18. }

注意:你可以用RegisterWindowMessage来得到你的自定义消息号,而不是用WM_APP。但对我们而言,WM_APP足够了。

只剩下一件事了-如何在主线程中终止脚本。我们在WM_CLOSE处理中做这件事,那么如果用户在脚本运行中关闭我们的窗口,我们强制终止脚本。引擎的IActiveScript的InterruptScriptThread函数是几个可以被线程调用的函数之一。我们传入一个SCRIPTTHEADID_ALL值,这意味着终止我们给引擎的所有正运行的脚本。(也就是,如果我们创建了几个线程,每个同时运行它自己的VBScript,这会导致VB引擎终止所有的脚本线程)。依次地,如果我们只要终止一个指定的脚本线程,我们可以传入线程的ID。

  1. caseWM_CLOSE:
  2. {
  3. //脚本在运行嘛?
  4. if(MyArgs.ThreadHandle)
  5. {
  6. //通过调用InterruptScriptThread来终止脚本
  7. MyArgs.EngineActiveScript->lpVtbl->InterruptScriptThread(
  8. MyArgs.EngineActiveScript,SCRIPTTHREADID_ALL,0,0);

当InterruptScriptThread返回时,不意味着线程已经终止了。只意味着引擎已经把运行的脚本标记为终止状态。我们依然要“等”线程停止。我们要测试到ThreadHandle是0。(记得上面脚本线程终止时会置0)。但还有一个问题。如果脚本线程不知何故“sleeping”或在等自己得某个东西,例如在调用MessageBox中,那么引擎永远也没有结束它的机会。我们要小心避免调用这样的我们自己的函数,但注意VBScript的msgbox函数也可会调用它。

为了绕过这个问题,我们可以增加一个计数,同时在两次增加这个计数之间使用Sleep()。当这个计数“时间到了”,那么我们假设脚本已经被锁了,我们通过调用TerminateThread强行终止它。

  1. wParam=0;
  2. while(MyArgs.ThreadHandle&&++wParam<25)Sleep(100);
  3. if(MyArgs.ThreadHandle)TerminateThread(MyArgs.ThreadHandle,0);

总之,脚本应该在一个单独的线程中运行而不是在主UI中。脚本线程必须CoInitialize自己。多数引擎的COM函数只能被脚本线程调用。我们的IActiveScriptSite的函数也在脚本线程中被调用。脚本线程应避免做些导致“等待”或“暂停”的操作。UI线程可以通过InterruptScriptThread强制中止脚本,但如果需要的话还需要做“超时”来强行终止脚本。

结论

这一章示范了如何用ActiveX脚本引擎运行脚本。但它只会对运行脚本有帮助,我们还没看到脚本如何与我们应用程序中的函数直接交互,交换数据。为此,我们需要给我们的应用程序添加另一个COM对象。这会是下一章的重点。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics