понедельник, 22 апреля 2013 г.

Использование Lua скриптов в составе ПО

Представьте себе ситуацию, ваше приложение реализует достаточно большой функционал для пользователя и он им пользуется достаточно успешно на протяжении достаточно большого периода времени. Но вот наступает момент, когда этого функционала пользователю становится недостаточно и он начинает просить немного расширить возможности ПО.

Выливается это обычно в перекомпиляцию проекта под новые требования.
Правда обычно проект в релизной стадии находится крайне редко, обычно он разобран. Нужно срочно собирать все хвосты и подчищать все нюансы, чтобы выкатить новый релиз заказчику. (Вариант с откатом на старый бранч - не рассматриваю :)
А если проект находится под защитой, помимо этого потребуется перевыписать лицензии на него (т.к. контрольные суммы исполняемых файлов изменились) и прочее-прочее.
Однако, если предусмотреть вариант расширения функционала ПО на базе уже имеющихся возможностей, вполне можно избежать лишних телодвижений.

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

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

Да в принципе можно взять и более приземленные темы (по крайней мере для меня).
К примеру возьмем те же отладчики. Практически в каждом из них есть собственный скриптовый язык, помогающий реверсеру выполнять большинство рутинных действий.
Существуют даже хранилища подобных скриптов, доступные в онлайне, или распространяемые в виде больших паков. По сути это утилитарные подпрограммы.

Вот что-то подобное мы и рассмотрим в данной статье.

В качестве основы скрипта я возьму интерпретируемый язык программирования "Lua".
Данный язык очень часто встречается в том инструментарии, который я использую в повседневной работе, поэтому его выбор для статьи был для меня весьма очевиден.




1. Реализуем управляющий класс


Дабы не подготавливать очередной пример кода, который будет использоваться для повествования, я возьму уже известный моим читателям пример из третьей части статьи: "Изучаем отладчик"
Вкратце напомню суть данного кода (если кому лень читать прошлую статью целиком): задача была обойти защиту приложения crackme.exe. Решалась эта задача посредством использования отладчика перехватывающего обработчик кнопки и выставляющего правильный флаг, на который и реагировала команда перехода JE.

Скачайте демопример к ней и посмотрите на реализацию DegugCoreTest.
Достаточно много кода, а ведь по сути нам нужно было выполнить всего лишь несколько действий, которые можно изобразить в виде вот такого скрипта:
-- закрывать процесс после завершения работы лоадера не будем
CloseDebugProcessOnFree = false;
-- инициализируем отладчик
InitProcess(true);
-- прячем отладчик
HideDebuger();
-- запускаем процесс
Run();
-- мы на точке входа, ставим бряк на обработчик кнопки
SetHardwareBreakpoint(0x467840, hsByte, hmExecute, 0);
-- запускаем процесс
Run();
-- мы на обработчике кнопки, говорим что все нормально
SetFlag(EFLAGS_ZF, true);
-- запускаем процесс и завершаем работу лоадера
StopScript();
Если бы у нас была возможность выполнить такой скрипт мы бы выполнили все условия задачи из оригинальной статьи всего за семь команд!
Более того, если перекомпилировать оригинальное приложение crackme.exe, изменится адрес обработчика кнопки, что приведет к неработоспособности лоадера.
В случае самостоятельно реализации лоадера, показанного в статье, нам пришлось бы перекомпилировать и его с целью исправить данный адрес на валидный, а в скрипте нам потребуется просто изменить только одну строчку.

Понравилась идея?
Тогда попробуем это реализовать :)

Для этого необходимо реализовать следующую архитектуру:
1. Непосредственно сам отладчик, которым будем управлять.
2. Класс, посредством которого происходит управление
3. Модуль обеспечивающий связку управляющего класса со скриптом
4. Сам скрипт
5. Утилитарные модули

Пункт первый у нас есть (это отладчик из прошлой статьи), а сам скрипт я показал чуть выше.

Как будет реализован второй пункт:
1. Данному классу нужно знать об отладчике и уметь с ним работать.
2. Данный класс должен предоставлять необходимый перечень функций, которые мы можем вызывать из скрипта.

