10

在我看来, IList 不能将事件处理程序作为其元素。程序在程序退出时访问冲突 $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.
4

1 回答 1

12

这是一个影响泛型的编译器缺陷。实例的生命周期TMyClass实际上并不相关。编译器无法处理的代码TList<T>.DeleteRangeInternalSpring.Collections.Lists. 这段代码:

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

请记住,这T是一个方法指针,即具有两个指针的类型。因此它比寄存器大。编译器将调用转换Changed为:

Spring.Collections.Lists.pas.641:已更改(默认(T),caReseted);
00504727 B105 移动 CL,05 美元
00504729 33D2 xor edx,edx
0050472B 8B45FC mov eax,[ebp-$04]
0050472E 8B18 mov ebx,[eax]
00504730 FF5374 呼叫 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 移动 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 移动 [ebp-$20],eax
00504730 8945E4 移动 [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 移动 dl,05 美元
0050473B 8B45FC mov eax,[ebp-$04]
0050473E 8B08 mov ecx,[eax]
00504740 FF5174 调用 dword ptr [ecx+$74]

请注意,生成此时间代码以将方法指针中的两个指针归零,然后通过堆栈传递它们。此调用代码与被调用者的代码匹配。一切都很好。

我会将这个解决方法提交给我的个人 Spring4D 存储库,Stefan 会将其合并到主存储库上的 1.2.2 修补程序分支中。

我提交了一个错误报告:RSP-20683

于 2018-06-06T19:02:14.740 回答