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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With