Последнее время я заметил некоторый интерес хабралюдей к такой теме как скриптинг. Были статьи про Lua, про V8 (JavaScript движок Google Chrome). Я же хотел бы рассказать об использовании технологии Active Scripting (она же ActiveX Scripting) от Microsoft.
Это технология, используемая для реализации поддержки скриптов в приложениях. Именно так работает движок JavaScript всеми любимого браузера IE ;) Однако, не спешите с выводами. Да, тот же движок V8 работает в разы быстрее, но и у данной технологии есть свои преимущества и возможные области применения, о которых я тоже расскажу.

Введение в Active Scripting


Собственно, ничего особо сложного эта технология из себя не представляет. Итак, по порядку.
Начнем с того, что технология основана на COM (Component Object Model — компонентной модели объектов). Основные компоненты — это host application и script engine.

Host application предоставляет «окружение» для script engines и предоставляет ему некоторый набор объектов, которыми скрипт может оперировать;
Script engine отвечает за парсинг, запуск и отладку скриптов на каком-либо конкретном языке.

Абстракция модулей находится на высоком уровне, хост-приложению даже не важно, какой язык используется для скриптов, поскольку вся работа по парсингу и запуску скрипта выполняется модулем скриптового движка.
Мы можем как написать свой скриптовый движок для какого-нибудь экзотического языка, так и наоборот, использовать в своем приложении готовые движки (JScript или VBScript), реал��зовав модуль script host. Данная статья посвящена последнему.

Где можно использовать


Наиболее ощутимую выгоду от использования Active Scripting можно получить, если ваше приложение также основано на COM. В этом случае вы можете напрямую полноценно взаимодействовать со своими COM-объектами из скрипта. Имеются кое-какие моменты, которые нужно учитывать (в основном это относится к типам принимаемых и возвращаемых значений методов).
Однако, даже если ваше приложение не использует COM, достаточно просто реализовать небольшую «прослойку» в виде COM-объекта, который будет обеспечивать взаимодействие скрипта и вашего кода.

С чего начать?


А начать, я думаю, лучше всего с простенького примера. Скачать его можно отсюда. А дальше по ходу статьи я буду приводить куски кода из него, поясняя отдельные моменты. Пример представляет собой консольное приложение, написанное на C++, с применением библиотеки ATL. В этом примере используется именно JavaScript движок, и далее по тексту статьи я буду говорить о JScript реализации script engine. Просто потому что люблю JavaScript и терпеть не могу VBS :) К тому же, как я уже говорил, к реализации script host это не имеет отношения. Итак, перейдем к практической части.

Реализация Script Host


Как уже говорилось, для того чтобы использовать готовые скриптовые движки, необходимо реализовать свой модуль script host. Он представляет собой обычный COM-объект, содержащий реализацию интерфейсов IActiveScriptSite и IActiveScriptSiteWindow. Чтобы не усложнять пример, я не стал делать полноценный COM-объект, а обошелся обычным C++ классом, унаследованным от IActiveScriptSite и IActiveScriptSiteWindow:

class CMyScriptHost :  public IActiveScriptSite,
            public IActiveScriptSiteWindow


* This source code was highlighted with Source Code Highlighter.


Начнем с реализации общего для всех COM-объектов интерфейса IUnknown (наш класс унаследовал его косвенно от интерфейсов IActiveScriptSite и IActiveScriptSiteWindow). Тут ничего сложного, всего три метода:

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


* This source code was highlighted with Source Code Highlighter.


AddRef увеличивает счетчик ссылок; Rlease — уменьшает, а также удаляет объект, когда счетчик станет равным 0; QueryInterface возвращает указатель на объект, если у него запрашивают один из поддерживаемых интерфейсов.

В реализации интерфейса IActiveScriptSite пока везде стоят заглушки

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.


Исключение составляет лишь метод OnScriptError, в нем формируется строка с информацией об ошибке и затем выводится с помощью MessageBox`а:

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.


В реализации IActiveScriptSiteWindow тоже пока заглушки.

Далее, добавим к нашему классу два поля для хранения указателей на объект script engine:

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.


На самом деле эти переменные указывают на разные интерфейсы одного и того же объекта.

Теперь добавим к нашему классу несколько методов:

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.


Метод Initialize(), как вы, наверное, догадались инициализирует 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.


Сперва мы создаем объект script engine и запоминаем его в m_pEngine. Потом получаем указатель на интерфейс IActiveScriptParse этого же объекта и сохраняем его в m_pParser (для незнакомых с ATL — получение интерфейса скрыто, т.к. используется умный указатель на интерфейс, CComQIPtr, который получает нужный интерфейс при присваивании ему значения). Далее, устанавливаем движку его site (т.е. хост) — себя. Инициализируем script engine.

Метод Close() обеспечивает корректное завершение работы скрипта:

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.


Метод PutScript() переданный ему текст скрипта скармливает парсеру и запускает движок, вызывая 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.


Ну и наконец, метод CallJSFunction() вызывает функцию с заданным именем и возвращает результат в виде переменной типа 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.


Обратите внимание, сейчас в функцию JavaScript никаких параметров не передается, но сделать это очень просто: используем вместо метода Invoke0 — Invoke1, Invoke2 или InvokeN и передаем параметры в переменных типа VARIANT.

Вот и все, теперь у нас есть необходимый минимум чтобы запустить простенький скрипт.

Привет, Мир!



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.


Компилируем, запускаем. Видим в консоли:

Result: Hello, World!

Press any key to exit…

Пожалуй, для первого раза достаточно. В следующей статье я расскажу о более глубоком взаимодействии скрипта с компилируемым кодом.

PS В классе CMyScriptHost в примере частично используется код zserg — гуру всего и всея в программировании :)
PPS Первая моя статья на Хабре, здоровая критика и пожелания приветствуются
PPPS Парсер видимо очень не любит слово Script, везде написал его исключительно маленькими буквами. Смотрите правильное написание в коде примера.