C++、Win32 和脚本:为您的应用程序添加脚本支持的快速方法






4.88/5 (31投票s)
使用 COM 和纯 C++ 自动添加脚本支持。
介绍
本文旨在帮助经验丰富的 C++ 和 Win32 程序员为其应用程序创建脚本功能。目的是允许 Windows 使用任何可用的脚本引擎(如 JavaScript、PHP、Perl、LUA、VBScript 等)在应用程序内部使用,而无需手动解析脚本。程序员只需定义可用的函数和变量,Windows 将通过我们在此讨论的 ActiveScript 机制来访问和调用它们。
我见过一些关于脚本的文章,但对我来说,大多数都太复杂了。这里有一个简单的,无需处理太多的 COM 细节。
背景
需要非常好的 C++ 知识、Win32 编程和 COM 基础。
为简单起见,我在下面的代码中省略了错误检查。当然,您的代码必须包含适当的错误检查,以确保在发生错误时能正确清理。
理论
过去,如果有人想为他们的应用程序添加脚本,他们不得不手动实现解析器等。这完全耗时且容易出错。Windows 允许我们只定义一组函数或变量,这些函数或变量可以从系统中的**任何可用**脚本语言访问。
为我们的脚本实现 IDispatch 接口
我们通过 IDispatch 接口提供我们的脚本功能(函数和数据)。脚本引擎将通过 IDispatch::Invoke 调用我们的函数(或请求/设置我们的数据)。为了简化我们的任务,我们将首先以 IDL 文件的形式定义我们的类
// Main.idl [ uuid(DA28ED8E-7AC5-42a0-9C2F-A545171F2259), version(1.0), helpstring("Foo Automation Interfaces") ] library FOO_1_0 { importlib("stdole2.tlb"); // Forward declare all types defined in this typelib interface FOO_Scripting_1; interface FOO_Scripting_2; // FOO_Scripting_1 [ uuid(DA28ED8E-7AC5-42a1-9C2F-A545171F2259),odl,dual,oleautomation ] interface FOO_Scripting_1 : IDispatch { // Functions [id(2001)] HRESULT __stdcall foo(); [id(2002)] HRESULT __stdcall message1(BSTR Path); // Properties [id(101), propget] HRESULT Interface2([out,retval] FOO_Scripting_2** pFoo2); }; // FOO_Scripting_2 [ uuid(DA28ED8E-7AC5-42a2-9C2F-A545171F2259),odl,dual,oleautomation ] interface FOO_Scripting_2 : IDispatch { // Functions [id(2003)] HRESULT __stdcall message2(BSTR Path); }; }
这里有什么?我只定义了 2 个类,FOO_Scripting_1 和 FOO_Scripting_2。FOO_Scripting_1 有两个成员函数(**foo** 和 **message1**),以及一个数据成员(以函数形式,**Interface2**),它允许我们通过它获取一个 FOO_Scripting_2。FOO_Scripting_2 也有一个成员函数(**message2**)。
当我们用 MIDL 编译这个 IDL 文件时,我们会得到一个 TLB 文件作为结果。
我们的接口是**双重**的。它们提供 **IDispatch**,允许脚本引擎访问我们的东西,同时也通过 vtable 提供函数,因此它们可以与下面的帮助函数 **DispInvoke** 一起调用:
struct FOO_Scripting_1 : public IDispatch { int refNum; ITypeInfo* ti; // IUnknown DWORD WINAPI AddRef(){return ++refNum;} DWORD WINAPI Release(){--refNum; return refNum;} long WINAPI QueryInterface( REFIID riid,void ** object) { *object = IsEqualIID(riid, IID_IDispatch) ? this:0; return *object? 0 : E_NOINTERFACE; } FOO_Scripting_1() { refNum = 1; ITypeLib* tlb = 0; LoadTypeLib(L"main.tlb",&tlb); tlb->GetTypeInfo(0,&ti); // Get the first type info (index 0) from the TLB file // Make sure to verify that tlb,ti are actually valid. Make sure to release them later. } // IDispatch long WINAPI GetTypeInfoCount(UINT* cc) { if (!cc) return E_POINTER; *cc = 1; return S_OK; } HRESULT WINAPI GetTypeInfo( UINT a, LCID li, ITypeInfo ** ptt) { if (a != 0) return DISP_E_BADINDEX; if (!ptt) return E_POINTER; if (!ti) return E_FAIL; *ptt = ti; ti->AddRef(); return S_OK; } long WINAPI GetIDsOfNames(REFIID riid,WCHAR** name,UINT cnt,LCID lcid,DISPID *id) { if (!ti) return E_FAIL; HRESULT hr = DispGetIDsOfNames(ti,name,cnt,id); return hr; } HRESULT WINAPI Invoke( DISPID id, REFIID riid, LCID lcid, WORD flags, DISPPARAMS *arg, VARIANT *ret, EXCEPINFO *excp, UINT *err) { HRESULT hr = DispInvoke(this,ti,id,flags,arg,ret,excp,err); return hr; } // Functions virtual HRESULT __stdcall foo() { return S_OK; } virtual HRESULT __stdcall message1(BSTR msg) { MessageBox(0,msg,L"FOO 1",MB_OK); return S_OK; } // Properties virtual HRESULT __stdcall Interface2(FOO_Scripting_2** pFoo2) { if (!pFoo2) return E_POINTER; // Assuming we have from somewhere a FOO_Scripting_2& f2; f2.AddRef(); *pFoo2 = &f2; return S_OK; } };
这就是类型库创建发挥作用的地方。我们必须实现 **GetIDSOfNames**(),以便脚本库将我们类的每个元素与一个唯一的 ID 相关联。帮助函数 DispGetIDsOfNames 自动读取我们的类型信息(来自我们从 **ITypeLib** 获取的 **ITypeInfo **),并为我们处理繁琐的工作。
我们还必须实现 Invoke()。通常,我们会读取 dispid,解析 DISPPARAMS* arg 中的参数,在类型不匹配时返回错误,然后根据 DISPID 调用适当的函数,最后返回任何值。如果 a) 我们没有创建类型库和/或 b) 我们没有指定双重接口,那么就会是这样。现在,DispInvoke 帮助函数自动为我们省去了所有这些工作。它读取我们的类型库,自动调用适当的函数,检查类型不匹配,并返回适当的值。为此,必须确保:
- 类中的所有函数都与 IDL 文件中的顺序相同。尝试在类中将 **foo**() 与 **message1**() 交换,看看会发生什么。
- 所有函数都必须遵循 **_stdcall** 调用约定。
- 所有函数都必须接受和返回特定的类型(**VARIANT** 联合中可用的类型)。
- 如果需要,您可以将 void* 例如强制转换为您自己的类。但是,这个指针不能被封送,所以当您在进程外使用脚本机制时会出现问题(此外,您实际上不知道 JavaScript 解析器,例如,是附加的 DLL 还是外部进程)。如果您想使用自己的类型,可以使用提供 IMarshal 的 IUnknown 接口,这样接口指针就能被正确封送。这可能会很麻烦,所以最好的方法是将您的对象序列化为 BSTR,并在之后使用该 BSTR(它将由 COM 自动正确封送)重新构造它们。
- 如果一个函数将返回一个字符串,它必须返回一个用 **SysAllocString** 分配的 **BSTR**。调用者将使用 SysFreeString 释放该字符串。
- 要返回数据,不能使用
**function**()。例如,如果您有一个 **int** 并且想将其作为名为 **abc** 的“属性 get”访问,正确的函数不是 **int __stdcall abc() { return x; },** 而是函数 **HRESULT __stdcall abc(INT* pInt);**。例如,请参见我上面的 **Interface2** 函数(它实际上将我们想要返回的类的“属性 get”封装为一个函数)。
告知 Windows 我们可以托管脚本
第一步是在我们的应用程序中实现一个 **IActiveScriptSite**。这个接口告诉 Windows 我们可以托管脚本执行。这个接口很简单,包含:
- IUnknown 成员。
- **GetLCID()** 指定引擎使用的区域设置。
- **GetItemInfo()**。在此函数中,我们提供我们主脚本接口的 IUnknown(当 req 参数的值为 SCRIPTINFO_IUNKNOWN 时请求)。在此代码中是 **FOO_Scripting_1**。Windows 将查询我们传递的内容是否为 IDispatch,然后调用我们的东西。
- **GetDocVersionString()**,用于从宿主的角度检索唯一标识当前文档版本的宿主定义的字符串。
- **OnScriptTerminate()**,用于在脚本终止时收到通知。
- **OnStateChange()**,用于在脚本引擎更改状态时收到通知。
- **OnScriptError()**,用于在发生错误时收到通知。
- **OnEnterScript()、OnLeaveScript()**,用于在脚本代码开始/结束执行时收到通知。
第二步是实现一个 **IActiveScriptSiteWindow**,它向 Windows 报告将托管脚本引擎的窗口(如果发生任何弹出窗口)。它包含
- IUnknown 成员。
- **GetWindow()** 设置脚本引擎的所有者窗口。
- **EnableModeless()** 用于启用或禁用引擎显示的任何对话框的无模式功能。
我们通过实现前面 **IActiveScriptSite** 中的 **QueryInterface** 使此接口可见。
第三步是向 Windows 请求一个 **IActiveScript**
GUID guid; CLSIDFromProgID(L"Javascript",&guid); IActiveScript* AS = 0; HRESULT hr = CoCreateInstance(guid, 0, CLSCTX_ALL,__uuidof(IActiveScript),(void **)&AS;
我们将传递给 **CoCreateInstance** 的 GUID 是我们想要使用的语言的 guid。例如,传递“**Javascript**”允许 **CLSIDFromProgID** 函数从注册表中获取 JavaScript 的 CLSID 值并为其创建一个 IActiveScript。您可以使用 **Javascript** 和 **VBScript** 来创建 **IActiveScript**,用于默认安装的两种脚本语言。要获取已安装语言的列表,您可以使用以下函数
void GetScriptEngines(vector<wstring>& vv) { // get the component category manager for this machine ICatInformation *pci = 0; HRESULT hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, 0, CLSCTX_SERVER, IID_ICatInformation, (void**)&pci); if (SUCCEEDED(hr)) { // get the list of parseable script engines CATID rgcatidImpl[1]; rgcatidImpl[0] = CATID_ActiveScriptParse; IEnumCLSID *pec = 0; hr = pci->EnumClassesOfCategories(1, rgcatidImpl, 0, 0, &pec); if (SUCCEEDED(hr)) { // print the list of CLSIDs to the console as ProgIDs enum {CHUNKSIZE = 16}; CLSID rgclsid[CHUNKSIZE]; ULONG cActual; do { hr = pec->Next(CHUNKSIZE, rgclsid, &cActual); if (FAILED(hr)) break; if (hr == S_OK) cActual = CHUNKSIZE; for (ULONG i = 0; i < cActual; i++) { OLECHAR *pwszProgID = 0; if (SUCCEEDED(ProgIDFromCLSID(rgclsid[i], &pwszProgID))) { wstring X = pwszProgID; vv.push_back(X); CoTaskMemFree(pwszProgID); } } } while (hr != S_FALSE); pec->Release(); } pci->Release(); } }
上述函数使用 **ICatInformation ** 枚举所有可用的脚本引擎。Windows 下的所有主要脚本语言(PHP、Perl、LUA、Python 等)都包含一个 DLL,它将为这些语言提供 IActiveScript 接口。所以,如果您安装了 PHP,例如,您可以使用普通的 PHP 代码来调用您的函数,而无需解析器。这不是很棒吗?!
第四步是将我们的脚本站点添加到从 Windows 获取的 **IActiveScript** 中
MyScriptHost TPSH; AS->SetScriptSite(&TPSH);
并添加一个根命名空间,这样我们的代码就不会与其他函数混淆。例如:
AS->AddNamedItem(L"Festival",SCRIPTITEM_ISVISIBLE);
现在我们可以通过命名空间 **Festival** 访问我们的代码,例如 **Festival**.**foo**(),**Festival**.**Interface2**.message2();
最后一步是实际执行脚本:
// Execute Script const wchar_t* s1 = L"\r\n\ var d1 = \"First Message\";\r\n\ var d2 = \"Second Message\";\r\n\ Festival.message1(d1);\r\n\ Festival.Interface2.message2(d2);\r\n\ Festival.Interface3.message2(d2); // Invalid, no such Interface3. Error should be generated.\r\n\ "; IActiveScriptParse* parse = 0; AS->QueryInterface(__uuidof(IActiveScriptParse),(void**)&parse); if (parse) { hr = parse->InitNew(); hr = parse->ParseScriptText(s1,0,0,0,0,0,0,0,0); } SCRIPTSTATE ssp = SCRIPTSTATE_CONNECTED; hr = AS->SetScriptState(ssp); hr = AS->Close(); if (parse) parse->Release();
可选地,我们可以定义一个 **ICanHandleException ** 来处理脚本错误。如果我们不这样做,就会调用我们的 **OnScriptError**。
代码
该代码演示了一个简单的应用程序,它定义了 2 个类,并且能够通过简单的 JavaScript 脚本调用我们的代码。随意尝试使用各种其他引擎。
玩得开心!
历史
- 2012-03-18:首次发布