В данной статье я постараюсь пошагово описать, как малыми усилиями встроить в ваше приложение поддержку скриптового языка.
Так как по роду своей работы мне приходится в основном разрабатывать на Delphi, поэтому и буду описывать данную методику на примере этого продукта (с принятой для этого языка терминологией). Ну а кому интересно – без проблем спроецируют это все на другие среды, поддерживающие работу с ActiveX-компонентами.
Основываться все это будет на компоненте Microsoft Script Control, который входит во все винды, начиная с 98, если не ошибаюсь. А для тех версий Windows, в которых его нет, на сайте Microsoft его можно скачать отдельно.

Итак, для начала нужно поместить в Delphi этот компонент. Для этого (если он у вас еще не «обернут» в VCL-интерфейс и не находится на палитре ActiveX) идем в Components – Import ActiveX Control, выбираем в списке Microsoft Script Control 1.0, и жмем Install. После этих манипуляций на палитре ActiveX будет доступен компонент ScriptControl. А физически он будет находиться в модуле MSScriptControl_TLB, который мы и будем впоследствии использовать.
Итак, первый этап сделан. Прямо сейчас мы можем создать этот компонент в приложении, поместить в него скрипт и вызвать его(за это ответственны такие функции как AddCode, Run и ряд остальных менее важных). Но нам этого мало! Поэтому идем дальше.
На следующем этапе нам необходимо создать объект автоматизации приложения. Для этого идем в New – Other – ActiveX – Automation Object. Я имя СоКласса выбрал HelloWorldDemo.

Все. Каркас сделан, теперь добавляем функциональность. Для начала я на главной форме нашего приложения я сделал один TButton и один TEdit. Потом в интерфейс IHelloWorldDemo добавил свойство StrValue. Свойство сделаем Read-Write, соответственно, для чтения и для записи значения этого свойства у нас будет два разных метода. Сделаем так, чтобы значение этого свойства было связано со значением строки ввода Edit.Text. Сначала в атрибутах свойства Type меняем long на VARIANT. Все остальное сделается автоматом (хотя при добавлении методов было бы чуть посложнее). Потом на панели нажимаем на кнопку Обновить, переходим в модуль, где находится implementation этого интерфейса, и добавляем туда следующее:


function THelloWorldDemo.Get_StrValue: OleVariant;
begin
  Result := fmMain.edParam.Text;
end;  

procedure THelloWorldDemo.Set_StrValue(Value: OleVariant);
begin
  fmMain.edParam.Text := Value;
end;

Таким образом, при чтении/изменении значения свойства StrValue у нас будет считываться/изменяться свойство Text компонента edParam.
Ну а теперь самое сложное. Нужно считать скрипт в память и вызвать его, передав в качестве входного параметра интерфейс объекта автоматизации нашего приложения. Поехали.


function TfmMain.RunScript(FunctionName: string): Variant;
var ScriptControl: TScriptControl;
  ScriptCode: TStringList;
  VParams, AutoObject, VarParam: Variant;
  rgsabound: array[0..0] of SAFEARRAYBOUND;
  Params: PSafeArray;
  Dims: array[0..0] of Integer;
  i: Integer;
begin
  ScriptControl := TScriptControl.Create(nil);
  try
    ScriptControl.Language := 'JScript';
    ScriptCode := TStringList.Create;
    try
      // Считываем скрипт из файла в компонент ScriptControl
      ScriptCode.LoadFromFile('ScriptControlDemo.js');
      ScriptControl.Reset;
      ScriptControl.AddCode(ScriptCode.Text);
      // Создаем наш объект автоматизации
      AutoObject := CreateOleObject('ScriptControlInterface.HelloWorldDemo');
      // Формируем массив параметров по стандартам OLE
      // Процедура универсальная, должна подойти для любого количества параметров
      VParams := VarArrayCreate([0,0], vtVariant);  // Вот здесь
      try
        VParams[0] := AutoObject;                   // и здесь надо изменять, если надо больше параметров
        rgsabound[0].lLbound := 0;
        rgsabound[0].cElements := VarArrayHighBound(VParams, 1) - VarArrayLowBound(VParams, 1) + 1;
        Params := SafeArrayCreate(VT_VARIANT, 1, rgsabound);
        try
          for i := 0 to VarArrayHighBound(VParams, 1) - VarArrayLowBound(VParams, 1) do begin
            Dims[0] := i;
            VarParam := VParams[і];
            SafeArrayPutElement(Params, Dims, VarParam);
          end;
          // Запускаем!
          Result := ScriptControl.Run(FunctionName, Params)
        finally
          VarParam := Unassigned;
          SafeArrayDestroy(Params);
        end;
      finally
        AutoObject := Unassigned;
        VParams := Unassigned;
      end;
    finally
      ScriptCode.Free;
    end;
  finally
    ScriptControl.Free;
  end;
