Implementing Active Script Site with ATL

The purpose of this article is not to explain benefits of Active Scripting technology. There are other articles on this subject. Our purpose is to explain the fastest and the easiest way of implementing Active Script Site - i.e. the fastest way of extending your application with internal scripting functionality. If you are familiar with Active Scripting technology, then you should skip the following section and proceed to "Implementation Plan". If you don't have any background on Active Scripting technology, the following section will help you at the beginning, but you really should read more before you decide to use the technology.

 In either case, this article assumes that you are familiar with COM and words like Interface, Dispatch and CoClass make sense to you.

Short Introduction to Active Scripting

So, briefly, what are we talking about here?

Imagine that your application needs to have a highly customizable MMI, the one where positions, captions and styles of different controls must vary from user to user and may change during the execution of the program.

Imagine that your program has to process data types of some kind using the same "tools" but in different combinations - only users will define those.

In both cases you (the developer) know what to do - write the functions that alter control styles/process data types. The big question is: how will your user interact with those functions?

The simplest solution is to create parameters file. If saving some parameters and restoring them next time the application opens can solve your customization problems, you really don't need anything more sophisticated. But what if your customization is based on some kind of logic? For instance: if button X was pressed and the application is in state Y, do something. Or when processing data types: if value of field X is Y, then change value of field Z to… Those two samples sound familiar, don't they? They sound exactly like the standard "if" in programming languages. The most intuitive way to allow your users to build such expressions is to let them use a simple programming language of some kind. Let's take a look at out two samples written in VB-Like language:


Sub X_OnClick()
	If  StateMachine.State = Y then
		Processor.DoProcessing()
	Else
		Processor.DoOtherProcessing()
	End If
End Sub

Or

If  Item.X = Y then
	Item.TypeStr = "It's a Y"
Else
	Item.TypeStr = "It's not a Y"
End If

The code looks simple and clear, doesn't it? Then it's settled, you need scripting in your application. Building an interpreter from scratch is not a simple thing to do. It is possible, of course, but it will take you quite some time, and I am sure, that you'll decide to implement a simple language - no functions, minimum of commands, no unnecessary operators, … Why bother? Will your language be compatible with anything? Well, you can try and implement something that looks like VBA or Java or C, again - it's possible, but you'll have to work hard.

If you are with me till this point - you definitely need to use Active Scrip hosting. The technology allows you to concentrate on your functionality - all you need to implement is your functions - functions that change the user interface or process some data. After you implement them, all you need to do is expose them in a COM interface and that's it - your application is script-ready.

Additional benefits: script engines are based on standard languages - VBScript, JScript - two powerful scripting languages. Implementing an interpreter for such language is quite a complex job, but now you don't need to - Microsoft gives you free scripting engines for those two languages. If that is not enough, there are additional scripting engines available from 3rd party companies - engines for Perl, Python and REXX. Each of those languages can become the scripting language of your application in minutes. All you have to do is build a COM object that implements 3 interfaces: IActiveScriptSite, IActiveScriptSiteWindow and your interface, the one that exposes your customization functions. Implementation of such object with Active Templates Library will be discussed in this article.

Implementation Plan

So, we need to build a COM object. The easiest way to do it is to use the Microsoft Active Templates Library (ATL). The only problem is that using ATL in MFC project is not supported in MSVC 5, this is one of the major advantages (in my opinion) of MSVC 6. One of the articles in MSJ clearly stated that now (VC 6) the use of ATL in MFC projects is fully supported - great news for COM developers. There is a good sample of Active Script hosting available from Microsoft - its is called mfcaxs and it is available for download from www.microsoft.com. The problem with the sample is that it is an MFC project, so it can't use ATL for COM objects. They use another wrapping of bare COM implementation - I don't want to discuss whether it is a successful one or not, but one thing is clear - it is not the preferred way to develop COM objects, and in MSVC 6 this way is obsolete. We'll use ATL to build our Active Script host.

Defining an Interface

First of all, we need to define an interface for our object. This interface will expose the customization functionality we need in our application. Does it have to be one object? No, this isn't compulsory. Only one object will become a top-level script host, but it's public members can be other COM objects, and the script will be able to access them. Here is an example for an interface in IDL:


	[
		object,
		uuid(E4F4B42E-5611-11D2-BB24-006094A513BD),
		dual,
		helpstring("IItemPreprocessor Interface"),
		pointer_default(unique)
	]
	interface IItemPreprocessor : IDispatch
	{
		[id(1), helpstring("method Run")] HRESULT Run();
		[id(2), helpstring("method DoSomething")] HRESULT DoSomething();
		[id(3), helpstring("method Stop")] HRESULT Stop();
	};

