IList<T> падает, когда T является обработчиком событий?

Мне кажется, что 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 ответ

Решение

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

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

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

Spring.Collections.Lists.pas.641: Изменено (по умолчанию (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 вызов 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: изменено (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 вызов dword ptr [ecx+$74]

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

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

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

Другие вопросы по тегам