Вкратце выглядеть он будет таким вот образом:
type
  TScriptProvider = class
  private
    // переменные, необходимые для работы класса
    FDebuger: TFWDebugerCore;
    FProcessPath: string;
    FBreakOnEP: Boolean;
    FCurrentThreadIndex: Integer;
    FStopScript: Boolean;
  protected
    // обработчики отладчика
    procedure OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
      ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
      var ReleaseBreakpoint: Boolean);
    procedure OnHardwareBreakPoint(Sender: TObject; ThreadIndex: Integer;
      ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: THWBPIndex;
      var ReleaseBreakpoint: Boolean);
    procedure OnUnknownBreakPoint(Sender: TObject; ThreadIndex: Integer;
      ExceptionRecord: Windows.TExceptionRecord);
    procedure OnIDLE(Sender: TObject);
  public
    // основной метод, через который запускается скрипт
    procedure ExecuteScript(Debuger: TFWDebugerCore; const ExePath, ScriptPath: string);
  public
    // функции и процедуры доступные скрипту
    procedure InitProcess(BreakOnEP: Boolean);
    function GetProcess: THandle;
    function Run: Boolean;
    procedure SetHardwareBreakpoint(Address: Pointer;
      Size: THWBPSize; Mode: THWBPMode; HWIndex: THWBPIndex);
    procedure SetFlag(Flag: DWORD; Value: Boolean);
    procedure StopScript;
  end;
Работа с отладчиком реализуется через перекрытие его событий.
Для описанного выше скрипта достаточно перекрыть всего три события:
OnBreakPoint и OnHardwareBreakPoint - останавливают выполнение функции Run при достижении точки остановки.
OnIDLE - для возможности работы функции StopScript, корректно завершающую работу отладчика.

В реальности сам класс предоставляет для работы всего лишь один единственный метод: ExecuteScript, в который передается созданый экземпляр отладчика, путь к отлаживаемому приложению и путь к файлу со скриптом.

Остальные публичные методы предназначены для вызова из самого скрипта.
Более подробно методы работы с отладчиком, реализованным в виде класса TFWDebugerCore, были рассмотрены во второй части статьи "Изучаем отладчик", поэтому заострять на них внимание я не буду.

Рассмотрим доступные скрипту функции по порядку:
procedure TScriptProvider.InitProcess(BreakOnEP: Boolean);
begin
  FBreakOnEP := BreakOnEP;
  if FDebuger.DebugProcessData.ProcessID = 0 then
    if not FDebuger.DebugNewProcess(FProcessPath, True) then
      RaiseLastOSError;
  FDebuger.CloseDebugProcessOnFree := luaCloseDebugProcessOnFree;
end;
Запускает отладку нового процесса вызовом DebugNewProcess и выставляет флаг, ориентируясь на который принимается решение, завершать отлаживаемый процесс при закрытии отладчика или нет.
Параметр BreakOnEP указывает отладчику нужно ли останавливаться на точке входа в приложение или нет. Он будет использоваться в функции Run, поэтому пока мы его просто запомним в соответствующей переменной класса TScriptProvider.
function TScriptProvider.Run: Boolean;
begin
  FDebuger.RunMainLoop;
  Result := FDebuger.DebugProcessData.AttachedFileHandle <> 0;
end;
Запускает отладочный цикл.
После окончания работы отладочного цикла проверяет, доступно ли еще приложение или нет, ориентируясь на поле DebugProcessData.AttachedFileHandle отладчика.

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

Логика функции Run проста, она выполняет отладочный цикл до тех пор, пока не произошла остановка на любой точке остановки, размещенной непосредственно нами.
Для этого используются события:
procedure TScriptProvider.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
  var ReleaseBreakpoint: Boolean);
begin
  FCurrentThreadIndex := ThreadIndex;
  if not FBreakOnEP then
  begin
    ReleaseBreakpoint := True;
    if ExceptionRecord.ExceptionAddress =
      Pointer(FDebuger.DebugProcessData.EntryPoint) then Exit;
  end;
  FDebuger.AbortMainLoop;
