Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / Win32

C++, Win32 and Scripting: Quick way to add Scripting support to your applications

Rate me:
Please Sign up or sign in to vote.
4.88/5 (32 votes)
28 Mar 2012CPOL6 min read 81.4K   1.8K   108   35
Use COM and plain C++ to add scripting support automatically.

 

Introduction 

This article aims to help the experienced C++ and Win32 programmer to create scripting facilities for their application. The purpose is to allow Windows to use any available scripting engine (such as JavaScript, PHP, Perl, LUA, VBScript etc) to be used inside an application without the need to manually parse the script. The programmer only defines the available functions and variables, and Windows accesses and calls them through the ActiveScript mechanism we will be discussing here.

I 've seen some articles about scripting around, but most of them seem to complex to me. Here is an easy one without messing with too many COM details. 

 

Background 

Very good C++ knowledge, Win32 programming and COM basics are required.  

For the sake of simplicity, I 've omitted error checking from the code below. Of course, your code must include proper error checking to make sure that you cleanup properly in case of an error.

 

Theory 

In the past, if one wanted to add scripting to their application, they had to manually implement a parser etc. This was totally time-consuming and error prone. Windows allows us to just define a set of functions or variables, which can be accessed from any available scripting language in the system.

 

Implementing the IDispatch Interface for our Scripting 

We provide our scripting capabilities (functions and data) through an IDispatch Interface. The Scripting engine will call our functions (or request/set our data) through IDispatch::Invoke. To make our task simpler, we will first define our classes in a form of an IDL file:

 

// 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);
		};


	}

What do we have here? I just defined 2 classes, FOO_Scripting_1 and FOO_Scripting_2. FOO_Scripting_1 has two member functions (foo and message1), and a data member (in a form of a function, Interface2) which will allow us to get through it a FOO_Scripting_2. FOO_Scripting_2 has a member function as well (message2). 

When we compile this IDL file with MIDL, we get a TLB file as a result. 

Our interfaces are dual.They provide the IDispatch, which allows the scripting engine to access our stuff, but also provide the functions in a vtable, so they can be called with the helper function DispInvoke, below: 

 

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;
		}


	};

 

This is where the type library creation pays off. We have to implement GetIDSOfNames() so the Scripting Library associates each element of our class with a unique ID. The helper function DispGetIDsOfNames automatically reads our type information (from the ITypeInfo we got from the ITypeLib) and takes care of the dirty work for us. 

We also have to implement Invoke(). Normally, we would read the dispid, parse the arguments inside the DISPPARAMS* arg, return error in case of type mismatch, call the appropriate function, depending on the DISPID and, finally, return any value. This would be something to do if a) we hadn't create a type library and/or b) we hadn't specified a dual interface. Now, the DispInvoke helper function automatically saves us from all this work. It reads our type library, automatically calling the proper functions, checking the types from mismatches and returning the proper value. For this to work, it must be ensured that:

  • All the functions in the class appear in the same order as in the IDL file. Try swapping foo() with message1() in the class and see what happens. 
  • All the functions must follow the _stdcall calling convention. 
  • All the functions must accept and return specific types (the types available in the VARIANT union).
  • If necessary, you can cast a void* for example, to your own class. However this pointer cannot be marshaled, so you will have a problem when using the scripting mechanism outside of the process (besides, you don't actually know if the Javascript parser, for example, is actually an attached DLL or an outside process). If you want to use your own types, you can use an IUnknown interface which provides an IMarshal, so the interface pointers are marshaled correctly. This can be dirty, so the best thing you can do is to serialize your objects into a BSTR and reconstruct them using that BSTR (which will be automatically and correctly marshaled by COM) later.  
  • If a function will return a string, it must return a BSTR allocated with SysAllocString. The caller will free this string with SysFreeString.  
  • To return data, one does not use <return type> function(). For example, if you have an int and you want it to be accessible as a "property get" with the name of abc, the proper function is not the int __stdcall abc() { return x; }, but the function HRESULT __stdcall abc(INT* pInt); For example, see my above Interface2 function (which actually wraps as a function the "property get" of the class we want to return). 

 

Telling Windows we can host scripts 

The first step is to implement an IActiveScriptSite in our application. This interface tells Windows we can host scripting execution. This interface is trivial, containing: 

 

  • The IUnknown members. 
  • GetLCID() to specify the locale used by the engine.  
  • GetItemInfo(). In this function we provide the IUnknown (requested when the DWORD req parameter has the value SCRIPTINFO_IUNKNOWN) of our main scripting interface, in this code the FOO_Scripting_1. Windows will query what we pass for an IDispatch and then call our stuff. 
  • GetDocVersionString(), to retrieve a host-defined string that uniquely identifies the current document version from the host's point of view. 
  • OnScriptTerminate(), to get notified when the script has terminated. 
  • OnStateChange(), to get notified when the scripting engine changes the state.
  • OnScriptError(), to get notified if an error occured. 
  • OnEnterScript(), OnLeaveScript(), to get notified on begin/end of execution of the script code. 

 

The second step is to implement an IActiveScriptSiteWindow, which tells Windows about the window that will host the scripting engine (should any popups occur). It contains:

  • The IUnknown members. 
  • GetWindow() sets the owner window for the scripting engine. 
  • EnableModeless() to enable or disable modeless function of any dialog boxes displayed by the engine.  

We make this interface visible through our implementation of QueryInterface in the previous IActiveScriptSite.

 

The third step is to request an IActiveScript from Windows:

    GUID guid;
CLSIDFromProgID(L"Javascript",&guid);
IActiveScript* AS = 0;
HRESULT hr = CoCreateInstance(guid, 0, CLSCTX_ALL,__uuidof(IActiveScript),(void **)&AS; 

 

The GUID that we will pass to CoCreateInstance is the guid of the language we want to use. Passing "Javascript" for example, allows the CLSIDFromProgID function to get from the registry the CLSID value of Javascript and create an IActiveScript for it. You can use Javascript and VBScript to create an IActiveScript for the two scripting languages that are installed by default. To get a list of all the languages installed, you can use the following function:

 

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();
		}
	} 

