📜 ⬆️ ⬇️

Adding scripting capabilities to your applications using Active scripting

Recently, I noticed some interest of habra people to such a topic as scripting. There were articles about Lua , about V8 (JavaScript engine Google Chrome). I would like to talk about the use of technology Active Scripting (also known as ActiveX Scripting) from Microsoft.
This is the technology used to implement application support scripts. This is how the JavaScript engine of everyone's favorite IE browser works;) However, do not rush to conclusions. Yes, the same V8 engine works many times faster, but this technology has its advantages and possible applications, which I will also discuss.

Introducing Active Scripting


Actually, this technology is nothing particularly difficult. So, in order.
To begin with, the technology is based on COM (Component Object Model - Component Object Model). The main components are the host application and the script engine .

Host application provides an “environment” for script engines and provides it with some set of objects with which the script can operate;
Script engine is responsible for parsing, running and debugging scripts in a particular language.

Abstraction of modules is at a high level, the host application does not even care what language is used for scripts, since all the work on parsing and running the script is performed by the script engine module.
We can both write our own scripting engine for some exotic language, and vice versa, use ready-made engines (JScript or VBScript) in our application by implementing the script script module. This article is dedicated to the latter.
')

Where to use


The most tangible benefits of using Active Scripting can be obtained if your application is also based on COM. In this case, you can directly fully interact with your COM objects from the script. There are some points that need to be taken into account (this mainly refers to the types of methods accepted and returned values).
However, even if your application does not use COM, it is enough just to implement a small “layer” in the form of a COM object that will ensure the interaction of the script and your code.

Where to begin?


And I think it’s best to start with a simple example. You can download it from here . And further on in the course of the article I will give pieces of code from it, explaining certain points. The example is a console application written in C ++ using the ATL library. In this example, the JavaScript engine is used, and further on in the text of the article I will talk about the JScript implementation of the script engine. Just because I love JavaScript and can't stand VBS :) Besides, as I said before, this has nothing to do with the implementation of the script host. So, we proceed to the practical part.

Implementing Script Host


As already mentioned, in order to use ready-made scripting engines, you need to implement your script host module. It is a normal COM object containing the implementation of the IActiveScriptSite and IActiveScriptSiteWindow interfaces. In order not to complicate the example, I did not make a full-fledged COM object, but I managed with the usual C ++ class inherited from IActiveScriptSite and IActiveScriptSiteWindow:

class CMyScriptHost : public IActiveScriptSite,
public IActiveScriptSiteWindow


* This source code was highlighted with Source Code Highlighter .


Let's start with the implementation of the IUnknown interface common to all COM objects (our class inherited it indirectly from the IActiveScriptSite and IActiveScriptSiteWindow interfaces). There is nothing complicated, just three methods:

STDMETHOD(QueryInterface)(REFIID riid, void * * ppvObj);
STDMETHOD_(ULONG, AddRef)();
STDMETHOD_(ULONG, Release)();


* This source code was highlighted with Source Code Highlighter .


AddRef increases the reference count; Rlease - reduces and also deletes the object when the counter becomes equal to 0; QueryInterface returns a pointer to an object if it is asked for one of the supported interfaces.

In the implementation of the interface IActiveScriptSite while everywhere there are stubs

STDMETHOD(GetLCID)(
LCID *plcid ); // address of variable for language identifier
STDMETHOD(GetItemInfo)(
LPCOLESTR pstrName, // address of item name
DWORD dwReturnMask, // bit mask for information retrieval
IUnknown **ppunkItem, // address of pointer to item's IUnknown
ITypeInfo **ppTypeInfo); // address of pointer to item's ITypeInfo
STDMETHOD(GetDocVersionString)(
BSTR *pbstrVersionString); // address of document version string
STDMETHOD(OnScriptTerminate)(
const VARIANT *pvarResult, // address of script results
const EXCEPINFO *pexcepinfo); // address of structure with exception information
STDMETHOD(OnStateChange)(
SCRIPTSTATE ssScriptState); // new state of engine
STDMETHOD(OnScriptError)(
IActiveScriptError *pase); // address of error interface
STDMETHOD(OnEnterScript)( void );
STDMETHOD(OnLeaveScript)( void );