end;
и
procedure TScriptProvider.OnHardwareBreakPoint(Sender: TObject;
  ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord;
  BreakPointIndex: THWBPIndex; var ReleaseBreakpoint: Boolean);
begin
  FCurrentThreadIndex := ThreadIndex;
  FDebuger.AbortMainLoop;
end;
Логика этих двух обработчиков событий тривиальна, просто запоминается переменная ThreadIndex во внутреннем поле (т.к. наш скрипт ничего о ней не знает) и происходит остановка отладочного цикла вызовом AbortMainLoop.
Ну и в обработчике OnBreakPoint происходит обработка ранее запомненного флага BreakOnEP. Если произошло прерывание на точке входа, а в скрипте сказано что на нем останавливаться не нужно, производиться выход из обработчика не вызывая AbortMainLoop.

Второй нюанс функции Run связан с проверкой наличия приложения, проверкой параметра AttachedFileHandle. Это необходимо для того чтобы вовремя прекратить работу скрипта, если мы закрыли отлаживаемое приложение руками, а наш лоадер все еще работает.
procedure TScriptProvider.SetHardwareBreakpoint(Address: Pointer;
  Size: THWBPSize; Mode: THWBPMode; HWIndex: THWBPIndex);
begin
  FDebuger.SetHardwareBreakpoint(FCurrentThreadIndex, Address, Size, Mode, HWIndex, '');
end;
Производит установку HBP ориентируясь на переданные из скрипта параметры.
procedure TScriptProvider.SetFlag(Flag: DWORD; Value: Boolean);
begin
  FDebuger.SetFlag(FCurrentThreadIndex, Flag, Value);
end;
Устанавливает указанный в скрипте флаг процессора в требуемое состояние.
procedure TScriptProvider.StopScript;
begin
  FStopScript := True;
  Run;
end;
Завершает работу отладчика, инициализируя флаг FStopScript, на который ориентируется обработчик OnIdle.
procedure TScriptProvider.OnIDLE(Sender: TObject);
begin
  if FStopScript then
    FDebuger.StopDebug;
end;
Ну вот собственно и все - класс для управления отладчиком готов.
Он очень простой и очень легко расширяемый.
Для удобства доступа к данному классу реализован синглтон:
function ScriptProvider: TScriptProvider;

2. Подключаем шлюз


Теперь нужно реализовать механизм, интерпретирующий команды скрипта и управляющий данным классом.

Скриптовый движок, конечно, с нуля писать не нужно.
Как я и сказал ранее, мы рассматриваем только вариант Lua.
Использовать его можно как в нативном варианте (используя напрямую соответствующие API библиотеки lua.dll), так и посредством шлюзовых оберток.
Ради интереса я даже пощупал нативный вариант..., но времени на изучение всех вкусностей не хватило :)

Выбирая альтернативные варианты, я наткнулся на одну интересную разработку нашего программиста Дмитрия Мозулева aka DevilDevil, именуемую CrystalLUA.
http://www.gamedev.ru/projects/forum/?id=140784
Она обеспечивает удобный для Delphi программиста шлюз между привычным нам кодом (забирая на себя всю возню с нативным API Lua) и конечным скриптом.
Ближайший аналог такого шлюза, который можно привести это VCL и WinAPI.

У нее конечно есть недостатки (ну а у кого их нет?), но достоинств, если честно за глаза :)

Впрочем будем последовательны:
Сначала посмотрим как с ней работать, а потому уже будем искать её плюсы и минусы.

Итак, для подключения данной библиотеки достаточно объявить в uses модуль CrystalLUA.
Доступ к экземпляру TLua необходимо реализовать самостоятельно (как сделано в примере ), либо объявив директиву LUA_INITIALIZE.
В статье рассматривается первый вариант через синглтон:
function LuaScript: TLua;
Таким образом программисту не нужно отвлекаться на создание и удаление данного объекта.

Пишем код.
Задача данного кода описать те функции которые может вызвать скрипт.
Их всего шесть:
procedure luaInitProcess(const Arg: TLuaArg);
begin
  ScriptProvider.InitProcess(Arg.AsBoolean);
end;

procedure luaHideDebuger;
begin
  HideDebuger(ScriptProvider.GetProcess);
