1

The following code is based on this article: http://blog.barrkel.com/2010/01/using-anonymous-methods-in-method.html.

When the event handler code inside the anonymous procedure is fired (upon changing a row in the grid), the first 'if' reports that dbgrid is not nil but the second one reports it is nil.

Any idea about what's going on here? You can get the full source code from here (you might have to change the FileName property of the TClientDataSet to point to the directory where you unzipped the project).

unit Main;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB, Vcl.DBGrids, Datasnap.DBClient, Vcl.Grids;

type

  AfterScrollEventHandler = reference to procedure (sender: TDataSet);

  TForm3 = class(TForm)
    dbgrdGrid: TDBGrid;
    cdsDataSet: TClientDataSet;
    dsDataSet: TDataSource;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  procedure MethodReferenceToMethodPtr(const MethRef; var MethPtr);
  procedure InjectEventHandler(dataSet: TDataSet; dbGrid: TDBGrid);

var
  Form3: TForm3;

implementation

{$R *.dfm}
procedure InjectEventHandler(dataSet: TDataSet; dbGrid: TDBGrid);
var
  eventHandlerRef : AfterScrollEventHandler;
  eventHandlerPtr : TDataSetNotifyEvent;
begin

  eventHandlerRef := procedure (sender: TDataSet)
    begin
      if dbGrid <> nil then
        MessageDlg('1: AfterScroll: The grid is not nil!', mtInformation, [mbOK], 0)
      else
        MessageDlg('1: AfterScroll: The grid is nil! It''s going to crash...', mtInformation, [mbOK], 0);

      if dbGrid <> nil then
        MessageDlg('2: AfterScroll: The grid is not nil!', mtInformation, [mbOK], 0)
      else
        MessageDlg('2: AfterScroll: The grid is nil! It''s going to crash...', mtInformation, [mbOK], 0);

    end;

  MethodReferenceToMethodPtr (eventHandlerRef, eventHandlerPtr);

  dataSet.AfterScroll := eventHandlerPtr;

end;



procedure MethodReferenceToMethodPtr(const MethRef; var MethPtr);
type
  TVtable = array[0..3] of Pointer;
  PVtable = ^TVtable;
  PPVtable = ^PVtable;
begin
  // 3 is offset of Invoke, after QI, AddRef, Release
  TMethod(MethPtr).Code := PPVtable(MethRef)^^[3];
  TMethod(MethPtr).Data := Pointer(MethRef);
end;

procedure TForm3.FormCreate(Sender: TObject);
var
  eventHandlerRef1, eventHandlerRef2 : AfterScrollEventHandler;
begin

  InjectEventHandler(cdsDataSet, dbgrdGrid)
end;

end.

Update:

This code works as expected (the anonymous procedure stores the dbGrid parameter in a local variable):

procedure InjectEventHandler(dataSet: TDataSet; dbGrid: TDBGrid);
var
  eventHandlerRef : AfterScrollEventHandler;
  eventHandlerPtr : TDataSetNotifyEvent;
begin

  eventHandlerRef := procedure (sender: TDataSet)
    var grid: TDBGrid;
    begin
      grid := dbGrid;
      if grid <> nil then
        MessageDlg('1: AfterScroll: The grid is not nil!', mtInformation, [mbOK], 0)
      else
        MessageDlg('1: AfterScroll: The grid is nil! It''s going to crash...', mtInformation, [mbOK], 0);

      if grid <> nil then
        MessageDlg('2: AfterScroll: The grid is not nil!', mtInformation, [mbOK], 0)
      else
        MessageDlg('2: AfterScroll: The grid is nil! It''s going to crash...', mtInformation, [mbOK], 0);

    end;

  MethodReferenceToMethodPtr (eventHandlerRef, eventHandlerPtr);

  dataSet.AfterScroll := eventHandlerPtr;

end;
boggy
  • 3,674
  • 3
  • 33
  • 56
  • The code relies on unspecified implementation details, and it is based on a blog post from early 2010, long before Delphi XE5 existed. You may want to check if those implementation details have changed since then. The behaviour seems suspiciously as if `Pointer(MethRef)` is not the correct value for `TMethod(MethPtr).Data` and/or the procedures use a different calling convention, causing you to read memory you shouldn't be reading, that happens to get reused (and overwritten) by your call to `MessageDlg`. –  Apr 25 '14 at 21:16
  • I was looking earlier for a way to assign anonymous methods as event handlers and came across Barry's post as well as this one: http://stackoverflow.com/questions/360254/can-i-use-a-closure-on-an-event-handler-ie-tbutton-onclick. I chose Barry's solution but then I came across this issue. – boggy Apr 25 '14 at 21:28
  • Have you tried making a local variable in InjectEventHandler, setting it to dbGrid and then using the local variable in the anonymous method? – Graymatter Apr 25 '14 at 21:35
  • @Graymatter: It works, the second 'if' reports grid as being different than nil! – boggy Apr 25 '14 at 21:47
  • 1
    You should avoid such hacking and find a clean solution – David Heffernan Apr 25 '14 at 21:49
  • @DavidHeffernan: Agreed. I am only trying to understand what's happening here. – boggy Apr 25 '14 at 21:51
  • I had some really strange behavior with this sort of thing from the compiler recently. It's sometimes helps to give it a few hints on what to do. – Graymatter Apr 25 '14 at 21:54
  • Sorry, my bad. I tried your suggestion and it doesn't work. – boggy Apr 25 '14 at 21:56
  • The object is not being freed so it looks like a memory overwrite of some sort undoubtedly caused by the hack. – Graymatter Apr 25 '14 at 22:16
  • 1
    The code in your update is still broken. The lifetime of the anonymous method is for the length of the InjectEventHandler routine, not any longer. But you are "casting" it to an event and using that longer. You need to keep the anonymous method alive as Rob says in his answer. – Stefan Glienke Apr 26 '14 at 09:23

1 Answers1

3

You're seeing undefined behavior. The article you cite mentions that the method reference needs to remain alive for the lifetime of the method pointer, but your code breaks that rule. The object associated with the method reference may get destroyed, so the dbGrid variable that held the captured value no longer exists. You're reading garbage values, and it's possible that the location in memory changes values between the first and second read. We don't even know whether the value you read is equal to the value you expect it to be, just that it isn't nil.

It appears to work when you use a local variable because whatever memory gets written between the first and second read apparently no longer overlaps with where the function expects the dbGrid or grid variables to reside. You're still reading garbage, though. It's just garbage that happens not to be nil twice instead of once.

Make your eventHandlerRef variable be a field of the enclosing class rather than a local variable and all should be well, assuming the undocumented implementation details for Delphi 2010 are still valid in the version you're using now.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • The implementation details are unlikely to be changed - anonymous method are still interfaces backed by a compiler generated class. – Stefan Glienke Apr 26 '14 at 09:18