2

I want to call a method that is sent as a parameter through a window message. I tried as in the example below... but I get access violation when the DoWork method is executed. I think the procedural CallBack variable is not reconstructed properly in the message handler. In Delphi documentation it says that a "method pointer" variable has 2 pointers: "These types represent method pointers. A method pointer is really a pair of pointers; the first stores the address of a method, and the second stores a reference to the object the method belongs to."... But I don't know how to access the second pointer, and then reconstruct the variable with those two pointers...

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TCallBackNotify = function(CallBack: Pointer = nil): Boolean of object;

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    procedure Test(var Msg: TMessage); message WM_USER;
    function DoWork(CallBack: Pointer = nil): Boolean;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Test(var Msg: TMessage);
var CallBack: TCallBackNotify;
begin
 if Msg.Msg = WM_USER then begin
  @CallBack:= Pointer(Msg.WParam);
  CallBack;
 end;
end;

function TForm1.DoWork(CallBack: Pointer = nil): Boolean;
begin
 Caption:= 'It''s working !';
end;

procedure TForm1.Button1Click(Sender: TObject);
var CallBack: TCallBackNotify;
begin
 CallBack:= DoWork;
 PostMessage(Handle, WM_USER, WPARAM(@CallBack) , 0);
end;

end.
Marus Gradinaru
  • 2,824
  • 1
  • 26
  • 55

1 Answers1

3

But I don't know how to access the second pointer, and then reconstruct the variable with those two pointers...

From the documentation on internal data formats:

A method pointer is stored as a 32-bit pointer to the entry point of a method, followed by a 32-bit pointer to an object.

in a 32-bit app. For a 64-bit app, the same applies, but with 64-bit pointers, of course.

This is actually all you need to know to access these pointers using pointer arithmetic, but it is much nicer to use the predefined TMethod record:

If M is any method pointer, then TMethod(M).Code is the code (procedure) pointer and TMethod(M).Data is the data (object) pointer.

If you want to send or post a method pointer, you can either utilize LPARAM and WPARAM for these two pointers, or you can send a single pointer to a single record (or object) containing both. Just remember that when posting a message, you cannot pass the address of a local variable, since that one might go out of scope before it is received by the recipient.


When you write

var
  CallBack: TCallBackNotify;
begin
  CallBack := DoWork;
  PostMessage(Handle, WM_USER, WPARAM(@CallBack) , 0);

you are sending @CallBack. Since CallBack is declared as a method pointer, @CallBack is the code pointer, i.e. @CallBack = TMethod(CallBack).Code. Hence, the data pointer isn't sent at all.

As a comparison, @@CallBack is the address of the local CallBack variable. If you had instead declared CallBack as a TMethod (a normal record), this address would have been denoted @CallBack only.

To illustrate this,

procedure TForm1.Button1Click(Sender: TObject);
var
  CallBack: TCallBackNotify;
  CallBackRec: TMethod absolute CallBack;
begin
  CallBack := DoWork;
  ShowMessage(
    'Data = ' + NativeInt(CallBackRec.Data).ToString + #13#10 +
    ' = Self = ' + NativeInt(Self).ToString + #13#10 +
    'Code = ' + NativeInt(CallBackRec.Code).ToString + #13#10 +
    ' = @CallBack = ' + NativeInt(@CallBack).ToString + #13#10 +
    '@@CallBack = ' + NativeInt(@@CallBack).ToString + #13#10 +
    ' = @CallBackRec = ' + NativeInt(@CallBackRec).ToString + #13#10
  );
end;

may yield

Data            = 17376288
 = Self         = 17376288
Code            = 6278088
 = @CallBack    = 6278088
@@CallBack      = 1635636
 = @CallBackRec = 1635636

So, to send a method, you may do, for instance,

procedure TForm1.Button1Click(Sender: TObject);
var
  CallBack: TCallBackNotify;
begin
  CallBack := DoWork;
  SendMessage(Handle, WM_USER, WParam(@@CallBack), 0);
end;

procedure TForm1.Test(var Msg: TMessage);
var
  CallBack: TCallBackNotify;
begin
  if Msg.Msg = WM_USER then
  begin
    CallBack := TCallBackNotify(PMethod(Msg.WParam)^);
    CallBack;
  end;
end;

Since SendMessage is synchronous, it will not return until the message has been processed by the receiver; in particular, SendMessage will not return before TForm1.Test returns. Hence the CallBack local variable will be alive during this processing, so it is perfectly safe to use a pointer to this variable during this time.

On the other hand, if you instead post the message using PostMessage, which returns immediately, you will crash if you are lucky and potentially cause memory corruption otherwise. Indeed, then Button1Click will reach its end, the CallBack local variable will be discarded, and only later will TForm1.Test run -- being given a dangling pointer.

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384