Последнее время я заметил некоторый интерес хабралюдей к такой теме как скриптинг. Были статьи про Lua, про V8 (JavaScript движок Google Chrome). Я же хотел бы рассказать об использовании технологии Active Scripting (она же ActiveX Scripting) от Microsoft.
Это технология, используемая для реализации поддержки скриптов в приложениях. Именно так работает движок JavaScript всеми любимого браузера IE ;) Однако, не спешите с выводами. Да, тот же движок V8 работает в разы быстрее, но и у данной технологии есть свои преимущества и возможные области применения, о которых я тоже расскажу.
Собственно, ничего особо сложного эта технология из себя не представляет. Итак, по порядку.
Начнем с того, что технология основана на 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. Он представляет собой обычный COM-объект, содержащий реализацию интерфейсов IActiveScriptSite и IActiveScriptSiteWindow. Чтобы не усложнять пример, я не стал делать полноценный COM-объект, а обошелся обычным C++ классом, унаследованным от IActiveScriptSite и IActiveScriptSiteWindow:
Начнем с реализации общего для всех COM-объектов интерфейса IUnknown (наш класс унаследовал его косвенно от интерфейсов IActiveScriptSite и IActiveScriptSiteWindow). Тут ничего сложного, всего три метода:
AddRef увеличивает счетчик ссылок; Rlease — уменьшает, а также удаляет объект, когда счетчик станет равным 0; QueryInterface возвращает указатель на объект, если у него запрашивают один из поддерживаемых интерфейсов.
В реализации интерфейса IActiveScriptSite пока везде стоят заглушки
Исключение составляет лишь метод OnScriptError, в нем формируется строка с информацией об ошибке и затем выводится с помощью MessageBox`а:
В реализации IActiveScriptSiteWindow тоже пока заглушки.
Далее, добавим к нашему классу два поля для хранения указателей на объект script engine:
На самом деле эти переменные указывают на разные интерфейсы одного и того же объекта.
Теперь добавим к нашему классу несколько методов:
Метод Initialize(), как вы, наверное, догадались инициализирует script host:
Сперва мы создаем объект script engine и запоминаем его в m_pEngine. Потом получаем указатель на интерфейс IActiveScriptParse этого же объекта и сохраняем его в m_pParser (для незнакомых с ATL — получение интерфейса скрыто, т.к. используется умный указатель на интерфейс, CComQIPtr, который получает нужный интерфейс при присваивании ему значения). Далее, устанавливаем движку его site (т.е. хост) — себя. Инициализируем script engine.
Метод Close() обеспечивает корректное завершение работы скрипта:
Метод PutScript() переданный ему текст скрипта скармливает парсеру и запускает движок, вызывая SetScriptState(SCRIPTSTATE_CONNECTED):
Ну и наконец, метод CallJSFunction() вызывает функцию с заданным именем и возвращает результат в виде переменной типа VARIANT:
Обратите внимание, сейчас в функцию JavaScript никаких параметров не передается, но сделать это очень просто: используем вместо метода Invoke0 — Invoke1, Invoke2 или InvokeN и передаем параметры в переменных типа VARIANT.
Вот и все, теперь у нас есть необходимый минимум чтобы запустить простенький скрипт.
Компилируем, запускаем. Видим в консоли:
Result: Hello, World!
Press any key to exit…
Пожалуй, для первого раза достаточно. В следующей статье я расскажу о более глубоком взаимодействии скрипта с компилируемым кодом.
PS В классе CMyScriptHost в примере частично используется код zserg — гуру всего и всея в программировании :)
PPS Первая моя статья на Хабре, здоровая критика и пожелания приветствуются
PPPS Парсер видимо очень не любит слово Script, везде написал его исключительно маленькими буквами. Смотрите правильное написание в коде примера.
Это технология, используемая для реализации поддержки скриптов в приложениях. Именно так работает движок 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, везде написал его исключительно маленькими буквами. Смотрите правильное написание в коде примера.