It looks like a regular interface. Make sure, that you implement a dual interface - one that can be accessed both via V-table and IDispatch. This is important, because scripting engines like VBScript use a "very late binding" method when calling COM objects - they use GetIDsOfNames and Invoke functions of IDispatch interface to call exported functions.

In this example our interface has only one function - DoSomething. This function will be called from script.

Defining Scripting Host Class

What we need to do now is define a class that will implement IActiveScriptSite, IActiveScriptSiteWindow and IItemPreprocessor interfaces. With ATL the code looks like the following:


class CItemPreprocessor : 
	public CComDualImpl, 
	public ISupportErrorInfo,
	public CComObjectRoot,
	public CComCoClass,
	public IActiveScriptSite,
	public IActiveScriptSiteWindow
{
public:
	CItemPreprocessor() {}
BEGIN_COM_MAP(CItemPreprocessor)
	COM_INTERFACE_ENTRY(IDispatch)
	COM_INTERFACE_ENTRY(IItemPreprocessor)
	COM_INTERFACE_ENTRY(IActiveScriptSite)
	COM_INTERFACE_ENTRY(IActiveScriptSiteWindow)
	COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
//DECLARE_NOT_AGGREGATABLE(CItemPreprocessor) 
// Remove the comment from the line above if you don't want your object to 
// support aggregation. 

DECLARE_REGISTRY_RESOURCEID(IDR_ItemPreprocessor)

~CItemPreprocessor();

// ISupportsErrorInfo
	STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

// IActiveScriptSite
		virtual HRESULT STDMETHODCALLTYPE GetLCID( 
            /* [out] */ LCID __RPC_FAR *plcid);
        
        virtual HRESULT STDMETHODCALLTYPE GetItemInfo( 
            /* [in] */ LPCOLESTR pstrName,
            /* [in] */ DWORD dwReturnMask,
            /* [out] */ IUnknown __RPC_FAR *__RPC_FAR *ppiunkItem,
            /* [out] */ ITypeInfo __RPC_FAR *__RPC_FAR *ppti);
        
        virtual HRESULT STDMETHODCALLTYPE GetDocVersionString( 
            /* [out] */ BSTR __RPC_FAR *pbstrVersion);
        
        virtual HRESULT STDMETHODCALLTYPE OnScriptTerminate( 
            /* [in] */ const VARIANT __RPC_FAR *pvarResult,
            /* [in] */ const EXCEPINFO __RPC_FAR *pexcepinfo);
        
        virtual HRESULT STDMETHODCALLTYPE OnStateChange( 
            /* [in] */ SCRIPTSTATE ssScriptState);
        
        virtual HRESULT STDMETHODCALLTYPE OnScriptError( 
            /* [in] */ IActiveScriptError __RPC_FAR *pscripterror);
        
        virtual HRESULT STDMETHODCALLTYPE OnEnterScript( void);
        
        virtual HRESULT STDMETHODCALLTYPE OnLeaveScript( void);

// IActiveScriptSiteWindow

		virtual HRESULT STDMETHODCALLTYPE GetWindow( 
            /* [out] */ HWND __RPC_FAR *phwnd);
        
        virtual HRESULT STDMETHODCALLTYPE EnableModeless( 
            /* [in] */ BOOL fEnable);
        


// IItemPreprocessor

public:
	STDMETHOD(Stop)();
	STDMETHOD(DoSomething)();
	STDMETHOD(Run)();
		IActiveScriptParse*		m_ScriptParser;
		IActiveScript*			m_ScriptEngine;

private:
		int CreateEngine();
};

Now we'll implement the class. Don't be afraid, you won't have to implement everything - just the "must" functions.

Implementing Script Host Class

Here is an implementation of the class defined in a previous chapter. As you'll see, we don't have to support al functions of IActiveScriptSite.


const GUID CLSID_VBScript = { 0xb54f3741, 0x5b07, 0x11cf, { 0xa4, 0xb0,  0x0,  0xaa,  0x0,  0x4a,  0x55,  0xe8 } };

This is a GUID of VBScript scripting engine - the one we'll be using in this sample.

Release scripting engine and parser objects in a DTOR:


CItemPreprocessor::~CItemPreprocessor()
{
	if (m_ScriptEngine)
		m_ScriptEngine->Release();
	if (m_ScriptParser)
		m_ScriptParser->Release();
}

The following is an implementation of IActiveScriptSite:

Following two methods are not compulsory, you can check their purpose in the help system.