* This source code was highlighted with Source Code Highlighter .


The only exception is the OnScriptError method, a string with information about the error is formed in it and then output using the MessageBoxʻa:

STDMETHODIMP CMyScriptHost::OnScriptError(IActiveScriptError *pase)
{
#ifdef _DEBUG
EXCEPINFO Exception;
HRESULT hr = pase->GetExceptionInfo(&Exception);
if (SUCCEEDED(hr))
{
CString sErrLog = _T( "" );
sErrLog += _T( "EXCEPINFO" );
sErrLog += _T( "\n\rDescription: " );
sErrLog += Exception.bstrDescription;
sErrLog += _T( "\n\rSource: " );
sErrLog += Exception.bstrSource;

CComBSTR bsSrcLineText;
hr = pase->GetSourceLineText(&bsSrcLineText);
if (SUCCEEDED(hr))
{
sErrLog += _T( "\n\rSource line text: " );
sErrLog += bsSrcLineText;
}

DWORD dwSourceContext = 0;
ULONG ulLineNumber = 0;
LONG lCharacterPosition = 0;

hr = pase->GetSourcePosition(&dwSourceContext, &ulLineNumber, &lCharacterPosition);
if (SUCCEEDED(hr))
{
CString sSourceContext;
sErrLog += _T( "\n\rSource context: " );
sSourceContext.Format(_T( "%d" ), dwSourceContext);
sErrLog += sSourceContext;

CString sLineNumber;
sErrLog += _T( "\n\rLine number: " );
sLineNumber.Format(_T( "%d" ), ulLineNumber);
sErrLog += sLineNumber;

CString sCharPos;
sErrLog += _T( "\n\rCharacterPosition: " );
sCharPos.Format(_T( "%d" ), lCharacterPosition);
sErrLog += sCharPos;
}

::MessageBox(0, sErrLog, COLE2T(Exception.bstrSource), 0);
}
#endif // _DEBUG

return S_OK;
}


* This source code was highlighted with Source Code Highlighter .


In the implementation of the IActiveScriptSiteWindow, so far, too, stubs.

Next, we add to our class two fields for storing pointers to the script engine object:

CComPtr<IActiveScript> m_pEngine; // reference to the scripting engine <br>CComQIPtr<IActiveScriptParse> m_pParser; // reference to the IActiveScriptParse interface of the scripting engine <br><br> * This source code was highlighted with Source Code Highlighter .


In fact, these variables point to different interfaces of the same object.

Now we add several methods to our class:

HRESULT Initialize();<br>HRESULT Close();<br>HRESULT PutScript(CString sScriptText);<br>HRESULT CallJSFunction(CString sFuncName, VARIANT *varResult); <br><br> * This source code was highlighted with Source Code Highlighter .


The Initialize () method, as you probably guessed, initializes the script host:

