On-Line Библиотека www.XServer.ru - учебники, книги, статьи, документация, нормативная литература.
       Главная         В избранное         Контакты        Карта сайта   
    Навигация XServer.ru






 

Как скриптуются приложения

Николай Куртов

Идея


Очень часто в различных задачах возникала потребность в реализации того или иного миниязыка, описывающего какие-то процессы или, чаще, реакцию на какие-то события. По-правде говоря, дело это довольно сложное и неблагодарное. Написанию парсеров и интерпретаторов посвящены горы документации и книг. А хочется всего навсего две-три функции, складывающие пару параметров, да возвращающе их назад приложению. Идея создания базиса для поддержки макроязыков (скриптов) возникла вместе с появлением 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, нужно произвести вызов через метод 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, который позволяет добавлять различные идентификаторы в пространство имен скрипта.

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

Скрипт-компонент можно добавить совершенно стандартным методом VB: из диалогового окна Components. Если же такого пункта в списке нет, то достаточно найти его на диске при помощи Browse.

Как найти script control

Поместив скрипт-компонент на какую-нибудь форму программы (от сего места будем звать его ScriptControl1), вы обнаружите, что сам компонентик-то не так уж и много свойств имеет: AllowUI, Language и Timeout.

Свойство AllowUI позволяет определить, может ли скрипт показывать свои диалоговые окна, такие как сообщения об ошибках, различные MsgBox и InputBox.

Свойство Language по умолчанию установлено в VBScript, но с равным успехом может содержать имя JScript.

Свойство TimeOut задает максимальный интервал, после которого скрипт будет принудительно завершен. Это полезно в том случае, если скрипт "нечаянно" зациклился.

Теперь можно обратиться к внутренностям компонента, при этом обнаружив некоторые функции, позволяющие уже что-то запускать, без мучительной подготовки: Eval и ExecuteStatment. Как нетрудно догадаться, первая вычисляет выражение, введенное в виде строки, а вторая выполняет завершенный оператор.

Private Sub Command1_Click()
 a = ScriptControl1.Eval("2*6+4")
 ScriptControl1.ExecuteStatement "MsgBox " & Str(a)
End Sub

Вычисляться могут любые выражения: в целых числах, строковые, логические, возврат функций. Результат возвращается в виде VARIANT переменной, которая, как известно, может содержать все.

Вряд ли взявшись за освоение Script Control, можно удовлетвориться единственной обрабатываемой строчкой. Метод AddCode позволяет добавить несколько строчек, которые потом можно запустить методом Run. Если функция принимает на вход параметры, то эти параметры можно также передать через Run.

Private Sub Command1_Click()
 aCode = "Const Hello = ""Hello,"" " & vbCrLf _
 & "Sub ShowMyName(Name)" & vbCrLf _
 & " MsgBox Hello & Name" & vbCrLf _
 & "End Sub"
 
 ' Вызов метода Reset необходим для исключения ошибки
 ' переопределения кода
 ScriptControl1.Reset
 ScriptControl1.AddCode aCode
 ScriptControl1.Run "ShowMyName", "Nickolay"
End Sub

Код, который передается в качестве параметра методу AddCode должен быть корректным и содержать одну или несколько процедур или функций. В случае, если код, добавляемый при помощи AddCode некорректен, компонент выбрасывает исключение. Задача разработчика - перехватить эту ошибку и дать пользователю понять, что задача пользователя - писать скрипты грамотнее.

 On Error Resume Next
 ScriptControl1.AddCode code
 If Err Then
 ' Здесь у нас синтаксическая ошибка
 MsgBox Err.Number & ": " & Err.Description, , _
 "Проверь синтаксис!"
 Else
 ScriptControl1.Run "SubName"
 If Err Then
 ' А здесь - runtime ошибка
 MsgBox Err.Number & ": " & Err.Description,,_
 "Ооопс:"
 End If

Вообще говоря, у Script Control имеется свойство Error и одноименное событие Error, которое вызывается в случае ошибки. В случае ошибок времени выполнения лучше всего воспользоваться именно этим свойством, поскольку оно несет больше информации об ошибке, чем стандартный вариант с Err.

Что касается внутренних объектов программы, то их экспортирование в пространство имен скрипта гораздо проще, чем в Visual C++. Метод AddObject позволяет расширять количество доступных скрипту объектов. Для этого нужно создать Class Module и объявить внутри него необходимые функции и public свойства.

' Динамически создайте новый класс, и
' поместите его под нужным именем в
' пространство имен скрипта
'
 Dim TheClass As New YourClassModule
 ScriptControl1.AddObject "Object1", TheClass

Теперь внутри скрипта можно совершенно спокойно обращаться к объявленному Object1.

MsgBox Object1.MySuperStatus

Таким образом, можно организовать некий proxy-объект для доступа к элементам управления в свой программе. Например, можно в YourClassModule создать функцию, добавляющую какую либо строчку в список, toolbar или меню.

 ' В модуле YourClassModule
 Public Sub AddItem(ByVal text As String) 
 Form1.List1.AddItem text
 End Sub

Следует учитывать, что если нужно передать какую либо внутреннюю переменную скрипта на обработку в ваш ClassModule, то работать придется только с переменными типа Variant.

' В модуле YourClassModule
 Public Sub MakeProperCase(text As Variant) 
 text = StrConv(text, vbProperCase)
 End Sub

Возможности становятся поистине безграничными, если выдавать через свойства YourClassModule какие-нибудь внутренние компоненты. Напрмер, Form.

' В модуле YourClassModule
Property Get ActiveForm() As Object
 Set ActiveForm = Screen.ActiveForm
End Property

Итак, путей для творческого поиска предостаточно, но прежде всего  рекомендую обратиться за полной документацией, а заодно и за последней версией Script Control на  http://msdn.microsoft.com/scripting/.



Литература по VBScript