当 T 是事件处理程序时 IList<T> 崩溃?
IList<T> crashes when T is an event handler?
在我看来,IList 不能将事件处理程序作为其元素。该程序在 PROGRAM 退出时存在访问冲突 $C00000005。
如果我使用 Delphi RTL 的 TList,一切都很好。
访问冲突发生在 32 位和 64 位版本上。当它发生时,它似乎停止在 Spring4D 的以下几行:
procedure TCollectionBase<T>.Changed(const item: T; action:
TCollectionChangedAction);
begin
if fOnChanged.CanInvoke then
fOnChanged.Invoke(Self, item, action);
end;
以下示例程序可以在 Windows 上使用 RAD Studio Tokyo 10.2.3 复制访问冲突。
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.
这是影响泛型的编译器缺陷。 TMyClass
实例的生命周期实际上并不相关。编译器无法处理的代码在Spring.Collections.Lists
中的TList<T>.DeleteRangeInternal
中。此代码:
if doClear then
Changed(Default(T), caReseted);
记住T
是一个方法指针,也就是一个有两个指针的类型。因此它比寄存器大。编译器将对 Changed
的调用转换为:
Spring.Collections.Lists.pas.641: Changed(Default(T), caReseted);
00504727 B105 mov cl,
00504729 33D2 xor edx,edx
0050472B 8B45FC mov eax,[ebp-]
0050472E 8B18 mov ebx,[eax]
00504730 FF5374 call dword ptr [ebx+]
注意编译器只将 4 个字节归零,然后将这四个字节传递给 Changed
。
然而,另一方面是 Changed
的实现,其访问它传递的 item
的代码如下所示:
Spring.Collections.Base.pas.1583: fOnChanged.Invoke(Self, item, action);
00502E58 FF750C push dword ptr [ebp+[=23=]c]
00502E5B FF7508 push dword ptr [ebp+]
00502E5E 8D55F0 lea edx,[ebp-]
00502E61 8B45FC mov eax,[ebp-]
00502E64 8B4024 mov eax,[eax+]
00502E67 8B08 mov ecx,[eax]
00502E69 FF513C call dword ptr [ecx+c]
前两行asm代码从栈中读取方法指针。所以方法指针参数的 ABI 是它们在堆栈上传递。记录如下:
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.
回到调用这个函数的代码。它在寄存器中传递参数。这种不匹配是异常的原因,实际上发生在很久以后。但这是一切都向南的地方。
让我们看看解决方法。我们将 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-],eax
00504730 8945E4 mov [ebp-c],eax
Spring.Collections.Lists.pas.644: Changed(defaultItem, caReseted);
00504733 FF75E4 push dword ptr [ebp-c]
00504736 FF75E0 push dword ptr [ebp-]
00504739 B205 mov dl,
0050473B 8B45FC mov eax,[ebp-]
0050473E 8B08 mov ecx,[eax]
00504740 FF5174 call dword ptr [ecx+]
请注意,这次生成代码是为了将方法指针中的两个指针置零,然后通过堆栈传递它们。此调用代码与被调用者的代码相匹配。一切都很好。
我会将此解决方法提交到我的个人 Spring4D 存储库,Stefan 会将其合并到主存储库的 1.2.2 修补程序分支中。
我提交了错误报告:RSP-20683。
在我看来,IList 不能将事件处理程序作为其元素。该程序在 PROGRAM 退出时存在访问冲突 $C00000005。
如果我使用 Delphi RTL 的 TList,一切都很好。
访问冲突发生在 32 位和 64 位版本上。当它发生时,它似乎停止在 Spring4D 的以下几行:
procedure TCollectionBase<T>.Changed(const item: T; action:
TCollectionChangedAction);
begin
if fOnChanged.CanInvoke then
fOnChanged.Invoke(Self, item, action);
end;
以下示例程序可以在 Windows 上使用 RAD Studio Tokyo 10.2.3 复制访问冲突。
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.
这是影响泛型的编译器缺陷。 TMyClass
实例的生命周期实际上并不相关。编译器无法处理的代码在Spring.Collections.Lists
中的TList<T>.DeleteRangeInternal
中。此代码:
if doClear then
Changed(Default(T), caReseted);
记住T
是一个方法指针,也就是一个有两个指针的类型。因此它比寄存器大。编译器将对 Changed
的调用转换为:
Spring.Collections.Lists.pas.641: Changed(Default(T), caReseted); 00504727 B105 mov cl, 00504729 33D2 xor edx,edx 0050472B 8B45FC mov eax,[ebp-] 0050472E 8B18 mov ebx,[eax] 00504730 FF5374 call dword ptr [ebx+]
注意编译器只将 4 个字节归零,然后将这四个字节传递给 Changed
。
然而,另一方面是 Changed
的实现,其访问它传递的 item
的代码如下所示:
Spring.Collections.Base.pas.1583: fOnChanged.Invoke(Self, item, action); 00502E58 FF750C push dword ptr [ebp+[=23=]c] 00502E5B FF7508 push dword ptr [ebp+] 00502E5E 8D55F0 lea edx,[ebp-] 00502E61 8B45FC mov eax,[ebp-] 00502E64 8B4024 mov eax,[eax+] 00502E67 8B08 mov ecx,[eax] 00502E69 FF513C call dword ptr [ecx+c]
前两行asm代码从栈中读取方法指针。所以方法指针参数的 ABI 是它们在堆栈上传递。记录如下:
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.
回到调用这个函数的代码。它在寄存器中传递参数。这种不匹配是异常的原因,实际上发生在很久以后。但这是一切都向南的地方。
让我们看看解决方法。我们将 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-],eax 00504730 8945E4 mov [ebp-c],eax Spring.Collections.Lists.pas.644: Changed(defaultItem, caReseted); 00504733 FF75E4 push dword ptr [ebp-c] 00504736 FF75E0 push dword ptr [ebp-] 00504739 B205 mov dl, 0050473B 8B45FC mov eax,[ebp-] 0050473E 8B08 mov ecx,[eax] 00504740 FF5174 call dword ptr [ecx+]
请注意,这次生成代码是为了将方法指针中的两个指针置零,然后通过堆栈传递它们。此调用代码与被调用者的代码相匹配。一切都很好。
我会将此解决方法提交到我的个人 Spring4D 存储库,Stefan 会将其合并到主存储库的 1.2.2 修补程序分支中。
我提交了错误报告:RSP-20683。