Search code examples
delphireference-countingdelphi-10.2-tokyo

Why is this interface not correctly released when the method is exited?


My current code looks like this:

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Winapi.Windows,
  System.Generics.Collections,
  System.SysUtils;

type
  TForm1 = class
  public
    Events: TList<TProc>;
    constructor Create;
    destructor Destroy; override;
  end;

  TTracingInterfacedObject = class(TInterfacedObject)
  public
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

  ISharedPtr<T> = interface
    ['{CC9EE6C5-F07B-40E5-B05D-2DFDBD3404A1}']
    function Get: T;
    function GetRefCount: Integer;
  end;

  ICatalog = interface
    ['{F421BBA8-8DA3-47EE-ADB9-DED26747472E}']
    function GetView: ISharedPtr<TForm1>;
    property View: ISharedPtr<TForm1> read GetView;
  end;

  ITree = interface
    ['{A1E2F71B-124B-48DB-B038-5F90AC5BE94B}']
    function GetId: TGUID;
    property Id: TGUID read GetId;
  end;

  TSharedPtr<T: class> = class(TTracingInterfacedObject, ISharedPtr<T>)
  private
    FObject: T;
  public
    constructor Create(const AObject: T);
    destructor Destroy; override;
    function GetRefCount: Integer;
    function Get: T;
  end;

  TCatalog = class(TTracingInterfacedObject, ICatalog)
  private
    FView: ISharedPtr<TForm1>;
  public
    constructor Create;
    function GetView: ISharedPtr<TForm1>;
  end;

  TTree = class(TTracingInterfacedObject, ITree)
  private
    FView: ISharedPtr<TForm1>;
  public
    constructor Create(const AView: ISharedPtr<TForm1>);
    function GetId: TGUID;
  end;

function TTracingInterfacedObject._AddRef: Integer;
begin
  OutputDebugString(PChar(ClassName + '._AddRef'));
  Result := inherited _AddRef;
end;

function TTracingInterfacedObject._Release: Integer;
begin
  OutputDebugString(PChar(ClassName + '._Release'));
  Result := inherited _Release;
end;

constructor TForm1.Create;
begin
  inherited;
  Events := TList<TProc>.Create;
end;

destructor TForm1.Destroy;
begin
  Events.Free;
  inherited;
end;

constructor TSharedPtr<T>.Create(const AObject: T);
begin
  inherited Create;
  FObject := AObject;
end;

destructor TSharedPtr<T>.Destroy;
begin
  FObject.Free;
  inherited;
end;

function TSharedPtr<T>.Get: T;
begin
  Result := FObject;
end;

function TSharedPtr<T>.GetRefCount: Integer;
begin
  Result := FRefCount;
end;

constructor TCatalog.Create;
begin
  inherited Create;
  FView := TSharedPtr<TForm1>.Create(TForm1.Create) as ISharedPtr<TForm1>;
end;

function TCatalog.GetView: ISharedPtr<TForm1>;
begin
  Result := FView;
end;

constructor TTree.Create(const AView: ISharedPtr<TForm1>);
begin
  inherited Create;
  FView := AView;
end;

function TTree.GetId: TGUID;
begin
  Result := TGUID.Empty;
end;

procedure Main;
var
  Catalog: ICatalog;
  Tree: ITree;
  Func: TFunc<TGUID>;
  Events: TList<TProc>;
  Event: TProc;
begin
  Catalog := TCatalog.Create as ICatalog;

  Events := Catalog.View.Get.Events;

  Event := procedure
    begin
    end;

  Events.Add(Event);

  Tree := TTree.Create(Catalog.View) as ITree;

  Func := function: TGUID
    begin
      Result := Tree.Id;
    end;
end;

begin
  Main;

end.

I have set a breakpoint at the final end. of the application.

The event log looks like this at that point:

Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TCatalog._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TTree._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TCatalog._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Source Breakpoint at $0047F675: C:\Users\Admin\Documents\Embarcadero\Studio\Projects\ViewFail\Project1.dpr line 168. Process Project1.exe (3456)

So:

  • Catalog is _AddRefed once and _Released once which is fine.
  • Tree is _AddRefed once and never _Released which is not what I expected.
  • Catalog.View is _AddRefed four times and _Released only three times which is also not what I expected.

Why does this happen? Is there a reference cycle somewhere that I am missing?


Solution

  • Yes, there is a reference cycle in your code. It is created through anonymous method variable capture mechanism.

    Anonymous methods are backed by reference counted, compiler generated class. Any variables captured by anonymous method are stored as fields in the same class. Compiler instantiates instance of that class and keeps it alive as long as anonymous method is in scope.

    Now, above facts would not be enough to create cycle. But same instance (same class) will be used to back up all anonymous methods within some routine.

    Translated to your code:

    • TForm1 holds Events
    • Catalog holds TForm1
    • Tree holds TForm1

    No cycles there - Tree does not reference Catalog, nor Catalog references Tree

    But, when you look at your Main procedure things change.

    Anonymous methods in Main will be backed by hidden object instance - so let's see what will be there:

    • First anonymous method
    • Second anonymous method
    • Tree - captured by second anonymous method

    Still no visible cycles - but, then you add first anonymous method Event to the Events list that is held by Tree. In order to keep that method alive the whole backing object will be kept alive too.

    Anonymous method object -> Event 
                            -> Tree -> Events -> Event -> Anonymous method object
    

    To break that cycle you have to clear some references. For instance set Tree to nil somewhere in Main.