Архивы: по дате | по разделам | по авторам

Как скриптуются приложения. Акт первый.

АрхивПрограммазм (архив)
автор : Николай Куртов   26.12.2000

Взгляд изнутри на внутреннюю автоматизацию программ или "как нынче модно 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, который упрощает все вышеперечисленные функции и организует их в более приемлемом виде. Не без ущерба в скорости, конечно, :).

Продолжение следует.

Обсуждение статьи - в форуме "Обсудим "СофтТерру"

© ООО "Компьютерра-Онлайн", 1997-2024
При цитировании и использовании любых материалов ссылка на "Компьютерру" обязательна.