Search code examples
delphievent-handlingdelphi-xe5

A regular procedure assigned to TGetStrProc event receives an empty string parameter


I tried to make an event handler out of a normal procedure, like I always did for TNotifyEvent, but this doesn't seem to work with TGetStrProc. The handler receives an empty string parameter.

program ProcAsTGetStrProc;

uses
  System.Classes, Winapi.Windows, System.SysUtils;

type
  TMyObject = class
  strict private
    _onLog: TGetStrProc;
    procedure _log(const msg: string);
  public
    procedure DoTheWork();
    property OnLog: TGetStrProc read _onLog write _onLog;
  end;

procedure mbox(msg: string);
begin
  MessageBox(0, PWideChar(msg), 'Test', 0);
end;

procedure TMyObject.DoTheWork();
begin
  _log('Doing the work');
end;

procedure TMyObject._log(const msg: string);
begin
  mbox(Format('TMyObject._log: "%s"', [msg]));
  if Assigned(_onLog) then _onLog(msg);
end;

procedure ProcLogging(const msg: string);
begin
  mbox(Format('ProcLogging: "%s"', [msg]));
end;

function MakeMethod(Data, Code: Pointer): TMethod;
begin
  Result.Data := Data;
  Result.Code := Code;
end;

var
  obj: TMyObject;

begin
  obj := TMyObject.Create();
  try
    obj.OnLog := TGetStrProc(MakeMethod(nil, @ProcLogging));
    obj.DoTheWork();
  finally
    obj.Free();
  end;
end.

Expected output

TMyObject._log: "Doing the work"

ProcLogging: "Doing the work"

Actual output

TMyObject._log: "Doing the work"

ProcLogging: ""

What could be wrong here?


Solution

  • The signature of your standalone ProcLogging() procedure is wrong.

    TGetStrProc is declared like this:

    TGetStrProc = procedure(const S: string) of object;
    

    An of object reference expects a non-static class method to be assigned to it, which means there is an implicit Self parameter involved. But there is no Self parameter in your ProcLogging() procedure.

    Behind the scenes, of object is implemented using the TMethod record, where TMethod.Code is a pointer to the start of the method's implementation code, and the TMethod.Data is the value of the method's Self parameter.

    The statement

    if Assigned(_onLog) then _onLog(msg);
    

    Is roughly equivalent to:

    if _onLog.Code <> nil then _onLog.Code(_onLog.Data, msg);
    

    So, when you use TMethod directly to call a standalone procedure, you have to declare the Self parameter explicitly to receive the TMethod.Data value and subsequent parameters correctly, eg:

    procedure ProcLogging(Self: Pointer; const msg: string);
    begin
      mbox(Format('ProcLogging: "%s"', [msg]));
    end;
    

    Whatever you assign to the TMethod.Data field will be passed as-is to the Self parameter. Specifying nil (as you are) is valid in this context, as long as the standalone procedure does not use it for anything.

    Alternatively, you can use a non-instance class method instead of a standalone procedure. A class method also has an implicit Self parameter (receiving a pointer to a class type rather than a pointer to an object instance), and thus is compatible with of object. However, a class method can be assigned as-is to an of object reference, so you don't have to mess around with TMethod directly, eg:

    type
      TLog = class
      public
        class procedure ProcLogging(const msg: string);
      end;
    
    class procedure TLog.ProcLogging(const msg: string);
    begin
      mbox(Format('ProcLogging: "%s"', [msg]));
    end;
    
    ...
    obj.OnLog := TLog.ProcLogging;
    ...