The above function uses ICatInformation to enumerate all available scripting engines. All major scripting languages (PHP, Perl, LUA, Python etc) for Windows include a DLL that will provide an IActiveScript interface for those. So, if you have installed PHP for example, you can use normal PHP code that can call your functions, without the need for a parser. Isn't that wonderful?! 

The fourth step is to add our script site to the IActiveScript we got from Windows:

 

MyScriptHost TPSH;
AS->SetScriptSite(&TPSH);

and to add a root namespace, so our code will not be mixed with other functions. For example: 

AS->AddNamedItem(L"Festival",SCRIPTITEM_ISVISIBLE);

and now we can access our code through the namespace Festival, for example Festival.foo(), Festival.Interface2.message2();

 

The final step is to actually execute the script: 

// 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();

 

Optionally, we can define an ICanHandleException to handle script errors. If we do not, our OnScriptError is called. 

 

The Code 

The code demonstrates a simple application that defines 2 classes and is able to call our code through a simple Javascript script. Feel free to try with various other engines.

Have fun! 

 

History 

  • 18 - 3 - 2012 : First Release 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Greece Greece
I'm working in C++, PHP , Java, Windows, iOS, Android and Web (HTML/Javascript/CSS).

I 've a PhD in Digital Signal Processing and Artificial Intelligence and I specialize in Pro Audio and AI applications.

My home page: https://www.turbo-play.com

Comments and Discussions

 
QuestionPassing variables from JavaScript back to the main C++ code Pin
sankartadi30-Mar-20 7:13
sankartadi30-Mar-20 7:13 
GeneralMy vote of 5 Pin
Kyle_Wang12-Aug-18 21:02
Kyle_Wang12-Aug-18 21:02 
GeneralRe: My vote of 5 Pin
Michael Chourdakis26-Nov-18 8:54
mvaMichael Chourdakis26-Nov-18 8:54 
QuestionVBScript MsgBox Fails Pin
Member 1089651221-Jun-14 5:58
Member 1089651221-Jun-14 5:58 
AnswerRe: VBScript MsgBox Fails Pin
Michael Chourdakis21-Jun-14 12:04
mvaMichael Chourdakis21-Jun-14 12:04 
GeneralRe: VBScript MsgBox Fails Pin
Member 1089651221-Jun-14 15:26
Member 1089651221-Jun-14 15:26 
QuestionVBScript MsgBox fails Pin
Member 1089651221-Jun-14 4:06
Member 1089651221-Jun-14 4:06 
GeneralMy vote of 5 Pin
Eytukan13-May-12 3:41
Eytukan13-May-12 3:41 
QuestionYou get 5 Pin
Christopher Melen28-Apr-12 12:56
Christopher Melen28-Apr-12 12:56 
AnswerRe: You get 5 Pin
Michael Chourdakis28-Apr-12 20:06
mvaMichael Chourdakis28-Apr-12 20:06 
AnswerRe: You get 5 Pin
Michael Chourdakis29-Apr-12 3:50
mvaMichael Chourdakis29-Apr-12 3:50 
GeneralRe: You get 5 Pin
Christopher Melen29-Apr-12 6:41
Christopher Melen29-Apr-12 6:41 
GeneralRe: You get 5 Pin
Michael Chourdakis4-May-12 9:50
mvaMichael Chourdakis4-May-12 9:50 
GeneralRe: You get 5 Pin
Christopher Melen5-May-12 8:03
Christopher Melen5-May-12 8:03 
QuestionYou have got a five Pin
Roberto Guerzoni2-Apr-12 20:45
professionalRoberto Guerzoni2-Apr-12 20:45 
AnswerRe: You have got a five Pin
Michael Chourdakis2-Apr-12 20:55
mvaMichael Chourdakis2-Apr-12 20:55 
GeneralMy vote of 5 Pin
brianma2-Apr-12 11:21
professionalbrianma2-Apr-12 11:21 
GeneralMy vote of 5 Pin
frwa31-Mar-12 4:03
frwa31-Mar-12 4:03 
GeneralMy vote of 4 Pin
wtwhite20-Mar-12 18:49
wtwhite20-Mar-12 18:49 
GeneralMy vote of 5 Pin
kanalbrummer19-Mar-12 20:07
kanalbrummer19-Mar-12 20:07 
GeneralMy vote of 3 Pin
xComaWhitex19-Mar-12 9:27
xComaWhitex19-Mar-12 9:27 
GeneralRe: My vote of 3 Pin
wtwhite20-Mar-12 18:47
wtwhite20-Mar-12 18:47 
GeneralRe: My vote of 3 Pin
xComaWhitex20-Mar-12 19:53
xComaWhitex20-Mar-12 19:53 
GeneralRe: My vote of 3 Pin
wtwhite21-Mar-12 2:10
wtwhite21-Mar-12 2:10 
GeneralRe: My vote of 3 PinPopular
Michael Chourdakis21-Mar-12 3:36
mvaMichael Chourdakis21-Mar-12 3:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.