Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

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

eManual - электронная документация

Как скриптуются приложения
Взгляд изнутри на внутреннюю автоматизацию программ или "как нынче модно 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)rnMsgBox str & Date()rnEnd 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, который позволяет добавлять различные идентификаторы в пространство имен скрипта.

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

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

Поместив скрипт-компонент на какую-нибудь форму программы (от сего места будем звать его 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/.



  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте