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

    Последнее время я заметил некоторый интерес хабралюдей к такой теме как скриптинг. Были статьи про 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, везде написал его исключительно маленькими буквами. Смотрите правильное написание в коде примера.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      +2
      эхх, ностальгия пробрала по лихим 90м, а вообще как круто смотрится С++/COM код…
        +1
        Спасибо, возьму на заметку.
        Мне вот хочется использовать v8 как script engine (например через wsh), пытаюсь разобраться как…
          –2
          В основе WSH тоже лежит Active scripting, и боюсь v8 прикрутить в качестве движка к нему будет очень и очень непросто. В основном из-за того, что все объекты, которыми оперируют script engine — это COM-объекты. Придется писать нехилый прокси для связки COM-V8.
          Я уже думал надо этим, но пока не было времени разобраться с V8 толком, слишком уж непривычно его архитектура выглядит, по крайней мере на первый взгляд :) Возможно, если у меня что-нибудь получится, будет тема для еще одной статьи )
          0
          Неактуально, ждем статью про Qtscript.
            0
            Технология старая, да, однако мне довелось с ней очень плотно поработать, и я ощутил, что информации в интернете очень мало на эту тему. В рунете так вообще почти нет.
            Поэтому решил поделиться своим опытом.

            Про Qtscript, к сожалению, пока ничего не могу написать, т.к. не работал с ним.
            0
            Муть, у многих же (у меня например) запрещено где-то в реестре выполение этих скриптов, уж не помню почему.
              0
              эти настройки касаются толкьо выполнения скриптов в Internet Explorer
                0
                Нет, в ИЕ разрешен яваскрипт. У меня инсталляторы некоторых программ писали что-то типа «у вас запрещены скрипты», и надо было где-то в реестре поменять, 0 на 1.

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

            Самое читаемое