Question

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;
Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top