end;

function luaRun(const Arg: TLuaArg): TLuaArg;
begin
  Result.AsBoolean := ScriptProvider.Run;
end;

procedure luaSetHardwareBreakpoint(const Args: TLuaArgs);
begin
  ScriptProvider.SetHardwareBreakpoint(
    Pointer(Args[0].AsInteger),
    THWBPSize(Args[1].AsInteger),
    THWBPMode(Args[2].AsInteger),
    THWBPIndex(Args[3].AsInteger));
end;

procedure luaSetFlag(const Args: TLuaArgs);
begin
  ScriptProvider.SetFlag(
    Args[0].AsInteger,
    Args[1].AsBoolean);
end;

procedure luaStopScript;
begin
  ScriptProvider.StopScript;
end;
Код крайне прост, все функции выступают шлюзом между скриптом и вызовом класса, управляющего отладчиком, представленного в виде ScriptProvider.
За исключением luaHideDebuger, она вызывает процедуру HideDebuger не реализованную в ScriptProvider, но в ней ничего особого нет, данную функцию я описывал ранее в третьей части статьи "Изучаем отладчик".

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

Также объявим переменную, значение которой можно будет модифицировать из скрипта:
var
  luaCloseDebugProcessOnFree: Boolean;

Она объявлена просто для демонстрации возможностей CristalLUA и используется для указания отладчику, что делать после завершения работы (оставлять отлаживаемый процесс работающим или закрыть его).

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

Выполняется это следующим кодом:
initialization
  LuaScript.RegVariable('CloseDebugProcessOnFree', luaCloseDebugProcessOnFree, TypeInfo(Boolean));
  LuaScript.RegProc('InitProcess', @luaInitProcess, 1);
  LuaScript.RegProc('HideDebuger', @luaHideDebuger);
  LuaScript.RegProc('Run', @luaRun);
  LuaScript.RegEnum(TypeInfo(THWBPSize));
  LuaScript.RegEnum(TypeInfo(THWBPMode));
  LuaScript.RegProc('SetHardwareBreakpoint', @luaSetHardwareBreakpoint, 4);
  LuaScript.RegConst('EFLAGS_ZF', EFLAGS_ZF);
  LuaScript.RegProc('SetFlag', @luaSetFlag, 2);
  LuaScript.RegProc('StopScript', @luaStopScript);
По порядку.

Lua.RegVariable - регистрирует переменную, значение которой можно изменять как из самой программы, так и из скрипта (и соответственно считывать ее значение).

Lua.RegProc - регистрируем в движке процедуру или функцию, с указанием ее наименования, доступного в скрипте, ее адреса (это необходимо для движка CristalLUA)  и указываем количество ее параметров, дабы движок CristalLUA правильно смог работать со стеком при вызове данных процедур.
Здесь правда есть нюанс. В реальности количество параметров можно вообще не указывать, данный параметр необходим для автоматического отслеживания ошибок движком CristalLUA. В частности количество параметров можно не указывать в случае использования процедур и функций со всеми параметрами по умолчанию.
Если их передаваемое и принимаемое количество не совпадут, получим ошибку от движка (ну или AV если все сделали неправильно :).

Lua.RegConst - аналогично RegVariable, но есть нюанс, в языке Lua нет такого понятия как константа - все есть переменные. Но CristalLUA берет эту работу на себя отслеживая изменения переменных, объявленных как константа, и выдает соответствующую ошибку при попытке их модификации.

А теперь к интересным моментам:
Lua.RegEnum - регистрация перечислимых типов, объявленных в Delphi коде.

Обратите внимание на эту строчку скрипта:
SetHardwareBreakpoint(0x467840, hsByte, hmExecute, 0);

Я могу свободно использовать параметры hsByte и hmExecute и их корректно опознает движок CristalLUA т.к. при регистрации данных перечислимых типов он пробежался по перечислению через RTTI и знает о всех возможных значениях.

После всего этого мы имеем на руках готовый проект, который можно запустить выполнив следующий код:
var
  Debuger: TFWDebugerCore;
