65.9K
CodeProject 正在变化。 阅读更多。
Home

用纯 C 语言编写 COM,第 6 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (31投票s)

2006 年 7 月 23 日

CPOL

25分钟阅读

viewsIcon

106316

downloadIcon

2420

如何在 C 语言中编写 ActiveX 脚本宿主。

目录

引言

在创建应用程序时,最好为最终用户提供一种“宏语言”,他可以使用该语言编写脚本(即包含“指令”或“命令”的文本文件)来控制应用程序的操作。例如,如果您创建了一个电子邮件程序,也许您希望最终用户可以编写一些脚本来向特定地址发送电子邮件。为此,您的宏语言可能会提供一个 `SendMail` 函数,脚本调用该函数,传入电子邮件地址和文本正文(每个都用双引号括起来),语法如下所示:

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

通过宏语言,最终用户可以编写脚本来自动执行重复操作(因此“自动化”一词用于描述控制应用程序的脚本),甚至可能为您的应用程序添加新功能(如果您的宏语言足够强大/灵活)。

微软决定在 Word、Excel 等许多产品中添加宏语言。事实上,微软决定使用 Visual Basic 的简化变体作为宏语言的基础。因此,微软将 Visual Basic 解释器的简化版本(没有将脚本编译成可执行文件等功能,以及其他高级功能)放入一个 DLL 中。然后,微软将特定的 COM 对象放入该 DLL 中,Word 或 Excel 可以获取/使用这些对象来告诉解释器运行 VB 脚本,并与应用程序自己的函数进行交互。例如,其中一个特殊的 COM 对象是 `IActiveScript` 对象。微软将其新的、简化的 DLL 中的 VB 解释器(带 COM 接口)称为 **VBScript**。而这个 DLL,连同那组特定的 COM 对象,被称为 **ActiveX 脚本引擎**。

然后,微软开始思考,如果能让最终用户选择宏语言就太好了。例如,有些最终用户可能希望使用类似 Java 的语言而不是 VBScript 来编写脚本。因此,微软还创建了另一个 DLL,其中包含一个实现了 Java 简化变体的解释器。这个解释器被称为 **JavaScript**。JavaScript DLL 包含与 VBScript DLL 相同的 COM 对象集。微软设计这些 COM 对象时,使其大部分都是“语言中立”的。换句话说,Excel 可以将 JavaScript 文件交给 JavaScript DLL 运行,就像 Excel 可以将 VBScript 文件交给 VBScript DLL 运行一样。因此,现在最终用户可以选择两种 ActiveX 脚本引擎来使用。后来,其他第三方将它们的解释器打包成 DLL,并使用相同的 COM 对象集,现在您可以在各种其他语言中找到 ActiveX 脚本引擎,例如 Python、Perl 等。它们都可以与任何支持 ActiveX 脚本引擎的应用程序一起使用。

利用 ActiveX 脚本引擎的应用程序被称为 **ActiveX 脚本宿主**。

为了让应用程序与引擎交互,应用程序 (EXE) 必须在其内部拥有自己的特殊 COM 对象,称为 `IActiveScriptSite` 对象。应用程序调用引擎的 COM 对象函数之一,向其提供应用程序的 `IActiveScriptSite` COM 对象的指针。然后,引擎和应用程序可以通过其 COM 对象的函数进行通信和协调脚本的运行。

本文将详细介绍如何编写 ActiveX 脚本宿主——即如何编写一个应用程序 (EXE),该应用程序可以加载其中一个 ActiveX 脚本引擎 (DLL),并调用引擎的 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,并将其复制到缓冲区中:

