Как скриптуются приложения. Акт первый.
АрхивПрограммазм (архив)Взгляд изнутри на внутреннюю автоматизацию программ или "как нынче модно VBScript прикручивать".
Идея
Очень часто в различных задачах возникала потребность в реализации того или иного миниязыка, описывающего какие-то процессы или, чаще, реакцию на какие-то события. По-правде говоря, дело это довольно сложное и неблагодарное. Написанию парсеров и интерпретаторов посвящены горы документации и книг. А хочется всего навсего две-три функции, складывающие пару параметров, да возвращающе их назад приложению. Идея создания базиса для поддержки макроязыков (скриптов) возникла вместе с появлением COM. Причем идея эта была проработана довольно фундаментально: можно использовать уже имеющиеся модули скрипт-языков, такие как JavaScript или VBScript, а можно и определить любой другой язык или использовать модули третьих фирм. И автоматизация проявляется во всей красе: достаточно лишь пронаследовать свои объекты от IDispatch (CCmdTarget), добавить их в список глобальных переменных пространства скрипта, а дальше хоть всю логику на скрипте пиши. Конечно, медленно, но некоторые задачи и нацелены на максимальную масштабируемость. Ну а ежели логику жалко, то можно просто пару событий обрабатывать.
В общем, идея получила название ActiveScripting и перелилась в отдельную технологию, базирующуюся на нескольких механизме COM.
Активное скриптование во плоти
Реализация была поделена на две части: Active Scripting Engine и Active Scripting Host. В качестве Active Scripting Host выступает прикладное приложение, использующее возможности скрипт-парсеров. А Active Script Engine – это компонент, который экспортирует некоторое количество COM интерфейсов и понимает, как обрабатывать синтаксис определенного скрипт-языка. Обычно, в составе Windows имеется два таких компонента : парсер JavaScript и парсер VBScript. Они лежат соответственно в библиотеках jscript.dll и vbscript.dll. Модуль VBScript реализует интерфейсы: IActiveScript, IActiveScriptDebug, IActiveScriptParse, IActiveScriptStats, IObjectSafety, IRemoteApplicationDebugEvents и IVariantChangeType. Как видите, довольно много, поэтому лучшим помощником при реализации своего скрипт-модуля будет документация MSDN. Большинству программистов это даже и не понадобится, поскольку VBScript удовлетворят большинству потребностей автоматизации. Хочется только обратить внимание на интерфейс IActiveScript и IActiveScriptParse, поскольку они и являются связующим звеном между Active Scripting Host и Active Scripting Engine.
- IActiveScript
Этот интерфейс первым запрашивается у модуля скрипт-языка и используется для инициализации. Я рассмотрю методы SetScriptSite, AddNamedItem, SetScriptState и GetScriptDispatch при реализации скрипт-хоста. - IActiveScriptParse
Реализация этого интерфейса отвечает за обработку текста самого скрипта через метод ParseScriptText.
Active Scripting Engine создается как обычный СOM объект:
#include <activscp.h> DEFINE_GUID(CLSID_VBScript, 0xb54f3741, 0x5b07, 0x11cf, 0xa4, 0xb0, 0x0, 0xaa, 0x0, 0x4a, 0x55, 0xe8); … CComPtr<IActiveScript> pScriptEngine; CComQIPtr<IActiveScriptParse> pScriptParse; pScriptEngine->CoCreateInstance(CLSID_VBScript); pScriptParse = pScriptEngine; …
Для того, чтобы все это начало как-то работать, нужно реализовать интерфейс IActiveScriptSite, через который скрипт-модуль может взаимодействовать с нашим приложением.
Как реализуется хост
Итак, необходимо реализовать IActiveScriptSite и укзать скрипт-модулю что ему есть с чем работать.
IActiveScriptSite : IUnknown { HRESULT GetLCID(LCID* plcid) = 0; HRESULT GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown** ppiunkItem, ITypeInfo** ppti) = 0; HRESULT GetDocVersionString(BSTR* pbstrVersion) = 0; HRESULT OnScriptTerminate(VARIANT* pvarResult, EXCEPINFO* pexcepinfo) = 0; HRESULT OnStateChange(SCRIPTSTATE ssScriptState) = 0; HRESULT OnScriptError(IActiveScriptError* pscripterror) = 0; HRESULT OnEnterScript(void) = 0; HRESULT OnLeaveScript(void) = 0; };
Реализовывать можно по схеме MFC с BEGIN_INTERFACE_MAP/ INTERFACE_PART или же по обычной схеме, расписывая AddRef, Release и QueryInterface. Поскольку объект должен экспортировать только один интерфейс, IActiveScriptSite, то все СOM- внутренности можно довольно просто расписать без помощи MFC. Подробный код для этого прилагается в примере.
После того, как скрипт-модуль проинициализирован, ему необходимо передать указатель на ActiveScriptSite :
pSite = new CScriptSite; pScriptEngine->SetScriptSite(pSite);
Теперь скрипт-модулю есть как общаться с нашим приложением, но нечего запускать. Настала пора передать придумать какой-нибудь скрипт и передать его через интерфейс IScriptParse на обработку и запуск.
LPWSTR pCode; EXCEPINFO pException = { 0 }; pCode = L”Sub Test(str)\r\nMsgBox str & Date()\r\nEnd Sub”; pScriptParse->InitNew(); pScriptParse->ParseScriptText (pCode, 0, NULL, NULL, 0, 0, 0, NULL, &pException); piScript->SetScriptState(SCRIPTSTATE_CONNECTED);
Это еще не все, жизнь становится немного сложнее :). Теперь нужно запустить сложнейшую процедуру Test. Кстати, если оформить код вне процедуры, то дальнейших шагов не нужно – скрипт автоматически бы запустит все, что не лежит в какой-либо процедуре. Говоря языком C, все, что у вас не разложено по процедурам и функциям, попадает в void main().
Для начала нужно получить указатель на Dispatch интерфейс, представляющий элемент внутреннего пространства скрипта. Он создается Script Engine после того, как вызвана функция ParseScriptText.
IDispatch* piDisp; piScriptEngine->GetScriptDispatch(L"", &piDisp);
Первый параметр, как видите, пустой. Здесь можно указать название объекта во внутреннем пространстве скрипта. Пустой параметр означает пространство всех функций.
Получив dispatch, нужно произвести вызов через метод Invoke , при этом передав необходимые для функции параметры. В нашем случае это строка для функции Test.
OLECHAR* szMember; DISPID dispid; VARIANTARG* pvarArgs; DISPPARAMS dispArgs; pvarArgs = new VARIANTARG[1]; pvarArgs[0].vt = VT_BSTR; pvarArgs[0].bstrVal = SysAllocString(L"Today is "); dispArgs.rgvarg = pvarArgs; dispArgs.cArgs = 1; dispArgs.cNamedArgs = 0; dispArgs.rgdispidNamedArgs = NULL; szMember = L"Test"; piDisp->GetIDsOfNames(IID_NULL, &szMember, 1,LOCALE_USER_DEFAULT, &dispid); piDisp->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispArgs, NULL, NULL, NULL); piDisp->Release(); delete pvarArgs[];
Даже при всем при том, что функция наша запустилась и выдала окошко, такой скрипт совсем никому не нужен. Ведь все это колдовство было устроено ради работы с внутренними объектами программы.
Интерфейс IActiveScript имеет метод AddNamedItem, который позволяет добавлять различные идентификаторы в пространство имен скрипта.
m_iActiveScript->AddNamedItem(L"MyObject", SCRIPTITEM_ISVISIBLE | SCRIPTITEM_ISSOURCE);
Таким образом мы добавили идентификатор, который пока что никакой информации не несет. А вот если что-то внутри VB-скрипта обратится к этому идентификатору, то Engine автоматически вызовет метод хоста IActiveScriptSite:: GetItemInfo, который должен уже быть нами реализован.
virtual HRESULT _stdcall GetItemInfo(LPCOLESTR pstrName, DWORD dwReturnMask, IUnknown **ppunkItem, ITypeInfo **ppti) { // Cпрашивает о ITypeInfo? if(ppti) { *ppti = NULL; // Его у нас нет. if(dwReturnMask & SCRIPTINFO_ITYPEINFO) return TYPE_E_ELEMENTNOTFOUND; } // Engine запрашивает наш объект if(ppunkItem) { *ppunkItem = NULL; if(dwReturnMask & SCRIPTINFO_IUNKNOWN) { // Проверим, наш ли это объект if (!_wcsicmp(L"MyObject", pstrName)) { // Это наш объект ? *ppunkItem = m_pScriptObject; // Увеличить счетчик ссылок m_pScriptObject->AddRef(); } } } return S_OK; }
Объект m_pScriptObject – указатель на нашего CCmdTarget наследника, который создается по технологии MFC Automation.
Теперь в скрипте можно совершенно смело обращаться к automation свойствам и методам MyObject. А в случае runtime-ошибки будет вызван метод IActiveScriptSite::OnScriptError.
Итак, тех, то сумел реализовать VBScript-автоматизацию “натуральным” методом, могу поздравить, а тех, кто не разобрался поспешу утешить: Microsoft выпустила ActiveX компонент, под названием ScriptControl, который упрощает все вышеперечисленные функции и организует их в более приемлемом виде. Не без ущерба в скорости, конечно, :).
Продолжение следует.
Обсуждение статьи - в форуме "Обсудим "СофтТерру"