Множественная обработка событий Delphi
События компонентов Delphi являются достаточно удобным и эффективным механизмом взаимодействия с компонентами,
однако, в отличие от популярных сейчас сред они не поддерживают никаких "встроенных" средств для множественной их
(событий) обработки.
Обработчик события Delphi является структурой, определенной в модуле SysUtils:
TMethod = record
Code, Data: Pointer;
end;
, где Data - указатель на объект-обработчик; Code - указатель на метод объекта обработчика.
Вызов события, условно, выглядит как
... Формирование параметров в стеке и регистрах ...
MOV EAX, Method.Data
CALL Method.Code
т. е. типовой вызов метода Delphi.
Типизированные события типа TNotifyEvent и прочих определяют структуру параметров вызова и позволяют делать проверки
совместимости типов в время компиляции. В то же время структура для хранения события остается одной и той же для любого
типа события и не зависит от параметров. Никто не мешает самостоятельно обработать события по-другому, но при этом вся
ответственность ложится на реализацию. Например, можно обработать событие не при помощи метода объекта, а при помощи
простой процедуры - в этом случае процедура должна иметь на одни параметр больше чем метод: это self объекта-обработчика,
являющийся неявным параметром методов и передаваемый в EAX.
//EAX = self = Data
procedure DoClick(Data: Pointer; Sender: TObject);
begin
ShowMessage('OnClick call');
end;
procedure TForm1.FormCreate(Sender: TObject);
var
M: TMethod;
begin
M.Code := @DoClick;
//self объекта-обработчика равен nil
M.Data := nil;
//Здесь событие назначится и контроля типов не будет
self.OnClick := TNotifyEvent(M);
end;
Как компонент может организовать возможность множественной обработки событий
Теперь представим себе множественную обработку событий. Для этого, вместо одного обработчика (TMethod), вызывающий компонент
должен, по идее, держать список типа array of TMethod и вызывать события по порядку. Учитывая типизацию это будут разнотипные
массивы: array of TNotifyEvent, array of TDataSetEvent... Очевидно, что такой подход может повлиять и на обработку результатов,
возвращенных обработчиками, ведь они вызываются последовательно, и вобщем-то "делят" общие параметры. Так же невозможно себе
представить, что событие останется простым свойством. Обработчик события надо иметь возможность добавлять и убирать. Здесь, помогла бы
перегрузка операций по типу:
self.OnClick := self.OnClick + MyEventHandler;
что, в общем, и практикуется в других средах, но для Delphi не самых последних версий :), это не свойственно. Поэтому логично вводить
процедуры регистрации и разрегистрации обработчиков, по типу:
function RegOnClick(AEvent: TNotifyEvent; After: boolean = true): Integer;
function UnRegOnClick(AEvent: TNotifyEvent): Integer;
(поскольку, мы говорм о списке, то имеет смысл указать куда добавить обработчик события - в начало, или конец списка).
Однако, придется, вероятно, вынести в интерфейс еще и метод вызова события (точнее списка) - ведь обычные события можно вызвать не только из
кода компонента, но из-вне его. Кроме того, связи стандартных событий сохраняются в ресурсах формы, а в данном случае, что-бы что-то сохранилось,
надо представить события сериализуемой форме, например, привязанными к элементам коллекции. Так может оказаться, что вместо типовых обработчиков,
компонент будет иметь свойства-коллекции по каждое событие, в элементах которых и будут храниться обработчики. Правда такая конструкция выглядит
несколько тяжеловесной.
Подытоживая, можно сказать, что для реализации множественной обработки нужен список, возможно, в виде сериализуемого объекта, который бы позволял
добавлять и убирать обработчики, а так же вызывать обработку.
Множественная обработка событий существующих компонентов
Допустим есть компонент с событиями (обычными) и хотелось бы организовать возможность их множественной обработки. Для решения этой задачи
надо учесть ряд нюансов.
- Поскольку компонент уже есть, то вся "множественность" обработки будет реализована вне его и он ее контролировать не будет.
Поэтому, любой код имеющий дело с событиями этого компонента должен учитывать принятые соглашения об обработке.
- Не всякие события хорошо обрабатываются "множественно". Если событие предполагает результат, то при множественной обработке могут быть проблемы,
т. к. насколько "множественным" должен быть результат по идее надо решать компоненту.
Что же нужно сделать для реализации множестветнной обработки в данном случае? Очеидно, что нужен класс-посредник, делающий перевызов к списку событий,
метод которого будет в свою очередь обработчиком для компонента.
TNotifyEventProxy = class
protected
FEvents: array of TNotifyEvent;
public
procedure Call(Sender: TObject);
function Reg(AEvent: TNotifyEvent; After: boolean = true): Integer;
function UnReg(AEvent: TNotifyEvent): Integer;
end;
Метод Call будет являться обработчиком перевызывающим обработчики зарегистрированные в массиве FEvents. Таким образом, каждый, кто работает с событиями компонента
должен обращаться к специализированному объекту соответствующего класса, для того, чтобы добавить свой обработчик в список или удалить его. При необходимости
добавления первого обработчика, должен создаваться такой объект-транслятор соответствующего класса и назначать свой вызов Call обработчиком события. При отключении последнего
обработчика от посредника логично убирать обработку события у компонента и, возможно, уничтожать объект-посредник. Для автоматизации этого процесса можно построить
специальный объект-менеджер который анализирует состояние обработчиков компонента (или компонентов) и оперирует назначением/удалением обработчиков, например,
посредством RTTI. Однако для проверки типов, а так же осуществления перевызовов потребуются конкретные классы объектов-посредников с различным описанием и реализацией
метода Call.
Универсальная множественная обработка событий существующих компонентов
Зададимся вопросом, возможно ли какая-то универсализация перевызова методов? Для этого представим себе условный обработчик Call в виде ассемблерного кода
MOV EAX, SomeMethod.Data
JMP SomeMethod.Code
Если SomeMethod содержит ссылку на корректный обработчик с подходящими параметрами, то вызов вполне пройдет: стек мы не меняли, регистра ECX, EDX, используемые
для передачи параметров, так же остались не измены, в EAX поместили self другого объекта-обработчика и перешли на код его метода инструкцией JMP. Таким образом,
ситуация такова, что наш метод Call как-будто и не вызывался - перевызов к SomeMethod пройдет так, словно SomeMethod был вызван непосредственно, причем возврат из
SomeMethod пройдет не в наш обработчик Call, а в код который его вызвал, поскольку адрес возврата сформировал этот (внешний) код, и мы его не меняли.
Можно ли использовать это для оргаганиции нескольких вызовов по списку? Вполне очевидно, что можно, но с некоторыми оговорками.
- Для того, чтобы вызвать второй и следующие обработчики в списке потребуется восстановить контекст вызова, который изменится после вызова первого обработчика.
Это стек и регистры ECX, EDX.
- Стек - это фактически просто указатель на его вершину ESP (здесь мы предполагаем, что обработка не испортит содержимое стека в области параметров вызова).
Поэтому речь идет о сохранении ряда регистров и восстановлении перед каждым последующим вызовом.
- Нам понадобится структура, позволяющая сохранить нужные нам значения. Причем, эта структура не может располагаться в стеке, тк нашего вызова Call "как-бы нет" и менять
состояние состояние стека мы не можем. Наиболее просто разместить ее в глобальной переменной.
- Структура для сохранения контекста должна обладать свойствами стека, тк вызовы могут быть рекурсивными.
- Структура должна учитывать возможность исключений в вызываемых обработчиках.
- Для того, чтобы конкретный обработчик вернулся в нашу процедуру Call, необходимо запомнить и подменить адрес возврата
Таким образом структура вызова обработчика будет следующей:
Компонент вызывает событие |
=> CALL => <= JMP <=
|
Универсальный Call с циклом |
=> JMP => <= RET <=
|
Конкретный обработчик |
К выше сказанному, можно так же добвить следующее
- Реализация псевдо-стека в глобальной переменной вызовет некоторые проблемы с многопоточностью, которые, вероятно, можно избежать, усложнив обработку.
- Обработка исключении по-хорошему требует обработки try-finally, но к сожалению я не нашел корректного способа регистрации фрейма исключения в fs:[0],
если данные фрейма расположены вне стека.
- Поскольку универсальный обработчик ничего не знает о параметрах вызова, то должен быть как минимум один реальный обработчик в списке, чтобы корректно
восстановить состояние стека (RET с правильным числом параметров в конце).
- Не всякие вызовы оставляют параметры в стеке неизменными. Например, параметр типа string без модификатора является копией строки, которую метод
освобождает в конце своей работы. Таким образом второй и последующий обработчики могут получить неожиданные параметры на входе.
Пример (9kb)
04.04.2009