HRESULT CMyScriptHost::Initialize()<br>{<br> HRESULT hr = E_FAIL;<br><br> //First, create the scripting engine with a call to CoCreateInstance, <br> //placing the created engine in m_Engine. <br><br> hr = m_pEngine.CoCreateInstance(CComBSTR(_T( "JScript" )));<br> if (SUCCEEDED(hr) && m_pEngine)<br> {<br> m_pParser = m_pEngine;<br> if (m_pParser)<br> {<br> //The engine needs to know the host it runs on. <br> hr = m_pEngine->SetScriptSite( this );<br> ATLASSERT(SUCCEEDED(hr));<br><br> //Initialize the script engine so it's ready to run. <br> hr = m_pParser->InitNew();<br> ATLASSERT(SUCCEEDED(hr));<br> }<br> }<br><br> return hr;<br>} <br><br> * This source code was highlighted with Source Code Highlighter .


First we create the script engine object and store it in m_pEngine. Then we get a pointer to the IActiveScriptParse interface of the same object and save it to m_pParser (for those unfamiliar with ATL, getting the interface is hidden, because a smart interface pointer, CComQIPtr, which gets the right interface when it is assigned a value). Next, install the engine of its site (i.e. host) - itself. Initialize the script engine.

The Close () method ensures the correct completion of the script:

HRESULT CMyScriptHost::Close()
{
HRESULT hr = E_FAIL;

if (m_pEngine)
{
if (m_pParser)
m_pParser.Release();

// Disconnect the host application from the engine. This will prevent the
// further firing of events. Event sinks that are in progress are
// completed before the state changes.
m_pEngine->SetScriptState(SCRIPTSTATE_DISCONNECTED);

// Call to InterruptScriptThread to abandon any running scripts and force
// a cleanup of all script elements.
m_pEngine->InterruptScriptThread(SCRIPTTHREADID_ALL, NULL, 0 );
m_pEngine->Close();

m_pEngine.Release();

hr = S_OK;
}

return hr;
}


* This source code was highlighted with Source Code Highlighter .


The PutScript method () transferred to it the script text feeds the parser and starts the engine by calling SetScriptState (SCRIPTSTATE_CONNECTED):

HRESULT CMyScriptHost::PutScript( CString sScriptText )
{
HRESULT hr = E_FAIL;

//Pass the script to be run to the script engine with a call to ParseScriptText
hr = m_pParser->ParseScriptText(sScriptText, NULL, NULL, NULL, 0, 0, 0, NULL, NULL);
hr = m_pEngine->SetScriptState(SCRIPTSTATE_CONNECTED);

return hr;
}


* This source code was highlighted with Source Code Highlighter .


Finally, the CallJSFunction () method calls a function with the specified name and returns the result as a variable of type VARIANT:

HRESULT CMyScriptHost::CallJSFunction( CString sFuncName, VARIANT *varResult )
{
HRESULT hr;
CComPtr<IDispatch> pDispScript;

hr = m_pEngine->GetScriptDispatch( NULL, &pDispScript );

if ( SUCCEEDED(hr) && pDispScript )
{
hr = pDispScript.Invoke0(sFuncName, varResult);
}

return hr;
}


* This source code was highlighted with Source Code Highlighter .


Note that now no parameters are passed to the JavaScript function, but this is very simple: we use instead of the Invoke0 method Invoke1, Invoke2 or InvokeN and pass parameters in variables of type VARIANT.

That's all, now we have the necessary minimum to run a simple script.

Hello World!



int _tmain( int argc, _TCHAR* argv[])
{
CoInitialize(NULL);

CMyScriptHost* myScriptHost = new CMyScriptHost(); // Script host`
myScriptHost->AddRef();

HRESULT hr = E_FAIL;

hr = myScriptHost->Initialize();
if (SUCCEEDED(hr))
{
// test
// - "Hello, World!"
CString sScriptText = _T( "function test() { \
return 'Hello, World!'; \
}"
);

hr = myScriptHost->PutScript(sScriptText);
if (SUCCEEDED(hr))
{
CComVariant varResult; //
hr = myScriptHost->CallJSFunction(_T( "test" ), &varResult); // test
if (SUCCEEDED(hr))
{
_tprintf(_T( "Result: %s\n\r" ), COLE2T(varResult.bstrVal)); //
_tprintf(_T( "\n\rPress any key to exit..." ));
_gettch();
}
}

myScriptHost->Close(); //
}

myScriptHost->Release();

CoUninitialize();
return 0;
}


* This source code was highlighted with Source Code Highlighter .


Compile, run. We see in the console:

Result: Hello, World!

Press any key to exit ...

Perhaps, for the first time is enough. In the next article I will talk about the deeper interaction of the script with the compiled code.

PS In the CMyScriptHost class, the example partially uses zserg code - the guru of everything and everyone in programming :)
PPS My first article on Habré, healthy criticism and suggestions are welcome
PPPS The parser doesn’t really like the word Script, everywhere I wrote it exclusively in small letters. See the correct spelling in the example code.

Source: https://habr.com/ru/post/74548/


All Articles