HRESULT STDMETHODCALLTYPE CItemPreprocessor::GetDocVersionString(/* [out] */ BSTR __RPC_FAR *pbstrVersion)
{
	return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE CItemPreprocessor::GetLCID(/* [out] */ LCID __RPC_FAR *plcid)
{
	return E_NOTIMPL;
}

This method is very important. It "tells" the scripting engine about the site it is running in. This method is called when a script needs to access one of the external objects. It is possible to access second level objects via the main host object like: Preprocessor.Reader.Advance, or it is also possible to expose "named" second-level objects to the scripting engine. This is very important when those objects fire events that should be processed by script. Remember: Only named objects' events can be caught by a script. Objects naming is done in CreateEngine, we'll see it later in the code.


HRESULT STDMETHODCALLTYPE CItemPreprocessor::GetItemInfo(/* [in] */ LPCOLESTR pstrName,
									  /* [in] */ DWORD dwReturnMask,
								      /* [out] */ IUnknown __RPC_FAR *__RPC_FAR *ppiunkItem,
									  /* [out] */ ITypeInfo __RPC_FAR *__RPC_FAR *ppti)
{
	
	

	TRACE("GetItemInfo: Name = %s Mask = %x\n", pstrName, dwReturnMask);

	if (dwReturnMask & SCRIPTINFO_ITYPEINFO)
	{
		if (!ppti)
			return E_INVALIDARG;
		*ppti = NULL;
	}

	if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
	{
		if (!ppiunkItem)
			return E_INVALIDARG;
		*ppiunkItem = NULL;
	}

	// Global object
	if (!_wcsicmp(L"Scripter", pstrName))
	{
		if (dwReturnMask & SCRIPTINFO_ITYPEINFO)
		{
			GetTypeInfo(0 /* lcid unknown! */, NULL, ppti);
			(*ppti)->AddRef();      // because returning
		}
		
		if (dwReturnMask & SCRIPTINFO_IUNKNOWN)
		{
			*ppiunkItem = (IDispatch*)this;//pThis->GetIDispatch(TRUE);
			(*ppiunkItem)->AddRef();    // because returning
		}
		return S_OK;
	}

	return E_INVALIDARG;
}

The following four functions notify the host about the script state. In this case their only purpose is to notify the debug environment.


HRESULT STDMETHODCALLTYPE CItemPreprocessor::OnScriptTerminate(/* [in] */ const VARIANT __RPC_FAR *pvarResult,
											/* [in] */ const EXCEPINFO __RPC_FAR *pexcepinfo)
{
	TRACE("\nOnScriptTerminate");
	return S_OK;
}
        
HRESULT STDMETHODCALLTYPE CItemPreprocessor::OnStateChange(/* [in] */ SCRIPTSTATE ssScriptState)
{
	TRACE("\nOnStateChange");
	return S_OK;
}

HRESULT STDMETHODCALLTYPE CItemPreprocessor::OnEnterScript( void)
{
	TRACE("\nOnEnterScript");
	return S_OK;
}
        
HRESULT STDMETHODCALLTYPE CItemPreprocessor::OnLeaveScript( void)
{
	TRACE("\nOnLeaveScript");
	return S_OK;
}

The following function is called when error occurs in a script. Error details can be explored using IActiveScriptError interface.


HRESULT STDMETHODCALLTYPE CItemPreprocessor::OnScriptError(/* [in] */ IActiveScriptError __RPC_FAR *pscripterror)
{

	EXCEPINFO ei;
	DWORD     dwSrcContext;
	ULONG     ulLine;
	LONG      ichError;
	BSTR      bstrLine = NULL;
	CString strError;

	pscripterror->GetExceptionInfo(&ei);
	pscripterror->GetSourcePosition(&dwSrcContext, &ulLine, &ichError);
	pscripterror->GetSourceLineText(&bstrLine);
	
	CString desc;
	CString src;

	desc = (LPCWSTR)ei.bstrDescription;
	src = (LPCWSTR)ei.bstrSource;

	strError.Format("%s\nSrc: %s\nLine:%d Error:%d Scode:%x", desc, src, ulLine, (int)ei.wCode, ei.scode);
	AfxMessageBox(strError);

	TRACE(strError);
	TRACE("\n");

	return S_OK;
	
}

The following is an implementation of IActiveScriptSiteWindow: In our case those functions really don't do too much. When asked a window handle, we return null - our host doesn't have a window. If we had a window, we'd like it to become modal when a message box is popped from the script, in this case, again we do nothing.


HRESULT STDMETHODCALLTYPE CItemPreprocessor::GetWindow(/*[out]*/HWND __RPC_FAR *phwnd)
{
	phwnd = NULL;
	return S_OK;
}
        
HRESULT STDMETHODCALLTYPE CItemPreprocessor::EnableModeless(/*[in]*/BOOL fEnable)
{
	return S_OK;
}

And now, to the main part - creating the script engine, parsing and executing the script:


int CItemPreprocessor::CreateEngine()
{
	HRESULT hr;

	// XX ActiveX Scripting XX 
	hr = CoCreateInstance(
		CLSID_VBScript, NULL, CLSCTX_INPROC_SERVER, IID_IActiveScript, (void **)&m_ScriptEngine);
	if (FAILED(hr))
		// If this happens, the scripting engine is probably not properly registered
		return FALSE;
	// Script Engine must support IActiveScriptParse for us to use it
	hr = m_ScriptEngine->QueryInterface(IID_IActiveScriptParse, (void **)&m_ScriptParser);
	
	if (FAILED(hr))
		goto Failure;
	
	hr = m_ScriptEngine->SetScriptSite((IActiveScriptSite*)this); 
	
	if (FAILED(hr))
		goto Failure;
	
	// InitNew the object:
	hr = m_ScriptParser->InitNew();
	
	if (FAILED(hr))
		goto Failure;

	// Add Top-level Global Named Item
	hr = m_ScriptEngine->AddNamedItem(L"Scripter", SCRIPTITEM_ISVISIBLE | SCRIPTITEM_ISSOURCE | SCRIPTITEM_GLOBALMEMBERS);
	if (FAILED(hr))
		TRACE("Couldn't add named item Scripter\n");

	return TRUE;

Failure:
	if (m_ScriptEngine)
		m_ScriptEngine->Release();
	if (m_ScriptParser)
		m_ScriptParser->Release();

	// XX ActiveX Scripting XX
	return FALSE;
}
STDMETHODIMP CItemPreprocessor::Run()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	// TODO: Add your implementation code here
	EXCEPINFO    ei;
	HRESULT		 hr;

	if (!CreateEngine())
	{
		AfxMessageBox("Failed to create script engine. Cannot execute script.");

		TRACE("Failed to create script engine. Cannot execute script.");
		return S_OK;
	}

	CStdioFile cfScript("script.scr",CFile::modeRead);

	CString strScriptText;
	CString csNewLine;

	while(cfScript.ReadString(csNewLine))
	{
		strScriptText += "\n";
		strScriptText += csNewLine;
	}
	

	BSTR bstrParseMe = strScriptText.AllocSysString();

	m_ScriptParser->ParseScriptText(bstrParseMe, L"Scripter", NULL, NULL, 0, 0, 0L, NULL, &ei);

	// Start the script running...
	hr = m_ScriptEngine->SetScriptState(SCRIPTSTATE_CONNECTED);
	if (FAILED(hr))
	{
		TRACE("SetScriptState to CONNECTED failed\n");
	}


	return S_OK;
}