begin
  try
    Debuger := TFWDebugerCore.Create(100);
    try
      ScriptProvider.ExecuteScript(Debuger,
        ExtractFilePath(ParamStr(0)) + 'crackme\crackme.exe',
        ExtractFilePath(ParamStr(0)) + 'crackme.lua');
    finally
      Debuger.Free;
    end;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.
В результате мы получим полный аналог работы кода из третьей части статьи "Изучаем Отладчик".

Теперь нюансик с функцией Run.
Как я и говорил ранее, она возвращает булевое значение.
В случае ели вы запустите пример и не будете нажимать на кнопку, а просто закроете отлаживаемый процесс, то будет произведен выход из данной функции в скрипте.
Но так как скрипт линеен, он попробует выполнится дальше, т.е. будет выполнена строчка:
SetFlag(EFLAGS_ZF, true);

Дабы такого не произошло можно немного видозменить концовку скрипта следующим образом:
-- запускаем процесс
if not Run() then 
 return; 
end;
-- мы на обработчике кнопки, говорим что все нормально
SetFlag(EFLAGS_ZF, true);
-- запускаем процесс и завершаем работу лоадера
StopScript();

3. Подключаем обработчики событий


А теперь рассмотрим пример, описываемый во второй части статьи "Изучаем Отладчик", а именно:
procedure TForm1.Button1Click(Sender: TObject);
begin
 try
    asm
      int 3
    end;
    ShowMessage('Debuger detected.');
  except
    ShowMessage('Debuger not found.');
  end;
end;
Как вы помните, суть его сводилась к тому, что генерировалось отладочное исключение посредством вызова третьего прерывания (INT 3) и в случае присутствия отладчика, оно "проглатывалось" не давая коду перейти в обработчик except.

Данная ситуация решалась вот таким кодом:
procedure TdlgDebuger.OnUnknownBreakPoint(Sender: TObject;
  ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord);
var
  ApplicationBP: Boolean;
begin
  ApplicationBP :=
    (DWORD(ExceptionRecord.ExceptionAddress) > FCore.DebugProcessData.EntryPoint) and
    (DWORD(ExceptionRecord.ExceptionAddress) < $500000);
 
  Writeln;
  if ApplicationBP then
  begin
    Writeln(Format('!!! --> Unknown application breakpoint at addr 0X%p',
      [ExceptionRecord.ExceptionAddress]));
    Writeln('!!! --> Exception not handled.');
    FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED;
  end
  else
  begin
    Writeln(Format('!!! --> Unknown breakpoint at addr 0X%p',
      [ExceptionRecord.ExceptionAddress]));
    Writeln('!!! --> Exception handled.');
    FCore.ContinueStatus := DBG_CONTINUE;
  end;
  Writeln;
end;
Т.е. при получении неопознанного исключения, в случае, если оно располагалось в пределах секции памяти, в которой хранится образ отлаживаемого приложения, выставлялся флаг DBG_EXCEPTION_NOT_HANDLED.
Таким образом данное исключение не обрабатывалось отладчиком и происходил переход в обработчик исключения непосредственно в отлаживаемом процессе.

А теперь, как можно смоделировать данный код на базе LUA скрипта:
Показанный выше скрипт был полностью линейный и он ничего не знал о событийной модели исполнения кода.
Но мы можем это исправить попробовав реализовать вот такой скрипт:
function OnUnknownBreakPoint(ExceptionRecord)
  DEBUG = ExceptionRecord.ExceptionAddress;
  if (ExceptionRecord.ExceptionAddress > 0x400000) and (ExceptionRecord.ExceptionAddress < 0x500000) then
 ContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
  else
    ContinueStatus = DBG_CONTINUE
  end;  
end;

CloseDebugProcessOnFree = true;
-- инициализируем отладчик
InitProcess(false);
-- запускаем процесс
Run();
Т.е. мы в скрипте объявим функцию, которая будет вызываться нашим лоадером при получении некоего события (в данном случае OnUnknownBreakPoint).

Здесь есть нюанс:
Функции и процедуры в скрипте всегда должны идти первыми, линейный код, исполняемый скриптом, в самом конце (таковы требования языка).

