【问题标题】:Getting Allen Bauer's TMulticastEvent<T> working让 Allen Bauer 的 TMulticastEvent<T> 工作
【发布时间】:2009-08-04 00:25:48
【问题描述】:

我一直在研究 Allen Bauer 的通用多播事件调度程序代码(请参阅他的博客文章 here)。

他提供了足够的代码让我想使用它,不幸的是他没有发布完整的源代码。我很乐意让它工作,但我的汇编技能不存在。

我的问题是 InternalSetDispatcher 方法。天真的方法是使用与其他 InternalXXX 方法相同的汇编程序:

procedure InternalSetDispatcher;
begin
   XCHG  EAX,[ESP]
   POP   EAX
   POP   EBP
   JMP   SetEventDispatcher
end;

但这用于带有一个 const 参数的过程,如下所示:

procedure Add(const AMethod: T); overload;

而SetDispatcher有两个参数,一个是var:

procedure SetEventDispatcher(var ADispatcher: T; ATypeData: PTypeData);

所以,我假设堆栈会损坏。我知道代码在做什么(通过弹出对 self 的隐藏引用来清理对 InternalSetDispatcher 的调用中的堆栈帧,我假设返回地址),但我只是想不出那一点汇编程序来获得整个事情进展顺利。

编辑:澄清一下,我正在寻找的是我可以用来让 InternalSetDispatcher 方法工作的汇编器,即,用于清理具有两个参数(一个是 var)的过程堆栈的汇编器。

EDIT2:我已经稍微修改了这个问题,感谢 Mason 到目前为止的回答。应该提一下,上面的代码不起作用,当 SetEventDispatcher 返回时,会引发一个 AV。

【问题讨论】:

  • 编辑了我的答案,以便更好地解释幕后发生的事情。
  • 所以,我想我需要重新提出问题...参数列表不是问题(谢谢梅森),还有其他问题。我要删除这个问题并重新开始吗?还是我完全改变了问题,让梅森的回答看起来很奇怪?
  • 最好再问一个问题。
  • 如何发布它,这样不是每个人都必须做同样的事情才能让它工作。我希望艾伦发布了一个 .zip。
  • 完成。我已将其附加到下面的答案中

标签: delphi generics events delphi-2009


【解决方案1】:

在我在网上跑了很多遍之后,答案是汇编程序假定调用 InternalSetDispatcher 时存在堆栈帧。

似乎没有为调用 InternalSetDispatcher 生成堆栈帧。

因此,修复就像使用 {$stackframes on} 编译器指令打开堆栈帧并重建一样简单。

感谢 Mason 帮助我得到这个答案。 :)


编辑 2012-08-08:如果您热衷于使用它,您可能需要查看 Delphi Sping Framework 中的实现。我还没有测试过,但看起来它比这段代码更好地处理不同的调用约定。


编辑:根据要求,我对艾伦代码的解释如下。除了需要打开堆栈帧之外,我还需要在项目级别打开优化才能使其工作:

unit MulticastEvent;

interface

uses
  Classes, SysUtils, Generics.Collections, ObjAuto, TypInfo;

type

  // you MUST also have optimization turned on in your project options for this
  // to work! Not sure why.
  {$stackframes on}
  {$ifopt O-}
    {$message Fatal 'optimisation _must_ be turned on for this unit to work!'}
  {$endif}
  TMulticastEvent = class
  strict protected
    type TEvent = procedure of object;
  strict private
    FHandlers: TList<TMethod>;
    FInternalDispatcher: TMethod;

    procedure InternalInvoke(Params: PParameters; StackSize: Integer);
    procedure SetDispatcher(var AMethod: TMethod; ATypeData: PTypeData);
    procedure Add(const AMethod: TEvent); overload;
    procedure Remove(const AMethod: TEvent); overload;
    function IndexOf(const AMethod: TEvent): Integer; overload;
  protected
    procedure InternalAdd;
    procedure InternalRemove;
    procedure InternalIndexOf;
    procedure InternalSetDispatcher;

  public
    constructor Create;
    destructor Destroy; override;

  end;

  TMulticastEvent<T> = class(TMulticastEvent)
  strict private
    FInvoke: T;
    procedure SetEventDispatcher(var ADispatcher: T; ATypeData: PTypeData);
  public
    constructor Create;
    procedure Add(const AMethod: T); overload;
    procedure Remove(const AMethod: T); overload;
    function IndexOf(const AMethod: T): Integer; overload;

    property Invoke: T read FInvoke;
  end;

