Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IList<T> crashes when T is an event handler?

It appears to me that IList can NOT take Event handler as its element. The program has access violation $C00000005 on PROGRAM exit.

Everything is fine if I use Delphi RTL's TList.

The access violation happens for both 32 bit and 64bit build. When it happens, it seems to halt at the following lines of Spring4D:

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

The following sample program can replicate the access violation, using RAD Studio Tokyo 10.2.3, on 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.
like image 734
Wuping Xin Avatar asked Jun 05 '18 20:06

Wuping Xin


1 Answers

This is a compiler defect that impacts generics. The lifetime of the TMyClass instance isn't actually relevant. The code that the compiler can't handle is in TList<T>.DeleteRangeInternal in Spring.Collections.Lists. This code:

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

Remember that T is a method pointer, that is a type with two pointers. As such it is larger than a register. The compiler turns the call to Changed into this:

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]

Notice that the compiler only zeroises 4 bytes, and then passes those four bytes to Changed.

However, on the other side of this is the implementation of Changed, whose code to access the item which it passed looks like this:

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]

The first two lines of asm code there read the method pointer from the stack. So the ABI for method pointer parameters is that they are passed on the stack. This is documented as follows:

A method pointer is passed on the stack as two 32-bit pointers. The instance pointer is pushed before the method pointer so that the method pointer occupies the lowest address.

Back to the code that called this function. It passed the argument in a register. That mismatch is the cause of the exception which actually happens much later on. But this is where everything goes south.

Let's look at a workaround. We change the code in TList<T>.DeleteRangeInternal to be like so:

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

Now the generated code is this:

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]

Note that this time code is generated to zeroise both pointers in the method pointer and then pass them via the stack. This calling code matches the callee's code. And all is well.

I will submit this workaround to my personal Spring4D repo and Stefan will merge it into the 1.2.2 hotfix branch on the main repo.

I submitted a bug report: RSP-20683.

like image 69
David Heffernan Avatar answered Nov 20 '22 16:11

David Heffernan