HRESULT getEngineGuid(LPCTSTR extension, GUID *guidBuffer)
{
   wchar_t   buffer[100];
   HKEY      hk;
   DWORD     size;
   HKEY      subKey;
   DWORD     type;

   // See if this file extension is associated
   // with an ActiveX script engine
   if (!RegOpenKeyEx(HKEY_CLASSES_ROOT, extension, 0, 
       KEY_QUERY_VALUE|KEY_READ, &hk))
   {
      type = REG_SZ;
      size = sizeof(buffer);
      size = RegQueryValueEx(hk, 0, 0, &type, 
             (LPBYTE)&buffer[0], &size);
      RegCloseKey(hk);
      if (!size)
      {
         // The engine set an association.
         // We got the Language string in buffer[]. Now
         // we can use it to look up the engine's GUID

         // Open HKEY_CLASSES_ROOT\{LanguageName}
again:   size = sizeof(buffer);
         if (!RegOpenKeyEx(HKEY_CLASSES_ROOT, (LPCTSTR)&buffer[0], 0, 
                           KEY_QUERY_VALUE|KEY_READ, &hk))
         {
            // Read the GUID (in string format)
            // into buffer[] by querying the value of CLSID
            if (!RegOpenKeyEx(hk, "CLSID", 0, 
                KEY_QUERY_VALUE|KEY_READ, &subKey))
            {
               size = RegQueryValueExW(subKey, 0, 0, &type, 
                      (LPBYTE)&buffer[0], &size);
               RegCloseKey(subKey);
            }
            else if (extension)
            {
               // If an error, see if we have a "ScriptEngine"
               // key under here that contains
               // the real language name
               if (!RegOpenKeyEx(hk, "ScriptEngine", 0, 
                   KEY_QUERY_VALUE|KEY_READ, &subKey))
               {
                  size = RegQueryValueEx(subKey, 0, 0, &type, 
                        (LPBYTE)&buffer[0], &size);
                  RegCloseKey(subKey);
                  if (!size)
                  {
                     RegCloseKey(hk);
                     extension = 0;
                     goto again;
                  }
               }
            }
         }

         RegCloseKey(hk);

         if (!size)
         {
            // Convert the GUID string to a GUID
            // and put it in caller's guidBuffer
            if ((size = CLSIDFromString(&buffer[0], guidBuffer)))
               MessageBox(0, "Can't convert engine GUID", 
                          "Error", MB_OK|MB_ICONEXCLAMATION);
            return(size);
         }
      }
   }

   MessageBox(0, "Can't get engine GUID from registry", 
              "Error", MB_OK|MB_ICONEXCLAMATION);
   return(E_FAIL);
}

因此,要查找 VBScript 的 GUID,我们可以调用 `getEngineGuid`,传入关联的文件扩展名 ".vbs",如下所示:

GUID  guidBuffer;

// Find the script engine to use for files that end with a .VBS extension.
// NOTE: Microsoft's VBscript engine sets up an association in the
// registry for this extension.
getEngineGuid(".vbs", &guidBuffer);

现在,要加载/打开 VBScript 引擎并获取其 `IActiveScript` 对象(存储在我们将命名为 `activeScript` 的变量中),我们可以调用 `CoCreateInstance`。请注意,`IActiveScript` 对象的 GUID 由 Microsoft 定义,使用名称 `IID_IActiveScript`,在名为 `activscp.h` 的头文件中,该文件可在 Platform SDK 中找到。

#include <window.h>
#include <objbase.h>
#include <activscp.h>

IActiveScript  *activeScript;

CoCreateInstance(&guidBuffer, 0, CLSCTX_ALL, 
                 &IID_IActiveScript, 
                 (void **)&activeScript);

我们还需要获取引擎的另一个 COM 对象,称为 `IActiveScriptParse` 对象。这是 `IActiveScript` 对象的子对象,因此我们可以将 `IActiveScriptParse` 的 GUID 传递给 `IActiveScript` 的 `QueryInterface` 函数。Microsoft 已将 `IActiveScriptParse` 的 GUID 定义为 `IID_IActiveScriptParse`。在这里,我们将此对象获取到名为 `activeScriptParse` 的变量中:

IActiveScriptParse  *activeScriptParse;

activeScript->lpVtbl->QueryInterface(activeScript, 
             &IID_IActiveScriptParse, 
             (void **)&activeScriptParse);

总而言之,每个 ActiveX 脚本引擎都有自己唯一的 GUID。宿主可以通过与访问任何其他 COM 组件相同的方式打开引擎(并获取引擎的 `IActiveScript` 和 `IActiveScriptParse` 对象)——通过将该唯一 GUID 传递给 `CoCreateInstance`。此外,引擎可能与特定的文件扩展名关联,因此可以通过查询文件扩展名的注册表项来“查找”引擎的 GUID。

我们的 IActiveScriptSite 对象

