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 = %xn", 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("%snSrc: %snLine:%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 Scriptern");

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 failedn");
}

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 failedn");
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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read