Функции InitProcess и Run нами уже были реализованы в прошлой главе, осталось научится делать вызов функции OnUnknownBreakPoint из приложения, передавая управление скрипту.

Для этого необходимо модифицировать код примерно таким образом:
procedure TScriptProvider.ExecuteScript(Debuger: TFWDebugerCore;
  const ExePath, ScriptPath: string);
begin
  ...
  FDebuger.OnUnknownBreakPoint := OnUnknownBreakPoint;
  ...
end;

procedure TScriptProvider.OnUnknownBreakPoint(Sender: TObject;
  ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord);
var
  Args: TLuaArgs;
begin
  if not Lua.ProcExists('OnUnknownBreakPoint') then Exit;
  SetLength(Args, 1);
  Args[0].AsRecord := LuaRecord(@ExceptionRecord, Lua.RecordInfo['TExceptionRecord']);
  LuaScript.Call('OnUnknownBreakPoint', Args);
  FDebuger.ContinueStatus := luaContinueStatus;
end;
Т.е. грубо перекрываем обработчик OnUnknownBreakPoint у самого отладчика, а потом смотрим, есть ли такой в самом скрипте, если есть, вызываем его, передавая структуру ExceptionRecord в виде параметра.
После вызова обработчика в скрипте, выставляем ContinueStatus измененный также в скрипте (извиняюсь за тафталогию :)

Конечно передача параметров в обработчик может показаться не привычной практически всем, кто ранее не сталкивался с CristalLUA, но к этому можно быстро привыкнуть.
(Тем более о такой непривычности я не раз упоминал, когда используешь сторонние фреймворки, взять тот же TcxBarEditItem, описанный в статье "Нюансы использования Ribbon от DevExpress").

Впрочем по поводу непривычности, сейчас покажу еще несколько моментов.
В скрипте появились две новые константы и переменная, которые мы должны зарегистрировать.
С ними все просто:
  LuaScript.RegConst('DBG_EXCEPTION_NOT_HANDLED', DBG_EXCEPTION_NOT_HANDLED);
  LuaScript.RegConst('DBG_CONTINUE', DBG_CONTINUE);
  LuaScript.RegVariable('ContinueStatus', luaContinueStatus, TypeInfo(DWORD));
Но помимо них в скрипт передается еще и структура ExceptionRecord с полями, одно из которых (ExceptionAddress) зачитывается, а вот с этим немного сложнее.

Регистрация структур выглядит следующим образом:
  Info := LuaScript.RegRecord('TExceptionRecord', Pointer(SizeOf(Windows.TExceptionRecord)));
  with TExceptionRecord(nil^) do
  begin
    Info.RegField('ExceptionAddress', @ExceptionAddress, TypeInfo(DWORD));
  end;
Здесь производится регистрация только одного поля, которое нам необходимо в скрипте, а если нужны все, код конечно немного увеличится.
Здесь сказывается то, что RTTI для структур не генерируется в ранних версиях дельфи, а CristalLUA пока что заточена на Delphi 2007 и ниже.
Но автор сейчас ведет работу над доработкой библиотеки под старшие версии Delphi (где RTTI для записей присутствует) и я думаю вскорости появится перекрытый вариант RegRecord, не требующий дополнительной регистрации полей структуры через RegField.

4. Итоги


Теперь остановимся на том, нужна ли вообще CristalLUA и что она дает разработчику?
Ответ прост - нужна и я вам покажу почему.

Вот вам регистрация ExceptionRecord без использования CristalLUA:
function TExceptionRecord__index(L: Plua_State): Integer; cdecl;
var
  Identifier: string;
  userdata: pointer;
begin
  Result := 1;
  userdata := lua_touserdata(L, 1);
  if (userdata = nil) then
  begin
    lua_pushstring(L, 'Wrong TExceptionRecord usage');
    lua_error(L);
  end;
  userdata := Pointer(userdata^);

  Identifier := lua_tostring(L, 2);
  if (Identifier = 'ExceptionAddress') then
  begin
    lua_pushnumber(L, DWORD(Windows.PExceptionRecord(userdata).ExceptionAddress));
  end else
  begin
    lua_pushstring(L, PChar(Format(
      'Identifier "%s" not found in TExceptionRecord instance', [Identifier])));
    lua_error(L);
  end;