我们需要提供自己的 COM 对象,称为 `IActiveScriptSite` 对象。微软已经为我们定义了它的 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` 和 `IActiveScriptSiteWindow`,如下所示:

typedef struct {
   // The IActiveScriptSite must be the base object.
   IActiveScriptSite        site;
   IActiveScriptSiteWindow  siteWnd;
   // Our IActiveScriptSiteWindow sub-object
   // for this IActiveScriptSite.
} MyRealIActiveScriptSite;

就我们而言,我们只需要一个 `IActiveScriptSite`(及其 `IActiveScriptSiteWindow`),所以最简单的方法就是全局声明它,并全局声明 VTable:

// Our IActiveScriptSite VTable.
IActiveScriptSiteVtbl SiteTable = {
 QueryInterface,
 AddRef,
 Release,
 GetLCID,
 GetItemInfo,
 GetDocVersionString,
 OnScriptTerminate,
 OnStateChange,
 OnScriptError,
 OnEnterScript,
 OnLeaveScript};

// IActiveScriptSiteWindow VTable.
IActiveScriptSiteWindowVtbl SiteWindowTable = {
 siteWnd_QueryInterface,
 siteWnd_AddRef,
 siteWnd_Release,
 GetSiteWindow,
 EnableModeless};

// Here's our IActiveScript and its IActiveScriptSite sub-object, wrapped
// in our MyRealIActiveScriptSite struct.
MyRealIActiveScriptSite  MyActiveScriptSite;

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

// Initialize the lpVtbl members of our IActiveScriptSite and
// IActiveScriptSiteWindow sub-objects
MyActiveScriptSite.site.lpVtbl = &SiteTable;
MyActiveScriptSite.siteWnd.lpVtbl = &SiteWindowTable;

在 `ScriptHost` 目录下,有一个简单的 ActiveX 脚本宿主示例。源文件 `IActiveScriptSite.c` 包含我们 `IActiveScriptSite` 和 `IActiveScriptSiteWindow` 对象(它们封装在我们自己的 `MyRealIActiveScriptSite` 结构体中)的 VTable 和函数。如本示例所述,大多数函数只是空存根例程。唯一非平凡的函数是 `OnScriptError`。如果脚本中存在语法错误(即脚本本身编写/格式不正确),或者脚本中存在运行时错误(例如,引擎在执行脚本时内存不足),引擎将调用我们的 `OnScriptError` 函数。

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

我们所做的只是调用一些 `IActiveScriptError` 函数来获取一些信息,对其进行重新格式化,并以消息框的形式显示给用户。

STDMETHODIMP OnScriptError(MyRealIActiveScriptSite *this, 
             IActiveScriptError *scriptError)
{
   ULONG        lineNumber;
   BSTR         desc;
   EXCEPINFO    ei;
   OLECHAR      wszOutput[1024];

   // Call GetSourcePosition() to retrieve the line # where
   // the error occurred in the script
   scriptError->lpVtbl->GetSourcePosition(scriptError, 0, &lineNumber, 0);

   // Call GetSourceLineText() to retrieve the line in the script that
   // has an error.
   desc = 0;
   scriptError->lpVtbl->GetSourceLineText(scriptError, &desc);

   // Call GetExceptionInfo() to fill in our EXCEPINFO struct with more
   // information.
   ZeroMemory(&ei, sizeof(EXCEPINFO));
   scriptError->lpVtbl->GetExceptionInfo(scriptError, &ei);

   // Format the message we'll display to the user
   wsprintfW(&wszOutput[0], L"%s\nLine %u: %s\n%s", ei.bstrSource,
      lineNumber + 1, ei.bstrDescription, desc ? desc : "");

   // Free what we got from the IActiveScriptError functions
   SysFreeString(desc);
   SysFreeString(ei.bstrSource);
   SysFreeString(ei.bstrDescription);
   SysFreeString(ei.bstrHelpFile);

   // Display the message
   MessageBoxW(0, &wszOutput[0], "Error", 
               MB_SETFOREGROUND|MB_OK|MB_ICONEXCLAMATION);
 
   return(S_OK);
}

请注意,`IActiveScriptError` 对象的生命周期仅限于我们的 `OnScriptError` 函数。换句话说,当我们的 `OnScriptError` 返回时,该特定的 `IActiveScriptError` 对象就会消失(除非我们明确地 `AddRef` 它)。另请注意,`IActiveScriptError` 的函数返回我们请求的任何信息的副本,因此我们最终必须释放它返回给我们的任何 `BSTR`。

总而言之,脚本宿主必须提供一个名为 `IActiveScriptSite` 的标准 COM 对象。它还可以选择提供一个 `IActiveScriptSiteWindow`,它是 `IActiveScriptSite` 的子对象。在最小化实现中,这些函数可以只是什么都不做的存根函数。但是,`OnScriptError` 函数通常用于通知用户脚本中发生的任何错误。

VBScript 示例

让我们运行以下 VBScript,它只是显示一个带有文本“Hello world”的消息框:

MsgBox "Hello world"

为了方便起见,我们只需将此脚本作为字符串直接嵌入到我们的可执行文件中,作为全局数据声明,如下所示:

wchar_t VBscript[] = L"MsgBox \"Hello world\"";

有一件重要的事情需要注意。我已将此字符串声明为宽 (UNICODE) 数据类型,并如此初始化(即,数据类型 `wchar_t` 表示宽字符,字符串上的 **L** 限定符也表示这一点)。所有脚本引擎函数都期望宽字符字符串。因此,当我们向 VBScript 引擎提供要运行的脚本时,它必须是 UNICODE 格式,即使我们的可执行文件本身内部没有使用 UNICODE。

初始化引擎

在运行脚本之前,我们必须首先打开引擎并获取其 `IActiveScript` 对象(通过 `CoCreateInstance`)及其 `IActiveScriptParse` 子对象,如前所述。

当引擎首次打开时,它处于“未初始化状态”。在我们向引擎提供任何要运行的脚本之前,我们必须初始化引擎(只需一次)。这仅涉及调用引擎 `IActiveScriptParse` 的 `Init` 函数。

此外,我们需要向引擎提供一个指向我们的 `IActiveScriptSite` 对象的指针。同样,我们只需要执行一次。这仅仅涉及调用引擎 `IActiveScript` 的 `SetScriptSite` 函数,传入一个指向我们的 `IActiveScriptSite` 的指针(它嵌入在我们的 `MyRealIActiveScriptSite` 的开头,所以一个简单的强制转换就可以完成任务)。

那么,这是引擎打开后我们只需要执行一次的两次调用:

// Let the engine do any initialization it needs to do.
activeScriptParse->lpVtbl->InitNew(activeScriptParse);

// Give the engine our IActiveScriptSite object.
activeScript->lpVtbl->SetScriptSite(activeScript,
     (IActiveScriptSite *)&MyActiveScriptSite);

完成上述两个调用后,引擎将自动切换到“已初始化状态”。它现在已准备好让我们向引擎添加脚本。

注意:引擎的 `SetScriptSite` 函数可能会调用我们 `IActiveScriptSite` 的 `QueryInterface` 来要求我们返回几个子对象。例如,也许我们会要求返回一个指向我们 `IActiveScriptSiteWindow` 子对象的指针。当我们调用 `SetScriptSite` 时,我们应该准备好提供任何请求的子对象。如果我们需要对我们自己的 COM 对象进行任何预初始化,我们应该在调用 `SetScriptSite` 之前完成。

总而言之,在运行任何脚本之前,宿主必须调用引擎的 `Init` 和 `SetScriptSite` 函数,分别初始化引擎并提供指向宿主 `IActiveScriptSite` 对象的指针。这应该在引擎打开后只做一次。

引擎添加脚本

为了运行脚本,我们首先需要将脚本提供给引擎。我们通过将包含脚本的内存缓冲区传递给引擎 `IActiveScriptParse` 的 `ParseScriptText` 函数来完成此操作。请记住,脚本必须采用宽字符格式,并且必须以空字符结尾。由于我们的 VBScript 已经在一个内存缓冲区中(即,它是我们可执行文件中的一个全局变量,声明为 `wchar_t` 并以空字符结尾),我们只需传递该全局变量的地址,如下所示:

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

`ParseScriptText` 接受许多其他参数,但在此处,我们可以将它们全部设置为 0。

那么,当我们调用 `ParseScriptText` 时会发生什么?首先,引擎会检查脚本的语法,以确保它是一个正确编写的脚本。在这里,VB 引擎会确保我们的脚本包含合法的 VB 指令。如果存在语法错误,引擎的 `ParseScriptText` 将调用我们 `IActiveScriptSite` 的 `OnHandleError`。引擎不会在内部添加脚本,并且(在我们的 `OnHandleError` 函数返回后)`ParseScriptText` 将返回一个错误(非零)值。

如果脚本在语法上是正确的,那么引擎会创建我们脚本的副本,也许会将其重新格式化为它自己选择的内部结构,以准备运行脚本。但引擎此时不会运行脚本,因为引擎仍处于其初始化状态。在我们将引擎置于“启动”或“连接”状态之前,引擎不会运行我们添加到引擎中的任何脚本。

如果一切顺利,`ParseScriptText` 返回 0 表示成功。引擎现在拥有我们脚本的内部格式化版本,准备运行。(此时,如果我们在包含脚本的缓冲区上分配了内存,如果需要,我们现在可以将其释放。)

总而言之,为了运行脚本,宿主必须首先将包含脚本的内存缓冲区(以宽字符格式并以空字符结尾)传递给引擎的 `ParseScriptText` 函数。这会使引擎创建脚本的副本,以准备运行脚本。但在引擎仍处于初始化状态时,脚本不会运行。

运行脚本

要运行我们的 VBScript,我们只需将引擎切换到其“启动”或“连接”状态。我们稍后将讨论这两种状态之间的区别,但现在,我们只需切换到连接状态。我们通过调用其 `IActiveScript` 的 `SetScriptState` 来更改引擎的状态,传入所需状态,这里是常量 `SCRIPTSTATE_CONNECTED`(在 MS 的 `activscp.h` 头文件中定义)。

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

一旦我们进行此调用,引擎就会开始执行脚本中的任何“立即指令”。什么是立即指令?这取决于语言。在 VBScript 中,立即指令是脚本开头未包含在任何子例程/函数中的任何指令。由于我们的示例脚本包含一个恰好符合该描述的指令,因此该指令会立即执行。我们应该看到一个弹出消息框,其中包含字符串“Hello World”。

`SetScriptState` 在所有这些立即指令完成之前不会返回。在这种情况下,它不会返回,直到我们关闭消息框。由于这是我们 VBScript 中的最后一个立即指令,`SetScriptState` 返回。此时,我们不再需要脚本或引擎,因此我们可以关闭引擎。

关闭引擎

要关闭引擎,我们只需调用其 `IActiveScript` 的 `Close` 函数,如下所示:

activeScript->lpVtbl->Close(activeScript);

这应该会导致引擎停止所有正在运行的脚本,释放所有不再需要的内部资源,并切换到“关闭”状态。它应该调用我们 `IActiveScriptSite` 的 `Release` 函数,并释放它从我们这里获得的任何东西(例如释放它创建的我们脚本的副本)。

在 `Close` 返回后,我们就可以调用引擎 `IActiveScriptParse` 和 `IActiveScript` 的 `Release` 函数,如下所示:

activeScript->lpVtbl->Release(activeScript);
activeScript->lpVtbl->Release(activeScript);

现在我们已经完成了引擎的操作。

加载脚本

当然,如果最终用户不能编写自己的脚本来运行,我们的脚本语言对他们来说就没什么用了。因此,我们不将 VBScript 硬编码到我们的可执行文件中,而是向最终用户提供一个文件对话框,以便他可以在磁盘上选择一个 VBScript。然后,我们将脚本加载到内存缓冲区中,确保脚本采用宽字符格式并以空字符结尾,并将该内存缓冲区传递给 `ParseScriptText`。

我不会讨论如何向最终用户提供文件对话框以获取他选择的文件名。

用户选择文件名后,我们会将其传递给名为 `loadUnicodeScript` 的函数,该函数返回一个包含脚本的内存缓冲区,该缓冲区采用宽字符格式并以空字符结尾。

OLECHAR * loadUnicodeScript(LPCTSTR fn)
{
   OLECHAR  *script;
   HANDLE   hfile;

   // Assume an error
   script = 0;

   // Open the file
   if ((hfile = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, 0, 
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)) != INVALID_HANDLE_VALUE)
   {
      DWORD  filesize;
      char   *psz;

      // Get a buffer to read in the file, with room to nul-terminate
      filesize = GetFileSize(hfile, 0);
      if ((psz = (char *)GlobalAlloc(GMEM_FIXED, filesize + 1)))
      {
         DWORD  read;

         // Read in the file
         ReadFile(hfile, psz, filesize, &read, 0);

         // Get a buffer to convert to UNICODE, plus an extra wchar_t
         // to nul-terminate it
         if ((script = (OLECHAR *)GlobalAlloc(GMEM_FIXED, (filesize + 1)
             * sizeof(OLECHAR))))
         {
            // Convert to UNICODE and nul-terminate
            MultiByteToWideChar(CP_ACP, 0, psz, filesize, script, filesize + 1);
            script[filesize] = 0;
         }
         else
            display_sys_error(0);

         GlobalFree(psz);
      }
      else
          display_sys_error(0);

      CloseHandle(hfile);
   }
   else
      display_sys_error(0);

   return(script);
}

void display_sys_error(DWORD err)
{
   TCHAR   buffer[160];

   if (!err) err = GetLastError();
   buffer[0] = 0;
   FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, err,
      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &buffer[0], 160, 0);
   MessageBox(0, &buffer[0], "Error", MB_OK);
}

注意:`loadUnicodeScript` 假设磁盘上的文件不是 Unicode。如果您有可能加载已经是 Unicode 格式的磁盘文件,那么您不应该再次转换它。在这种情况下,`loadUnicodeScript` 需要修改以检查文件中的任何“签名”。有关不同文本文件编码的更多信息,请参阅其他文档。

我们可以对代码进行一些小的更改来运行脚本。我们只需调用 `loadUnicodeScript` 将磁盘脚本加载到内存缓冲区中,然后将此缓冲区传递给 `ParseScriptText`。之后,我们可以释放缓冲区,并将引擎的状态更改为“已连接”,以运行脚本:

LPOLESTR   str;

// Load the script from disk.
str = loadUnicodeScript(fn);

// Have the script engine parse it and internally prepare it to be run.
hr = activeScriptParse->lpVtbl->ParseScriptText(activeScriptParse, str,
    0, 0, 0, 0, 0, 0, 0, 0);

// We no longer need the loaded script.
GlobalFree(str);

// Execute the script's immediate instructions.
activeScript->lpVtbl->SetScriptState(activeScript, SCRIPTSTATE_CONNECTED);

枚举已安装的引擎

当用户选择要运行的脚本时,我们不希望假设它将是一个 VBScript。也许它会是一个 Jscript,或者一个与 Python 引擎关联的脚本,等等。

我们将做的是获取用户选择的文件名,隔离文件扩展名,并将该扩展名传递给 `getEngineGuid`。这将为我们提供需要打开的相应引擎的 GUID。

但是,如果文件名没有扩展名,或者它的扩展名与任何已安装的脚本引擎都没有关联,会发生什么呢?在这种情况下,我们需要向最终用户提供一个已安装的 ActiveX 脚本引擎列表,并让他手动选择他想要的引擎。然后,我们将获取所选引擎的 GUID,并打开它。

微软的 OLE 函数提供了我们可以获取和使用的 COM 对象,以获取已安装引擎及其 GUID 的列表。我们需要获取的 COM 对象是 `ICatInformation`,特别是,我们想要列出脚本引擎的 `ICatInformation` 对象。我们可以通过调用 `CoCreateInstance` 来获取其中一个对象。然后我们可以调用它的 `EnumClassesOfCategories` 来获取一个子对象,其 `Next` 函数会枚举脚本引擎的 GUID。我们还可以调用 `ProgIDFromCLSID` 来获取每个引擎的名称(由引擎的安装程序注册)。

在 `ScriptHost2` 目录中是一个带有“运行脚本”菜单项的窗口 (GUI) C 应用程序示例。当用户选择此菜单项时,会弹出一个文件对话框以获取要运行的脚本的名称。选择脚本名称后,应用程序会隔离扩展名,并尝试查找与此扩展名关联的引擎 GUID。如果找不到此类引擎,则应用程序会显示一个列出已安装引擎的对话框,以便用户可以选择要使用的所需引擎。

源文件 `ChooseEngine.c` 包含显示已安装引擎列表并获取所选引擎 GUID 的代码。

另一个线程中运行脚本

我们的 GUI 应用程序有一个问题。脚本在与用户界面相同的线程中运行。这样做的不利之处在于,如果脚本执行某个永无止境的循环,我们将永远困在对 `SetScriptState` 的调用中,用户无法中止脚本。事实上,在脚本运行时,用户界面完全被占用,因此用户甚至无法移动我们的应用程序窗口。

因此,最好启动一个单独的线程来运行脚本。但有一个很大的注意事项。大多数引擎的 COM 函数只能由调用 `SetScriptSite` 的线程调用。因此,我们需要让我们的“脚本线程”完成运行脚本所需的大部分设置/初始化和清理工作。另一个注意事项是,我们的 `IActiveScriptSite` 函数将在我们的脚本线程中调用,因此如果我们的 `IActiveScriptSite` 函数和 UI 线程函数都访问任何数据,我们将需要某种同步,例如在对该数据的任何访问周围使用临界区。

在 `ScriptHost3` 目录中是 **ScriptHost2** 的修改版本,它在辅助线程中运行脚本。本质上,我们所做的就是将我们的函数 `runScript` 变成了第二个线程的入口点。不需要太多的改动,因为 `runScript` 已经完成了脚本线程需要完成的所有初始化和清理工作。大部分改动涉及线程初始化和清理。首先,Windows 操作系统规定线程只能传递一个参数(由我们自己选择)。但我们的 `runScript` 接受两个参数——一个文件名和一个 GUID。我们需要定义一个新的单一结构来封装这两个参数。我们称之为 `MYARGS` 结构,并定义如下:

typedef struct {
   IActiveScript  *EngineActiveScript;
   HANDLE         ThreadHandle;
   TCHAR          Filename[MAX_PATH];
   GUID           Guid;
} MYARGS;

然后,我们将 `runScript` 传递一个指向我们的 `MYARGS` 的指针。

`MYARGS` 有两个额外的成员。`ThreadHandle` 存储脚本线程的句柄。我们还将让脚本线程将引擎的 `IActiveScript` 对象指针存储在我们的 `MYARGS` 中。这是为了让主线程以后也能访问它。

由于我们一次只启动一个脚本,我们将声明一个全局 `MYARGS`:

MYARGS MyArgs;

我们的主线程在应用程序启动时将其 `ThreadHandle` 成员初始化为 0。我们使用此成员来确定脚本线程是否正在运行。当 `ThreadHandle` 为 0 时,脚本线程未运行。当不为 0 时,它就是正在运行的脚本线程的句柄。

`runScript` 需要在线程启动后调用 `CoInitialize`。每个线程都负责为自己初始化 COM。当然,`runScript` 必须在完成后调用 `CoUninitialize`。此外,我们将把主线程对 `CoInitialize` 的调用更改为 `CoInitializeEx` 并传递值 `COINIT_MULTITHREADED`。这确保了如果我们的主线程调用任何 `IActiveScript` 函数,那么引擎不会阻塞我们的主线程并强制函数在我们的脚本线程中执行。当我们希望主线程通过 `InterruptScriptThread` 中止脚本线程时,这非常重要。我们不想信任脚本线程自行中止,如果它“挂起”了,它就无法做到这一点。

注意:为了让编译器识别 `CoInitializeEx`,您必须将符号 `_WIN32_WINNT` 定义为 0x0400(或更大),并且必须在您 `objbase.h` 之前完成。

当我们的主(UI)线程处理 `IDM_FILE_RUNSCRIPT` 消息时,它会用要运行的脚本的名称和要使用的引擎的 GUID 填充 `MYARG` 的文件名和 GUID 字段。然后,我们的主线程通过调用 `CreateThread` 创建/启动脚本线程,并传递我们的 `MYARGS`,如下所示:

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

注意:如果您的脚本线程或 `IActiveScriptSite` 函数调用任何 C 语言函数,则请改用 `beginthread`。并检查您的 C/C++“代码生成”设置,确保您使用的是多线程 C 库。在我的示例代码中,我没有调用任何对多线程敏感的 C 库函数,因此我可以使用 `CreateThread`。

请注意,我们将线程的句柄保存在 `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` 中,我们将以下错误处理从...

if ((hr = activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
   display_COM_error("Can't initialize engine : %08X", hr);

...改为...

if ((hr = activeScriptParse->lpVtbl->InitNew(activeScriptParse)))
   PostMessage(MainWindow, WM_APP, (WPARAM)"Can't initialize engine : %08X", hr);

我们需要稍微修改 `loadUnicodeScript`,使其不调用 `display_sys_error`,而是调用 `PostMessage` 将错误消息显示传递给主线程。

我们的脚本线程还有一个地方可能调用 `MessageBox`,那就是我们的 `IActiveScriptSite` 的 `OnScriptError`。让我们重写它,使其 `GlobalAlloc()` 错误消息,然后 `PostMessage()` 到主线程显示。您可以查看 `IActiveScriptSite.c` 中更新后的代码。

我们需要在主窗口过程添加代码来处理 `WM_APP`,如下所示:

case WM_APP:
{
   // Our script thread posts a WM_APP if it needs
   // us to display an error message.
   // wParam = A pointer to the string
   // to display. If 0, then lParam is an error
   // number to be passed to display_sys_error().
   // lParam = The HRESULT. If 0, then wParam
   //          is an allocated WCHAR string which we must
   // free with GlobalFree().
   if (!wParam)
      display_sys_error((DWORD)lParam);
   else if (!lParam)
   {
      MessageBoxW(hwnd, (const WCHAR *)wParam, 
                  "Error", MB_OK|MB_ICONEXCLAMATION);
      GlobalFree((void *)wParam);
   }
   else
      display_COM_error((LPCTSTR)wParam, (HRESULT)lParam);
   return(0);
}

注意:您可能希望使用 `RegisterWindowMessage` 来获取自己的自定义消息编号,而不是使用 `WM_APP`。但就我们的目的而言,`WM_APP` 足够了。

只剩下最后一点——如何在主线程中中止脚本。让我们在 `WM_CLOSE` 处理中完成此操作,因此如果用户在脚本运行时尝试关闭我们的窗口,我们将强制脚本中止。引擎 `IActiveScript` 的 `InterruptScriptThread` 函数是少数可以由任何线程调用的函数之一。我们传递值 `SCRIPTTHREADID_ALL`,这仅仅意味着中止我们已提供给引擎的所有正在运行的脚本(即,如果我们创建了许多线程,每个线程同时运行自己的 VBScript,这将导致 VB 引擎中止所有这些脚本线程)。或者,如果我们要中止特定脚本线程,我们可以传递该线程的 ID。

case WM_CLOSE:
{
   // Is a script running?
   if (MyArgs.ThreadHandle)
   {
      // Abort the script by calling InterruptScriptThread.
      MyArgs.EngineActiveScript->lpVtbl->InterruptScriptThread(
          MyArgs.EngineActiveScript, SCRIPTTHREADID_ALL, 0, 0);
   ...

当 `InterruptScriptThread` 返回时,这并不意味着线程已终止。它仅仅意味着引擎已将正在运行的脚本标记为终止。我们仍然需要“等待”线程终止。我们将通过测试 `ThreadHandle` 何时为 0 来做到这一点。(请记住,脚本线程在终止时将其归零。)但还有另一个问题。如果脚本线程以某种方式“休眠”或正在等待某事,例如在调用 `MessageBox` 中,那么引擎将永远没有机会终止它。我们一直小心避免自己调用此类函数,但请注意 VBScript 也有一个 `msgbox` 函数可以调用。

为了解决这个问题,我们将增加一个计数,并在每次递增之间 `Sleep()`。当计数“超时”时,我们假设脚本被锁定,然后通过调用 `TerminateThread` 强行终止它。

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

总而言之,脚本应该在与主 UI 分离的线程中运行。脚本线程必须自行 `CoInitialize`。大多数引擎的 COM 函数只能从脚本线程调用。我们的 `IActiveScriptSite` 的函数也在脚本线程中调用。脚本线程应避免做任何使其“等待”或“暂停”的事情。UI 线程可以通过 `InterruptScriptThread` 强制脚本中止,但如果需要,可能还需要进行“超时”以强行终止脚本线程。

结论

本章演示了如何使用 ActiveX 脚本引擎运行脚本。但是,虽然能够简单地运行脚本很有用,但我们尚未看到脚本如何直接与应用程序中的函数交互并交换数据。为此,我们需要向应用程序添加另一个 COM 对象。这将是下一章的重点。

© . All rights reserved.