The mysterious case of the unexpected implicit interface variable
Asked Answered
A

2

8

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.

Affray answered 18/3, 2011 at 16:6 Comment(1)
BTW: This is one reason why, when mixing object and interface reference paradigms I'd prefer to inherit from TInterfacedObject and manually reference count my object references as needed. (FListener as IUnknown)._AddRef and instead of typical object style destruction... (FListener as IUnknown)._Release (which probably in most cases results in immediate self-destruction).Degradable
H
12

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.

Hooknosed answered 18/3, 2011 at 16:17 Comment(1)
Yes, that's it - see updated question for corresponding code. Thanks, can't believe I missed that.Affray
A
11

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?
Aara answered 18/3, 2011 at 16:27 Comment(1)
Thanks. I guess I need to spend more time learning how to read disassembled code.Affray

© 2022 - 2024 — McMap. All rights reserved.