end;

function TExceptionRecord__newindex(L: Plua_State): Integer; cdecl;
var
  Identifier: string;
  userdata: pointer;
begin
  Result := 0;
  userdata := lua_touserdata(L, 1);
  if (userdata = nil) then
  begin
    lua_pushstring(L, 'Wrong TExceptionRecord usage');
    lua_error(L);
  end;
  userdata := Pointer(userdata^);

  Identifier := lua_tostring(L, 2);
  if (Identifier = 'ExceptionAddress') then
  begin
    DWORD(Windows.PExceptionRecord(userdata).ExceptionAddress) := Trunc(lua_tonumber(L, 3));
  end else
  begin
    lua_pushstring(L, PChar(Format(
      'Identifier "%s" not found in TExceptionRecord instance', [Identifier])));
    lua_error(L);
  end;
end;

...


  lua_createtable(FScript, 0, 0);
  lua_pushvalue(FScript, 1);
    lua_pushstring(FScript, '__index');
    lua_pushcfunction(FScript, @TExceptionRecord__index);
    lua_rawset(FScript, -3);
    lua_pushstring(FScript, '__newindex');
    lua_pushcfunction(FScript, @TExceptionRecord__newindex);
    lua_rawset(FScript, -3);
  lua_setmetatable(FScript, 2);
  lua_setfield(FScript, LUA_GLOBALSINDEX, 'TExceptionRecord');
Я думаю разница очевидна :)

Ну а если не впечатлились, то вот вам самый первый пример из справки по CristalLUA:
procedure TForm1.Button1Click(Sender: TObject);
begin
  Lua.RegClass(TForm1);
  Lua.RegVariable('Form1', Form1, typeinfo(TForm1));
  Lua.RunScript(
    'Form1 {Caption="Text", Position=poScreenCenter, Color=0x007F7F7F}'); // сразу несколько свойств
end;
Я думаю здесь сразу понятно, сколько работы производится в "скрытом от разработчика" режиме, по регистрации всего RTTI от формы, чтобы потом можно было вот так легко взять и поменять любой понравившийся параметр из скрипта :)

5. Плюсы и минусы


Я описал только часть плюсов, затронув только малую часть верхушки айсберга.
По функционалу библиотеки и ее возможностям на самом деле можно написать приличный такой букварь.
Самый огромный плюс - это саппорт, например когда мне потребовался некий функционал из Lua 5.2 автор выкатил его мне буквально за сутки, хотя я думал что у него на это уйдет примерно неделя.

Ну а теперь к минусам.

Во первых CristalLUA пока что заточен исключительно под ANSI варианты дельфи (2007 и ниже). Это очень большой минус, но ведется работа...

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

В третьих библиотека не совсем кроссплатформенная (в пределах битности) из-за обилия асмкода. Это конечно оправдано в некоторых вещах, да и по существу, проанализировав код, я не скажу что в данном случае это был неверный подход, хотя и есть перегибы.
Но в принципе 90 процентов кода Delphi программистами пишутся пока что под 32 бита и под Win.

Правда есть небольшой нюанс, для 32 битных ОС не на базе Windows добавить поддержку будет достаточно просто, достаточно убрать завязку на Win32 Lua API.

6. Ну и в заключение.


Я надеюсь что по минимуму справился с поставленной задачей и заинтересовал вас в данной разработке. Такие вещи обычно пишутся командами из "много человек", взять тот-же паскаль-скрипт.

В коде движка CristalLUA я вижу очень профессиональную работу, но, к сожалению, всего лишь одного энтузиаста.

Правда, очень квалифицированного энтузиаста, но даже он не сможет объять необъятное и решить все насущные задачи.

Если вы заинтересовались в данной разработке и имеете свободное время, и желание помочь в развитии данного проекта - то милости просим:

Контакты автора библиотеки CristalLUA:

ICQ: 250481638
email: softforyou@inbox.ru

Исходный код демопримеров можно забрать здесь.

Удачи.

---

Александр (Rouse_) Багель
Апрель, 2013

Комментариев нет:

Отправить комментарий