Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The mysterious case of the unexpected implicit interface variable

I recently came across some behaviour that I simply could not and cannot explain, related to Delphi interface variables.

Essentially, it boils down to an implicit interface variable that the compiler generates in the Broadcast method.

At the end statement that terminates the method, the epilogue code contains two calls to IntfClear. One of which I can explain, it corresponds to the Listener local variable. The other one I cannot explain and it takes you to TComponent._Release (Debug DCUs) after the object instance has been destroyed. It doesn't result in an AV, but that's just lucky, and with full FastMM debug a post-destruction instance access is reported.

Here's the code:

program UnexpectedImplicitInterfaceVariable;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

type
  IListener = interface
    ['{6D905909-98F6-442A-974F-9BF5D381108E}']
    procedure HandleMessage(Msg: Integer);
  end;

  TListener = class(TComponent, IListener)
  //TComponent._AddRef and TComponent_Release return -1
  private
    procedure HandleMessage(Msg: Integer);
  end;

{ TListener }

procedure TListener.HandleMessage(Msg: Integer);
begin
end;

type
  TBroadcaster = class
  private
    FListeners: IInterfaceList;
    FListener: TListener;
  public
    constructor Create;
    procedure Broadcast(Msg: Integer);
  end;

constructor TBroadcaster.Create;
begin
  inherited;
  FListeners := TInterfaceList.Create;
  FListener := TListener.Create(nil);
  FListeners.Add(FListener);
end;

procedure TBroadcaster.Broadcast(Msg: Integer);
var
  i: Integer;
  Listener: IListener;
begin
  for i := 0 to FListeners.Count-1 do
  begin
    Listener := FListeners[i] as IListener;
    Listener.HandleMessage(Msg);
  end;
  Listener := nil;

  FListeners.Clear;
  FreeAndNil(FListener);
end;//method epilogue: why is there a call to IntfClear and then TComponent._Release?

begin
  with TBroadcaster.Create do
  begin
    Broadcast(42);
    Free;
  end;
end.

And here's the disassembly of the epilogue:

enter image description here

There, clear as day, are the two calls to IntfClear.

So, who can see the obvious explanation that I am missing?


UPDATE

Well, Uwe got it straight away. FListeners[i] needs a temporary implicit variable for its result variable. I didn't see that since I was assigning to Listener, but of course that's a different variable.

The following variant is an explicit representation of what the compiler is generating for my original code.

procedure TBroadcaster.Broadcast(Msg: Integer);
var
  i: Integer;
  Intf: IInterface;
  Listener: IListener;
begin
  for i := 0 to FListeners.Count-1 do
  begin
    Intf := FListeners[i];
    Listener := Intf as IListener;
    Listener.HandleMessage(Msg);
  end;
  Listener := nil;

  FListeners.Clear;
  FreeAndNil(FListener);
end;

When written this way it is obvious that Intf cannot be cleared until the epilogue.

like image 279
David Heffernan Avatar asked Mar 18 '11 16:03

David Heffernan


2 Answers

Just a guess, but perhaps the FListeners[i] as IListener uses a temporary variable for FListeners[i]. After all it is the result of a function call.

like image 144
Uwe Raabe Avatar answered Nov 04 '22 07:11

Uwe Raabe


Uwe Raabe is correct, if you look at the code further up:

Project4.dpr.51: Listener := FListeners[i] as IListener;
00441C16 8D4DE0           lea ecx,[ebp-$20]
00441C19 8B55F4           mov edx,[ebp-$0c]
00441C1C 8B45FC           mov eax,[ebp-$04]
00441C1F 8B4004           mov eax,[eax+$04]
00441C22 8B18             mov ebx,[eax]
00441C24 FF530C           call dword ptr [ebx+$0c]
00441C27 8B55E0           mov edx,[ebp-$20]
00441C2A 8D45F0           lea eax,[ebp-$10]
00441C2D B9A81C4400       mov ecx,$00441ca8
00441C32 E8A573FCFF       call @IntfCast

You can see how the result of the FListeners[i] call is placed in [ebp-$20] and then procedure _IntfCast(var Dest: IInterface; const Source: IInterface; const IID: TGUID); is called on that (eax being the target, [ebp-$10], edx the source, [ebp-$20], and ecx the address where the appropriate guid can be found.

You can fix your code by changing the Broadcast method to:

procedure TBroadcaster.Broadcast(Msg: Integer);
var
  i: Integer;
  Intf: IInterface;
  Listener: IListener;
begin
  for i := 0 to FListeners.Count-1 do begin
    Intf := FListeners[i];
    if Supports(Intf, IListener, Listener) then
      Listener.HandleMessage(Msg);
  end;
  Listener := nil;
  Intf := nil;

  FListeners.Clear;
  FreeAndNil(FListener);
end;//method epilogue: why is there a call to IntfClear and then TComponent._Release?
like image 30
Thorsten Engler Avatar answered Nov 04 '22 05:11

Thorsten Engler