6
votes

Here is some sample code, it is a standalone console app in Delphi, it creates an object and then creates an object which is a TInterfacedObject and assigns the Interface reference to a field in a TObject:

program ReferenceCountingProblemProject;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils;
type
  ITestInterface = interface
     ['{A665E2EB-183C-4426-82D4-C81531DBA89B}']
     procedure AnAction;
  end;
  TTestInterfaceImpl = class(TInterfacedObject,ITestInterface)
     constructor Create;
     destructor Destroy; override;

     // implement ITestInterface:
         procedure AnAction;
  end;

  TOwnerObjectTest = class
     public
         FieldReferencingAnInterfaceType1:ITestInterface;
   end;
   constructor TTestInterfaceImpl.Create;
   begin
     WriteLn('TTestInterfaceImpl object created');
   end;
   destructor TTestInterfaceImpl.Destroy;
   begin
     WriteLn('TTestInterfaceImpl object destroyed');
   end;
   procedure TTestInterfaceImpl.AnAction;
   begin
       WriteLn('TTestInterfaceImpl AnAction');
   end;
procedure Test;
var
  OwnerObjectTest:TOwnerObjectTest;
begin
  OwnerObjectTest := TOwnerObjectTest.Create;
  OwnerObjectTest.FieldReferencingAnInterfaceType1 := TTestInterfaceImpl.Create as ITestInterface;
  OwnerObjectTest.FieldReferencingAnInterfaceType1.AnAction;
  OwnerObjectTest.Free;      // This DOES cause the clearing of the interface fields automatically.
  ReadLn; // wait for enter.
end;
begin
   Test;
end.

I wrote this code because I was not sure if, in trivial examples, Delphi would always clear out my interface pointers. Here is the output when the program runs:

TTestInterfaceImpl object created
TTestInterfaceImpl AnAction
TTestInterfaceImpl object destroyed

This is the output I very much hoped to see. The reason I wrote this program is because I am seeing this "contract between me and Delphi" violated in a large Delphi application that I am working on. I am seeing objects NOT be freed, unless I explicitly zero them out in my destructor like this:

 destructor TMyClass.Destroy;
 begin
        FMyInterfacedField := nil; // work around leak.
 end;

My belief is that Delphi is doing its level best to zero these interfaces, and so, when I set a breakpoint on the destructor in the test code above, I get this call stack:

ReferenceCountingProblemProject.TTestInterfaceImpl.Destroy
:00408e5f TInterfacedObject._Release + $1F
:00408d77 @IntfClear + $13
ReferenceCountingProblemProject.ReferenceCountingProblemProject

As you can see a call to @IntfClear is being generated but the lack of a "Free" in the call stack above is slightly confusing me as it appears that the two are causally linked, but not directly in each other's call paths. This suggests to me that the compiler itself emits an @IntfClear in my application at some point after the invocation of the destructor TObject.Free. Am I reading this sign correctly?

My question is: Does Delphi's TObject always guarantee finalization of Fields of Interface types? If not, when will my Interface be cleared for me, and when do I have to manually clear it? Is this finalization of the interface reference implemented as part of TObject, or as part of some general compiler-scope-semantics? What are, in fact, the rules that I should follow as to when to manually zero out an interface, and when to let Delphi do it for me? Imagine I have (as I do have) 200+ classes in my application that store Interfaces as Fields. Do I set them all to Nil in my destructor, or not? How do I decide what to do?

My suspicion is that either (a) TObject provides this guarantee, with the proviso that if you do something stupid, and somehow do not get down to invoking TObject.Destroy on the object that contains the Interface reference Field, you leak both, or (b) that the compiler at a level lower than TObject provides this semantic guarantee, at the level of things going out of scope, and it is this side that leaves me then, scratching my head and unable to explain the complex scenarios I might encounter in the real world.

For trivial cases, like the one where I remove OwnerObjectTest.Free; from the demo above, and you leak both objects that the demo code creates, I have no problem understanding the behaviour of the language/compiler/runtime, but I wish to be sure that I have fully understood what contract or guarantee, if any, exists with respect to Fields in Objects that are of Interface type.

Update By single stepping and declaring my own destructor, I was able to get a different call stack, which makes more sense:

ReferenceCountingProblemProject.TTestInterfaceImpl.Destroy
:00408e5f TInterfacedObject._Release + $1F
:00408d77 @IntfClear + $13
:00405483 TObject.Free + $B
ReferenceCountingProblemProject.ReferenceCountingProblemProject

This appears to show that @IntfClear is invoked BY TObject.Free which is what I very much expected to see.

1
Can you override _AddRef and _Release of your leaked class to log the calls and later see if they were balanced ? Also note that different interfaces of the same object might (though should not) have different _addRef and _Release bodies - Arioch 'The
You should not expect to see IntfClear called directly by TObject.Free. You can see in System.pas that the only function TObject.Free calls is TObject.Destroy. Compiler magic makes the destructor call ClassDestroy. That calls TObject.FreeInstance, which calls TObject.CleanupInstance, which calls FinalizeRecord, which calls FinalizeArray, and that's what calls IntfClear. That all those functions are omitted from the call stack I attribute to optimization or to the RTL playing fast and loose with calling conventions and the debugger being unable to compensate. - Rob Kennedy
The debugger does omit stack frames when it cannot recognise the calling conventions used. - Jeroen Wiert Pluimers
I'm anxious to see a reproducible case where you actually have to set the interface reference to nil. Until then follow Arhioch's advice: make sure you log the _AddRef and _Release calls to further zoom in to see what is going on. - Jeroen Wiert Pluimers
I believe I could reproduce it by taking a reference object and nil-ing it out via an ugly FillChar(PAnsiChar(p),...) hack - Warren P

1 Answers

1
votes

All fields of an object instance are finalised when the object's destructor is executed. That is guaranteed by the runtime. Indeed, all fields of managed types are finalised upon destruction.

The likely explanations for such reference counted objects not being destroyed are:

  1. The destructor is not being executed, or
  2. Something else holds a reference to the object.