Home | Papers | Object Pascal | Decoupling Interfaces and Object Lifetimes
 

Decoupling Interfaces from Object Lifetime Management

Abstract

Interfaces carry with them implicit code that automatically free objects when no longer needed. Under unusual circumstances, one may wish a class to support interfaces yet allows the object creator to retain control of when the class instance is destroyed.

Introduction

An object's lifetime referrs to the conditions under which a class is instantiated, exists, and is destroyed. Lifetime management referrs to ensuring that an object exists at the appropriate times and is destroyed in a timely manner. Interfaces carry with them implicit code that automatically frees an object instance when no longer needed. One must track class instances not bound to an interface to ensure their timely destruction.

Under unusual circumstances, one may wish the best of both worlds -- a class that supports interfaces but yet allows an owning structure to retain control of when class instances are destroyed. This paper discusses once such implmentation.

Warning

Do not meddle in the affairs of wizards, for they are subtle and quick to anger.

This paper describes the internal workings of a minor incantation. Be very judicious when dealing with any degree of magic. It readily causes heartburn. I do not recommend people actually use this code as it solved a very unusual and highly specific problem. I've published this paper as an intellectual exercise, not as a license to dynamite your project!

Object Lifetime Review

Class Instantiation

In Object Pascal, one must always explicitly instantiate a class isntance. One may do this either directly through a class' constructor (listing 1), or indirectly through a function call that calls the class' constructor, sometimes known as a class factory (listing 2).

type
  TFoo = class
    ...
  end;

. . .

var
  foo :TFoo;
begin
  foo := TFoo.Create;
  ...
end;
type
  TFoo = class
    ...
  end;


. . .

//
// This is a class factory for TFoo.
//
function FooFactory:TFoo;
begin
  Result := TFoo.Create;
end;

. . .

var
  foo :TFoo;
begin
  foo := FooFactory;
  ...
end;
  
Listing 1: Direct InstantiationListing 2: Indirect Instantiation Using a Class Factory

Class Destruction

Class destruction happens either explicitly or implicitly. Normally one will explicitly destroy a class by calling its Free method. If the instance will be created, used, then disposed, one normally wraps it in a try ... finally ... end block (listing 3).

Object instances are implicitly freed when one turns over lifetime management to another class (such as attaching a component to a form programmatically), or to an interface (listing 4).

var
  foo :TFoo;
begin
  foo := TFoo.Create;
  try
    foo.Bar;
  finally
    foo.Free;
  end;
procedure TForm1.Button1Click(Sender: TObject);
var
  button :TButton;
begin
  // Passing a form instance (e.g. Self) to the 
  // constructor as the AOwner parameter delegates
  // destruction of the button to the owning form.
  button := TButton.Create(Self);
Listing 3: Explicit DestructionListing 4: Implicit Destruction Through a Form

When an object instance is attached to an interface, the Object Pascal compiler sprinkles some magic in the machine code to automatically handle the object's lifetime (figure 1).

Figure 1: Magic Machine Code Controlling an Interface

The magic code ultimately calls the object instance's _AddRef and _Release methods to track the number of references to the object thus:

  1. Every time a new reference to the object is created, the magic code calls _AddRef, which increases an internal variable called RefCount.
  2. Every time a reference to the object is elimitated, the magic code calls _Release, which performs a two step process. First, it decrements the internal variable called RefCount, and secondly it calls the object's Destroy destructor when RefCount reaches zero.

The Problem Domain

I ran into a performance optimization problem with classes that encapsulate data that is expensive to retrieve without caching.

  1. Each class instance encapsulates a single datum and remain unique per datum regardless of the number of simultaneous references to it. In other words, there may be many instances of the data encapsulating class, but no two instances must ever operate on the same datum.
  2. The class instance must track the number of references to it.
  3. The class must not be destroyed when all references have disappeared, but remain cached in memory on the chance that it may be requested again soon.
  4. Entries in this "retirement" cache may be deleted at any time deemed appropriate by the caching class factory.

This brought about an unusual combination of contstraints:

  1. Constraint 1 suggests a caching class factory that retains knowledge about previously-instantiated classes. Caching may be done easily by retaining a copy of an interface.
  2. Constraint 2 suggests interfaces.
  3. Constraints 3 and 4 throws a wrench into the system. TInterfacedObject contains no mechanism to turn off the autodestruction. Neither does it contain a feedback mechanism to notify the cache that it no longer needs to be retained in memory.

Our Solution

This problem was solved by separating the caching concerns in the class factory, and using a customized base class.

Separation of Caching Concerns

The caching class factory retains handles to two types of objects: those that have active references and those that do not. It retains those that have active references in an "active object" cache. It retains those that do not in a "retirement" cache. Those object instances in the retirement cache will be eliminated in a least recently used (LRU) order when considered sufficiently stale.

In either case, the caching class factory retains a handle to the object instance, not to an interface joined to the object instance. It also retains tight control over the lifetime of every object.