implementation

{ TMulticastEvent }

procedure TMulticastEvent.Add(const AMethod: TEvent);
begin
  FHandlers.Add(TMethod(AMethod))
end;

constructor TMulticastEvent.Create;
begin
  inherited;
  FHandlers := TList<TMethod>.Create;
end;

destructor TMulticastEvent.Destroy;
begin
  ReleaseMethodPointer(FInternalDispatcher);
  FreeAndNil(FHandlers);
  inherited;
end;

function TMulticastEvent.IndexOf(const AMethod: TEvent): Integer;
begin
  result := FHandlers.IndexOf(TMethod(AMethod));
end;

procedure TMulticastEvent.InternalAdd;
asm
  XCHG  EAX,[ESP]
  POP   EAX
  POP   EBP
  JMP   Add
end;

procedure TMulticastEvent.InternalIndexOf;
asm
  XCHG  EAX,[ESP]
  POP   EAX
  POP   EBP
  JMP   IndexOf
end;

procedure TMulticastEvent.InternalInvoke(Params: PParameters; StackSize: Integer);
var
  LMethod: TMethod;
begin
  for LMethod in FHandlers do
  begin
    // Check to see if there is anything on the stack.
    if StackSize > 0 then
      asm
        // if there are items on the stack, allocate the space there and
        // move that data over.
        MOV ECX,StackSize
        SUB ESP,ECX
        MOV EDX,ESP
        MOV EAX,Params
        LEA EAX,[EAX].TParameters.Stack[8]
        CALL System.Move
      end;
    asm
      // Now we need to load up the registers. EDX and ECX may have some data
      // so load them on up.
      MOV EAX,Params
      MOV EDX,[EAX].TParameters.Registers.DWORD[0]
      MOV ECX,[EAX].TParameters.Registers.DWORD[4]
      // EAX is always "Self" and it changes on a per method pointer instance, so
      // grab it out of the method data.
      MOV EAX,LMethod.Data
      // Now we call the method. This depends on the fact that the called method
      // will clean up the stack if we did any manipulations above.
      CALL LMethod.Code
    end;
  end;
end;

procedure TMulticastEvent.InternalRemove;
asm
  XCHG  EAX,[ESP]
  POP   EAX
  POP   EBP
  JMP   Remove
end;

procedure TMulticastEvent.InternalSetDispatcher;
asm
  XCHG  EAX,[ESP]
  POP   EAX
  POP   EBP
  JMP   SetDispatcher;
end;

procedure TMulticastEvent.Remove(const AMethod: TEvent);
begin
  FHandlers.Remove(TMethod(AMethod));
end;

procedure TMulticastEvent.SetDispatcher(var AMethod: TMethod;
  ATypeData: PTypeData);
begin
  if Assigned(FInternalDispatcher.Code) and Assigned(FInternalDispatcher.Data) then
    ReleaseMethodPointer(FInternalDispatcher);
  FInternalDispatcher := CreateMethodPointer(InternalInvoke, ATypeData);
  AMethod := FInternalDispatcher;
end;

{ TMulticastEvent<T> }

procedure TMulticastEvent<T>.Add(const AMethod: T);
begin
  InternalAdd;
end;

constructor TMulticastEvent<T>.Create;
var
  MethInfo: PTypeInfo;
  TypeData: PTypeData;
begin
  MethInfo := TypeInfo(T);
  TypeData := GetTypeData(MethInfo);
  inherited Create;
  Assert(MethInfo.Kind = tkMethod, 'T must be a method pointer type');
  SetEventDispatcher(FInvoke, TypeData);
end;

function TMulticastEvent<T>.IndexOf(const AMethod: T): Integer;
begin
  InternalIndexOf;
end;

procedure TMulticastEvent<T>.Remove(const AMethod: T);
begin
  InternalRemove;
end;

procedure TMulticastEvent<T>.SetEventDispatcher(var ADispatcher: T;
  ATypeData: PTypeData);
begin
  InternalSetDispatcher;
end;

end.

【讨论】:

【解决方案2】:

来自博文:

这个函数的作用是删除 本身和来自的直接调用者 调用链并直接转移 控制到相应的“不安全” 方法同时保留传入的 参数。

代码正在消除InternalAdd 的堆栈帧,它只有一个参数Self。它对您传入的事件没有影响,因此可以安全地复制任何其他只有一个参数和 register 调用约定的函数。

编辑:在回复评论时,您遗漏了一点。当您写道:“我知道代码在做什么(从父调用中清除堆栈帧)”时,您错了。 它不会触及父调用。它不是从 Add 清理堆栈帧,而是从 当前 调用 InternalAdd 清理堆栈帧。

这里有一些基本的 OO 理论,因为您在这一点上似乎有点困惑,我承认这有点令人困惑。 Add 真的 没有一个参数,而 SetEventDispatcher 没有两个。他们实际上分别有两个和三个。任何未声明 static 的方法调用的第一个参数是Self,它是由编译器无形添加的。所以三个内部函数各有一个参数。这就是我写这篇文章时的意思。

Allen 的代码正在解决编译器限制。每个事件都是一个方法指针,但是对于泛型没有“方法约束”,所以编译器不知道 T 总是可以转换为 TMethod 的 8 字节记录。 (事实上​​,不必如此。如果你真的想以新的有趣的方式破坏你的程序,你可以创建一个TMulticastEvent&lt;byte&gt;。)内部方法使用汇编来手动模拟类型转换,将它们自己剥离出来。完全调用堆栈并 JMPing(基本上是 GOTO)到适当的方法,使其具有与调用它的函数相同的参数列表。

所以当你看到

procedure TMulticastEvent.Add(const AMethod: T);
begin
  InternalAdd;
end;

如果它可以编译,它的作用相当于以下内容:

procedure TMulticastEvent.Add(const AMethod: T);
begin
  Add(TEvent(AMethod));
end;

您的 InternalSetDispatcher 将想要做完全相同的事情:剥离它自己的单参数调用,然后跳转到具有与调用方法 SetEventDispatcher 完全相同的参数列表的 SetDispatcher。调用函数有什么参数或者它要跳转到的函数并不重要。重要的是(这很关键!)是 SetEventDispatcher 和 SetDispatcher 具有相同的调用签名。

是的,您发布的假设代码可以正常工作,并且不会破坏调用堆栈。

【讨论】:

  • 确实! :) 它适用于您描述的那些功能。我希望的是一个带有两个参数的函数的汇编器,一个是 var。
  • 感谢您的回复...我了解所有 OOP 内容,使用隐藏参数 (self),我也了解正在从堆栈中删除的是对 InternalXXX 的调用(我会修改我的问题是为了减少混淆)。我可以从您的答案中看到的唯一问题(顺便说一句,这很好)是代码实际上不起作用。当 SetEventDispatcher 返回时,它跳到 la-la-land,产生一个 AV。因此,据此我推测有问题,堆栈确实被破坏或返回地址被破坏。
  • 问题可能是(我认为)var 参数没有被传回吗?
  • 啊!当然,我是个白痴,var无所谓,它只是一个指向值的指针,而不是值本身……
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-02
  • 2017-12-31
  • 1970-01-01
  • 1970-01-01
  • 2012-11-06
  • 2014-03-11
相关资源
最近更新 更多