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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (31投票s)

2012年3月18日

CPOL

6分钟阅读

viewsIcon

86745

downloadIcon

1851

使用 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:首次发布 

© . All rights reserved.