Customized Base Class Design

The customized base class required behaviour that mimics TInterfacedObject with one difference: the final call to _Release does not destroy the instance, but triggers a TNotifyEvent event called OnLastRelease. The object passes itself as the Sender parameter.

We later decided to make this custom destruction conditional. We added a property named AutoDestruct. This causes the object to automatically destruct after calling OnLastRelease. Upon construction, the object by default turns the AutoDestruct behaviour off.

The class' declaration appears in listing 5.

TtcInterfacedChild = class(TObject, IInterface)
protected
  FAutoDestruct: Boolean;
  FOnLastRelease: TNotifyEvent;
  FRefCount: Integer;
  function _AddRef: Integer; stdcall;
  function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  function _Release: Integer; stdcall;
public
  constructor Create;
  destructor Destroy; override;
  property AutoDestruct :Boolean read FAutoDestruct write FAutoDestruct;
  property OnLastRelease: TNotifyEvent read FOnLastRelease write FOnLastRelease;
  property RefCount: Integer read FRefCount;
end;
Listing 5: Base Class Declaration

There is nothing special about the constructor, destructor, _AddRef and QueryInterface (listing 6). Do note that this is simpler than TInterfacedObject since we don't have to worry about internal reference counting destroying the object.

Some may find the use of assembly language objectionable, but I don't believe one can reasonably expect to impose the overhead of critical sections inside of _AddRef and _Release.


function TtcInterfacedChild._AddRef: Integer;
asm
  // Here some assembler magic is needed to ensure thread safety.
  // The LOCK prefix forms a hardware-level critical section around
  // a single machine instruction. The XADD is a single command used
  // to swap values and then add. This atomic read/modify/write
  // retains the original value of FRefCount without danger of
  // another thread modifying FRefCount's value as could happen in
  //   Inc(FRefCount);
  //   Result := FRefCount;
  MOV ECX,Self
  MOV EAX,1
  LOCK XADD [ECX].FRefCount,EAX  // EAX := FRefCount; Inc(FRefCount) safely
  INC EAX
  MOV @Result,EAX
end;

constructor TtcInterfacedChild.Create;
begin
  inherited;
  FAutoDestruct := False;
  FOnLastRelease := nil;
  FRefCount := 0;
end;

destructor TtcInterfacedChild.Destroy;
begin
  Assert(FRefCount=0,ClassName+'.Destroy: Reference count not zero!');
  inherited;
end;

function TtcInterfacedChild.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  Result := IfThen(GetInterface(IID,Obj), S_OK, E_NOINTERFACE);
end;
Listing 6: Uninteresting Method Declarations

The interesting part is the destructor, which is quite simple. It performs the reference count release, and imposes the conditional call to the event handler when the reference count reaches zero. Additionally, it ill destruct itself if AutoDestruct is true.


function TtcInterfacedChild._Release: Integer;
begin
  // Here some assembler magic is needed to ensure thread safety.
  // The LOCK prefix forms a hardware-level critical section around
  // a single machine instruction. The XADD is a single command used
  // to swap values and then add. This atomic read/modify/write
  // retains the original value of FRefCount without danger of
  // another thread modifying FRefCount's value as could happen in
  //   Dec(FRefCount);
  //   Result := FRefCount;
  asm
    MOV ECX,Self
    MOV EAX,-1
    LOCK XADD [ECX].FRefCount,EAX  // EAX := FRefCount; Dec(FRefCount) safely
    DEC EAX
    MOV @Result,EAX
  end;
  if Result>0 then Exit;
  try
    if Assigned(OnLastRelease) then OnLastRelease(Self);
  finally
    if AutoDestruct then Destroy;
  end;
end;


Listing 7: The Modified _Release Method

Use

  1. The caching class factory creates an instance of the active object cache and the retirement cache in its constructor.
  2. When the class factory receives a request for a class, it examines the active object cache for a preëxisting instance. If it finds a preëxisting instance, it returns it in an interface.
  3. If it does not find a preëxisting instance in the active object cache, it looks in the retirement cache. If it finds a preëxisting instance in the retirement cache, it moves the instance to the active object cache and returns the instance in an interface.
  4. If it does not find a preëxisting instance in the retirement cache, it creates a new instance. It assigns the new instance's, OnLastRelease method to an event handler named RetireInstance. It adds the new instance to the object cache, and returns the class instance in an interface.
  5. The class factory's RetireInstance event handler moves the object in the Sender parameter from the active object cache to the retirement cache.

Note the assumption that the caching class factory will not be destroyed until all active proxy instances have been retired. There are no provisions for turning over ownership of data proxy instances to any interface. The class factory's destructor determines whether the active object cache is empty. If it's not empty, then the class factory complains about a design error.


Version History:

2002-02-20
Initial version.
Home | Papers | Object Pascal | Decoupling Interfaces and Object Lifetimes
Copyright © 2012 James Knowles
All rights reserved.