Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What guarantees does TObject provide about the clearing of interface fields at object destruction time?

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.

like image 380
Warren P Avatar asked Dec 31 '13 16:12

Warren P


1 Answers

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.
like image 184
David Heffernan Avatar answered Nov 15 '22 04:11

David Heffernan