IList <T>падает, когда T является обработчиком событий? - PullRequest
0 голосов
/ 05 июня 2018

Мне кажется, что IList НЕ может принимать обработчик событий в качестве своего элемента.Программа имеет нарушение прав доступа $ C00000005 при выходе из программы.

Все хорошо, если я использую TList Delphi RTL.

Нарушение доступа происходит как для 32-битной, так и для 64-битной сборки.Когда это происходит, кажется, что он останавливается в следующих строках Spring4D:

procedure TCollectionBase<T>.Changed(const item: T; action:      
   TCollectionChangedAction);
begin
   if fOnChanged.CanInvoke then
       fOnChanged.Invoke(Self, item, action);
end;

Следующий пример программы может воспроизвести нарушение доступа, используя RAD Studio Tokyo 10.2.3, в Windows.

program Test_Spring_IList_With_Event_Handler;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Spring.Collections;

type
  TSomeEvent = procedure of object;

  TMyEventHandlerClass = class
    procedure SomeProcedure;
  end;

  TMyClass = class
  private
    FEventList: IList<TSomeEvent>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddEvent(aEvent: TSomeEvent);
  end;

procedure TMyEventHandlerClass.SomeProcedure;
begin
  // Nothing to do.
end;

constructor TMyClass.Create;
begin
  inherited;
  FEventList := TCollections.CreateList<TSomeEvent>;
end;

destructor TMyClass.Destroy;
begin
  FEventList := nil;
  inherited;
end;

procedure TMyClass.AddEvent(aEvent: TSomeEvent);
begin
  FEventList.Add(aEvent);
end;

var
  MyEventHandlerObj: TMyEventHandlerClass;
  MyObj: TMyClass;
begin
  MyObj := TMyClass.Create;
  MyEventHandlerObj := TMyEventHandlerClass.Create;

  try
    MyObj.AddEvent(MyEventHandlerObj.SomeProcedure);
  finally
    MyObj.Free;
    MyEventHandlerObj.Free;
  end;
end.

1 Ответ

0 голосов
/ 06 июня 2018

Это дефект компилятора, который влияет на дженерики.Время жизни экземпляра TMyClass на самом деле не имеет значения.Код, который не может обработать компилятор, находится в TList<T>.DeleteRangeInternal в Spring.Collections.Lists.Этот код:

if doClear then
  Changed(Default(T), caReseted);

Помните, что T - это указатель на метод, то есть тип с двумя указателями.Как таковой он больше, чем регистр.Компилятор превращает вызов Changed в следующее:

Spring.Collections.Lists.pas.641: Changed(Default(T), caReseted);
00504727 B105             mov cl,$05
00504729 33D2             xor edx,edx
0050472B 8B45FC           mov eax,[ebp-$04]
0050472E 8B18             mov ebx,[eax]
00504730 FF5374           call dword ptr [ebx+$74]

Обратите внимание, что компилятор обнуляет только 4 байта, а затем передает эти четыре байта в Changed.

Однако придругая сторона этого - реализация Changed, чей код для доступа к item, который он передал, выглядит следующим образом:

Spring.Collections.Base.pas.1583: fOnChanged.Invoke(Self, item, action);
00502E58 FF750C           push dword ptr [ebp+$0c]
00502E5B FF7508           push dword ptr [ebp+$08]
00502E5E 8D55F0           lea edx,[ebp-$10]
00502E61 8B45FC           mov eax,[ebp-$04]
00502E64 8B4024           mov eax,[eax+$24]
00502E67 8B08             mov ecx,[eax]
00502E69 FF513C           call dword ptr [ecx+$3c]

Первые две строки кода asm читают указатель метода изстек.Таким образом, ABI для параметров указателя метода состоит в том, что они передаются в стеке.Это задокументировано следующим образом:

Указатель метода передается в стек как два 32-разрядных указателя.Указатель экземпляра помещается перед указателем метода, поэтому указатель метода занимает самый низкий адрес.

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

Давайте посмотрим на обходной путь.Мы изменили код в TList<T>.DeleteRangeInternal так, чтобы он был таким:

var
  defaultItem: T;
....
if doClear then
begin
  defaultItem := Default(T);
  Changed(defaultItem, caReseted);
end;

Теперь сгенерированный код выглядит так:

Spring.Collections.Lists.pas.643: defaultItem := Default(T);
0050472B 33C0             xor eax,eax
0050472D 8945E0           mov [ebp-$20],eax
00504730 8945E4           mov [ebp-$1c],eax
Spring.Collections.Lists.pas.644: Changed(defaultItem, caReseted);
00504733 FF75E4           push dword ptr [ebp-$1c]
00504736 FF75E0           push dword ptr [ebp-$20]
00504739 B205             mov dl,$05
0050473B 8B45FC           mov eax,[ebp-$04]
0050473E 8B08             mov ecx,[eax]
00504740 FF5174           call dword ptr [ecx+$74]

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

Я отправлю этот обходной путь в мой личный репозиторий Spring4D, и Стефан объединит его с веткой исправлений 1.2.2 в главном репо.

Я отправил отчет об ошибке: RSP-20683 .

...