STDMETHODIMP CItemPreprocessor::Stop()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState())

	HRESULT	hr;

	// TODO: Add your implementation code here
	// Close Script
	hr = m_ScriptEngine->Close();
	if (FAILED(hr))
	{
		TRACE("Attempt to Close AXS failed\n");
		goto FailedToStopScript;
	}
	// NOTE: Handle OLESCRIPT_S_PENDING
	// The documentation reports that if you get this return value,
	// you'll need to wait until IActiveScriptSite::OnStateChange before
	// finishing the Close. However, no current script engine implementation
	// will do this and this return value is not defined in any headers currently
	// (This is true as of IE 3.01)

	if (m_ScriptParser)
	{
		m_ScriptParser->Release();
		m_ScriptParser = NULL;
	}
	if (m_ScriptEngine)
	{
		m_ScriptEngine->Close();
		m_ScriptEngine->Release();
		m_ScriptEngine = NULL;
	}

	return S_OK;

FailedToStopScript:
	AfxMessageBox("Attempt to stop running script failed!\n");


	return S_OK;
}

Well, that's it. You host is ready. Does it look like a lot of code to you? Well, it really isn't. The good thing about this code is that it is reusable. You can just copy the code from this sample and use it. That's what I did - I copied large parts of the code from the sample I downloaded from Microsoft site and changed the syntax from MFC-OLE to ATL in some places. Now we can implement our DoSomething function, and the script will call it.


STDMETHODIMP CItemPreprocessor::DoSomething()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	AfxMessageBox("Do Something");
	// TODO: Add your implementation code here

	return S_OK;
}

Conclusion

In this article I've explained the basic idea of Active Scripting technology (briefly) and I've given an example of implementation of Active Script host using Active Templates Library. After reading this article you can easily implement one yourself. The only major issue about Active Scripting not covered in this article is firing events from named items to script engine. Hopefully, this topic will be covered in my next article. The idea of writing this article came to my mind when I was looking for information about Active Scripting technology on the Internet and I didn't find anything that could guide me through creation of Active Scripting host. I hope that this article will help you.


Notes (By V. Rama Krishna)

Following are the project files. Since I am using VC++ 6.0 the .dsw files are converted into VC++ 6.0 format. Those of you who are using VC++ 5.0 please rename .001 files to .dsw and use it.

There is a file Script.scr in the Script Client project that has a single function DoSomething.

Download Script Host 31 KB

Download Script Client KB



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 20, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds