让Allen Bauer的TMulticastEvent <T>工作

Nat*_*Nat 18 delphi generics events delphi-2009

我一直在摆弄周围与Allen Bauer的代码的通用多播事件调度程序(见他关于它的博客文章在这里).

他提供了足够的代码让我想要使用它,不幸的是他没有发布完整的源代码.我有一个bash让它工作,但我的汇编技能是不存在的.

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

procedure InternalSetDispatcher;
begin
   XCHG  EAX,[ESP]
   POP   EAX
   POP   EBP
   JMP   SetEventDispatcher
end;
Run Code Online (Sandbox Code Playgroud)

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

procedure Add(const AMethod: T); overload;
Run Code Online (Sandbox Code Playgroud)

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

procedure SetEventDispatcher(var ADispatcher: T; ATypeData: PTypeData);
Run Code Online (Sandbox Code Playgroud)

所以,我认为堆栈会被破坏.我知道代码在做什么(通过弹出对self的隐藏引用来清理堆栈帧从调用到InternalSetDispatcher,我假设返回地址),但我只是无法弄清楚那个小小的汇编程序来获取整个代码事情进行着.

编辑:只是为了澄清,我正在寻找的是我可以用来使InternalSetDispatcher方法工作的汇编程序,即汇编程序用两个参数清理一个过程的堆栈,一个是var.

编辑2:我已经修改了一下这个问题,感谢梅森到目前为止的答案.我应该提到上面的代码不起作用,并且当SetEventDispatcher返回时,会引发AV.

Nat*_*Nat 15

在我在Web上运行了很多之后,答案是汇编程序假定在调用InternalSetDispatcher时存在堆栈框架.

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

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

感谢Mason的帮助,让我得到了这个答案.:)


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


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

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.
Run Code Online (Sandbox Code Playgroud)


Mas*_*ler 6

来自博文:

该函数的作用是从调用链中删除自身和直接调用者,并直接将控制转移到相应的"不安全"方法,同时保留传入的参数.

代码消除了InternalAdd的堆栈帧,它只有一个参数Self.它对您传入的事件没有影响,因此只使用一个参数和寄存器调用约定复制任何其他函数是安全的.

编辑: 回应评论,有一点你错过了.当你写下"我知道代码在做什么(从父调用中清理堆栈帧)时,"你错了. 它不会触及父呼叫. 它不是从Add清理堆栈帧,它是从当前调用InternalAdd 清理堆栈帧.

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

Allen的代码正在做的是解决编译器限制.每个事件都是一个方法指针,但是对于泛型没有"方法约束",因此编译器不知道T总是会成为可以转换为TMethod的8字节记录.(事实上​​,它不一定是.TMulticastEvent<byte>如果你真的想以新的和有趣的方式破坏你的程序,你可以创建一个.)内部方法使用程序集通过完全从调用堆栈中剥离自己来手动模拟类型转换和JMPing(基本上是一个GOTO)到适当的方法,留下与调用它的函数相同的参数列表.

所以当你看到

procedure TMulticastEvent.Add(const AMethod: T);
begin
  InternalAdd;
end;
Run Code Online (Sandbox Code Playgroud)

它正在做什么等同于以下,如果它将编译:

procedure TMulticastEvent.Add(const AMethod: T);
begin
  Add(TEvent(AMethod));
end;
Run Code Online (Sandbox Code Playgroud)

您的InternalSetDispatcher将要执行完全相同的操作:剥离其自己的单参数调用,然后使用与调用方法SetEventDispatcher完全相同的参数列表跳转到SetDispatcher.调用函数具有什么参数或它跳转到的函数无关紧要.重要的是(这很关键!)是SetEventDispatcher和SetDispatcher具有相同的调用签名.

所以,是的,您发布的假设代码将正常工作,它不会破坏调用堆栈.