views:

339

answers:

2

Hi, this is a constructed example. I don't want to post the original code here. I tried to extract the relevant parts though.

I have an interface that manages a list of listeners.

TListenerProc = reference to procedure (SomeInt : ISomeInterface);

ISomeInterface = interface
   procedure AddListener (Proc : TListenerProc);   
end;

Now I register a listener:

SomeObj.AddListener (MyListener);

procedure MyListener (SomeInt : ISomeInterface);
begin
  ExecuteSynchronized (procedure
                       begin
                       DoSomething (SomeInt);
                       end);
end;

I do get memory leaks. Both the anonymous method and the interfaces are never freed. I suspect that this is due to some kind of circular reference here. The anonymous method keeps the interface alife and the interface keeps the anonymous method alife.

Two questions:

  1. Do you support that explanation? Or am I missing something else here?
  2. Is there anything I can do about it?

Thanks in advance!


EDIT: It's not so easy to reproduce this in an application small enough to post it here. The best I can do by now is the following. The anonymous method does not get released here:

program TestMemLeak;

{$APPTYPE CONSOLE}

uses
  Generics.Collections, SysUtils;

type
  ISomeInterface = interface;
  TListenerProc  = reference to procedure (SomeInt : ISomeInterface);

  ISomeInterface = interface
  ['{DB5A336B-3F79-4059-8933-27699203D1B6}']
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  end;

  TSomeInterface = class (TInterfacedObject, ISomeInterface)
  strict private
    FListeners          : TList <TListenerProc>;
  protected
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  public
    constructor Create;
    destructor  Destroy; override;
  end;


procedure TSomeInterface.AddListener(Proc: TListenerProc);
begin
FListeners.Add (Proc);
end;

constructor TSomeInterface.Create;
begin
FListeners := TList <TListenerProc>.Create;
end;

destructor TSomeInterface.Destroy;
begin
FreeAndNil (FListeners);
  inherited;
end;

procedure TSomeInterface.NotifyListeners;

var
  Listener : TListenerProc;

begin
for Listener in FListeners do
  Listener (Self);
end;

procedure TSomeInterface.Test;
begin
// do nothing
end;

procedure Execute (Proc : TProc);

begin
Proc;
end;

procedure MyListener (SomeInt : ISomeInterface);
begin
Execute (procedure
         begin
         SomeInt.Test;
         end);
end;

var
  Obj     : ISomeInterface;

begin
  try
    ReportMemoryLeaksOnShutdown := True;
    Obj := TSomeInterface.Create;
    Obj.AddListener (MyListener);
    Obj.NotifyListeners;
    Obj := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
+2  A: 

Looks to me like a definite circular reference issue. Anonymous methods are managed through hidden interfaces, and if the TList<TListenerProc> is owned by the object that ISomeInterface is implemented on, then you've got a circular reference issue.

One possible solution would be to put a ClearListeners method on ISomeInterface which calls .Clear on the TList<TListenerProc>. As long as nothing else is holding a reference to the anonymous methods, that would make them all vanish and drop their references to the ISomeInterface.

I've done a few articles about the structure and implementation of anonymous methods that might help you understand what you're really working with and how they operate a little bit better. You can find them at http://tech.turbu-rpg.com/category/delphi/anonymous-methods.

Mason Wheeler
And where to call the `ClearListeners` method?
Smasher
Sorry if this is a somewhat generic answer, but "during cleanup". Whenever you want all this to go out of scope.
Mason Wheeler
Thanks so far, Mason! Could you have a look at the my edited question and the example code? When I put the call to `ClearListeners` at the end of the main method, the anonymous method is still leaked.
Smasher
+5  A: 

Your code is far from minimal. The following:

program AnonymousMemLeak;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TListenerProc  = reference to procedure (SomeInt : IInterface);

procedure MyListener (SomeInt : IInterface);
begin
end;

var
  Listener: TListenerProc;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    Listener := MyListener;
    Listener := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

has the very same problem (Delphi 2009 here). This can't be worked or designed around. Looks to me like a bug in the compiler.

Edit:

Or maybe this is a problem of the memory leak detection. It has nothing to do with the parameter being an interface, a parameterless procedure leads to the same "leak". Very strange.

mghie
That seems to be a compiler issue. If you move the code (the try block) from the program's main routine to a procedure and then have the main call the procedure, no leak is reported.
Mason Wheeler
Okay, sry for the sample code being too long. I thought that interface reference counting must be involved. Seems that this isn't the case.
Smasher
and +1 for extracting the essence of this problem.
Smasher
@Mason: I discovered that as well. Unfortunately it's not so easy in the original code. So there seems to be no workaround besides avoiding anonymous methods in this spot.
Smasher
@Smasher: Interface reference counting *definitely* is involved, for the procedure reference. When you single-step this in the debugger you will see that the ref count goes from 1 to 2 to 1 during the assignments to `Listener`. It just doesn't go back to zero, or it does only after FastMM4 has already complained. That would also explain why moving this to another procedure works, this introduces another scoping level that can actually be left before the program exits.
mghie
There is a big chance this has to do with unit initialization (and deinitialization) order.
Jeroen Pluimers
@Jeroen: Agreed. Unfortunately for these units the programmer has no influence on initialization order.
mghie
@mghie I don't have time to test myself, but he might want to insert a reference to FastMM4. (I'm in crunch mode to finish a project before teaching a 2 day hands-on workshop on writing quality Delphi apps during the Entwickler Tage in Germany)
Jeroen Pluimers