Pull to refresh

Добавление возможности скриптинга своим приложениям с помощью Active scripting

Reading time10 min
Views4.5K
Последнее время я заметил некоторый интерес хабралюдей к такой теме как скриптинг. Были статьи про 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, везде написал его исключительно маленькими буквами. Смотрите правильное написание в коде примера.
Tags:
Hubs:
+7
Comments8

Articles