end;

Ну а теперь вешаем обработчик OnClick на кнопку.
procedure TfmMain.btnHelloClick(Sender: TObject);


procedure TfmMain.btnHelloClick(Sender: TObject);
begin
  if not RunScript('TransformStr') then
    Application.MessageBox('Щось пішло не так :(', PChar(Application.Title));
end;

Без лишних слов привожу код файла ScriptControlDemo.js. Он всего лишь инвертирует строку.


function TransformStr (ScriptDemoInstance) {
  InStr = ScriptDemoInstance.StrValue;	// Считываем значение свойства
  OutStr = "";
  for (i = InStr.length-1; i >= 0; i--) OutStr += InStr.charAt(i); // Инвертируем строку
  ScriptDemoInstance.StrValue = OutStr;	// Записываем значение свойства
  return true;				// Ну вроде все нормально
}

Итак, что же мы только что сделали?

  1. Создали и зарегистрировали объект автоматизации нашего приложения (он регистрируется при первом запуске программы)
  2. Считали в Script Control извне подпрограмму, предоставили интерфейс нашего обьекта автоматизации
  3. Запустили данную подпрограмму
  4. Подпрограмма, обращаясь к API основной программы (запросив строку), выполнила нужные действия (инвертировала ее) и возвратила результат (обработанную строку)
  5. Ну, а программа потом пошла работать дальше, имея у себя результат работы скрипта

Все работает, все довольны. Конечно же, приведенный пример абсолютно непрактичен и носит чисто показательный характер. Но возможности для фантазии безграничны.

Ну и по окончанию стоит сделать несколько замечаний относительно данного метода.

  • Несмотря на простоту приведенного примера, мы добились полного двустороннего взаимодействия нашего приложения с его «выносной» частью – скриптом (которую, кстати, можно модифицировать даже в Run-Time)
  • Данным методом вполне можно обеспечить функциональность метапрограммирования (когда программа сама генерирует свой кусок кода, и впоследствии его выполняет)
  • Скриптовый язык может быть любым, который поддерживается Windows Script Control (в поставке Windows это JScript и VBScrpt, но сторонними производителями разработаны интерпретаторы языков Perl, Python, PHP, TCL, Lua и многих других)
  • Покопавшись в MSDN, можно обнаружить, что на самом деле возможности данного метода намного шире (например, можно обеспечить собственный обработчик ошибок скрипта, и т.д.)
  • Реализовав соответствующим образом объект автоматизации в DLL, можно из скрипта вызывать API нашей программы даже в тот момент, когда она не загружена, нужно всего лишь создать этот объект из самого скрипта… Но это уже тема отдельного разговора.

Для того, кто хочет более детально ознакомится с кодом и результатом его работы, я выложил архив вот здесь. Надеюсь, кому-то вышеизложенное окажется полезным.

З.Ы. Не бейте сильно ногами. Это моя первая статья ;)

google.com bobrdobr.ru del.icio.us technorati.com linkstore.ru news2.ru rumarkz.ru memori.ru moemesto.ru

5 Комментариев на “Практика написания расширяемых приложений. Пишем скрипты для… своих программ.”

  1. usix сказал:

    Вот это просто? :eek:
    Лет десять назад может оно так и было, но на 7-го июля 2007-го года информация немного устарела... :idea:
    Тот же Iron Python одной строчкой запускается :cry:

  2. Stalker сказал:

    В статье говорится немного о другом. Я не говорил ничего ни о платформе CLR, ни о реализации дополнительных языков под нее. А скорее о том, как без дополнительных компонент создать расширяемое скриптами Win32-приложение.

  3. usix сказал:

    Сорри, это я под конец рабочей недели бешусь :)
    Если по существу, то сказав А, надо говорить и Б. Мне кажется, что, с точки зрения архитектуры, красивей будет выглядеть решение, в котором скрипт так же является полноценным COM-объектом. На том же перле такое запросто можно реализовать.

  4. zeroreturn сказал:

    из "замечаний" в конце статьи последнее очень дельное, только без каркаса, который бы упрощал создание подобных адаптеров не обойтись --- слишком много boilerplate кода.

    интересно какова стоимость такого решения по скорости выполнения.

  5. Stalker сказал:

    На самом деле, создать COM-объект из скрипта гораздо проще - основная часть функции RunScript заменится 2-3 строчками скрипта. Но в таком случае идеология взаимодействия скрипта с приложением была бы другой (здесь я старался максимально их друг с другом увязать, чтобы скрипт выполнял роль "плагина").

    Насчет скорости - процедура RunScript вполне может сойти за шаблонную (хотя я не претендую на идеальность кода), если нет желания вникать в суть COM-взаимодействий. Ну а процесс наращивания функционала, имея каркас, сильно по скорости не будет отличаться, зато гибкость увеличится на несколько порядков.





Оставте свое мнение