Question

I ran into a strange situation while testing something today.

I have a number of interfaces and objects. The code looks like this:

IInterfaceZ = interface(IInterface)
['{DA003999-ADA2-47ED-A1E0-2572A00B6D75}']
  procedure DoSomething;
end;

IInterfaceY = interface(IInterface)
  ['{55BF8A92-FCE4-447D-B58B-26CD9B344EA7}']
  procedure DoNothing;
end;

TObjectB = class(TInterfacedObject, IInterfaceZ)
  procedure DoSomething;
end;

TObjectC = class(TInterfacedObject, IInterfaceY)
public
  FTest: string;
  procedure DoNothing;
end;

TObjectA = class(TInterfacedObject, IInterfaceZ, IInterfaceY)
private
  FInterfaceB: IInterfaceZ;
  FObjectC: TObjectC;
  function GetBB: IInterfaceZ;
public
  procedure AfterConstruction; override;
  procedure BeforeDestruction; override;
  property BB: IInterfaceZ read GetBB implements IInterfaceZ;
  property CC: TObjectC read FObjectC implements IInterfaceY;
end;

procedure TObjectB.DoSomething;
begin
  Sleep(1000);
end;

procedure TObjectA.AfterConstruction;
begin
  inherited;
  FInterfaceB := TObjectB.Create;
  FObjectC := TObjectC.Create;
  FObjectC.FTest := 'Testing';
end;

procedure TObjectA.BeforeDestruction;
begin
  FreeAndNil(FObjectC);
  FInterfaceB := nil;
  inherited;
end;

function TObjectA.GetBB: IInterfaceZ;
begin
  Result := FInterfaceB;
end;

procedure TObjectC.DoNothing;
begin
  ShowMessage(FTest);
end;

Now if I access the various implementations like this I get the following results:

procedure TestInterfaces;
var
  AA: TObjectA;
  YY: IInterfaceY;
  ZZ: IInterfaceZ;
  NewYY: IInterfaceY;
begin
  AA := TObjectA.Create;
  // Make sure that the Supports doesn't kill the object. 
  // This line of code is necessary in XE2 but not in XE4
  AA._AddRef;

  // This will add one to the refcount for AA despite the fact
  // that AA has delegated the implementation of IInterfaceY to
  // to FObjectC.
  Supports(AA, IInterfaceY, YY);
  YY.DoNothing;

  // This will add one to the refcount for FInterfaceB.
  // This is also allowing a supports from a delegated interface
  // to another delegated interface.
  Supports(YY, IInterfaceZ, ZZ);
  ZZ.DoSomething;

  // This will fail because the underlying object is actually
  // the object referenced by FInterfaceB.
  Supports(ZZ, IInterfaceY, NewYY);
  NewYY.DoNothing;
end;

The first Supports call, which uses the variable in the implements, returns YY which is actually a reference to TObjectA. My AA variable is reference counted. Because the underlying reference counted object is a TObjectA, the second supports, which uses the interface in the supports call, works and returns me an interface. The underlying object is actually now a TObjectB. The internal object behind FInterfaceB is the object being reference counted. This part makes sense because GetBB is actually FInterfaceB. As expected here, the last call to Supports returns a null for NewYY and the call at the end fails.

My question is this, is the reference counting on TObjectA with the first supports call by design? In other words, when the property that implements the interface is returning an object and not an interface does this mean that the owner object will be the one doing the reference counting? I was always under the impression that implements would also result in the internal delegated object being reference counted instead of the main object.

The declarations are as follows:

  property BB: IInterfaceZ read GetBB implements IInterfaceZ;

With this option above, the internal object behind FInterfaceB is the one that is reference counted.

  property CC: TObjectC read FObjectC implements IInterfaceY;

With this second option above, TObjectA is the one that is being reference counted and not the delegated object FObjectC.

Is this by design?

Edit

I just tested this in XE2 and the behavior is different. The second Supports statement returns nil for ZZ. The debugger in XE4 tells me that the YY is referring to (TObjectA as IInterfaceY). In XE2 it tells me that its a (Pointer as IInterfaceY). Also, in XE2, the AA is not ref counted on the first support statement but the internal FObjectC is reference counted.

Additional Information after the question answered

There is one caveat to this. You can chain the Interface version but not the object version. That means that something like this will work:

TObjectBase = class(TInterfacedObject, IMyInterface)
  …
end;

TObjectA = class(TInterfacedObject, IMyInterface)
  FMyInterfaceBase: IMyInterface;
  property MyDelegate: IMyInterface read GetMyInterface implements IMyInterface;
end;

function TObjectA.GetMyInterface: IMyInterface;
begin
  result := FMyInterfaceBase;
end;

TObjectB = class(TInterfacedObject, IMyInterface)
  FMyInterfaceA: IMyInterface;
  function GetMyInterface2: IMyInterface;
  property MyDelegate2: IMyInterface read GetMyInterface2 implements IMyInterface;
end;

function TObjectB.GetMyInterface2: IMyInterface;
begin
  result := FMyInterfaceA;
end;

But the object version gives a compiler error with this saying that TObjectB doesn't implement the methods for the interface.

TObjectBase = class(TInterfacedObject, IMyInterface)
  …
end;

TObjectA = class(TInterfacedObject, IMyInterface)
  FMyObjectBase: TMyObjectBase;
  property MyDelegate: TMyObjectBase read FMyObjectBase implements IMyInterface;
end;

TObjectB = class(TInterfacedObject, IMyInterface)
  FMyObjectA: TObjectA;
  property MyDelegate2: TObjectA read FMyObjectA implements IMyInterface;
end;

So if you want to start chaining the delegation then you need to stick to interfaces or work around it another way.

Was it helpful?

Solution

tl;dr This is all by design – it's just that the design changes between XE2 and XE3.

XE3 and later

There is quite a difference between delegation to an interface type property and delegation to a class type property. Indeed the documentation calls out this difference explicitly with different sections for the two delegation variants.

The difference from your perspective is as follows:

  • When TObjectA implements IInterfaceY by delegating to class type property CC, the implementing object is the instance of TObjectA.
  • When TObjectA implements IInterfaceZ by delegating to interface type property BB, the implementing object is the object that implements FInterfaceB.

One key thing to realise in all this is that when you delegate to a class type property, the class that is delegated to need not implement any interfaces. So it need not implement IInterface and so need not have _AddRef and _Release methods.

To see this, modify your code's definition of TObjectC to be like so:

TObjectC = class
public
  procedure DoNothing;
end;

You will see that this code compiles, runs, and behaves exactly the same way as does your version.

In fact this is ideally how you would declare a class to which an interface is delegated as a class type property. Doing it this way avoids the lifetime issues with mixing interface and class type variables.

So, let's look at your three calls to Supports:

Supports(AA, IInterfaceY, YY);

Here the implementing object is AA and so the reference count of AA is incremented.

Supports(YY, IInterfaceZ, ZZ);

Here the implementing object is the instance of TObjectB so its reference count is incremented.

Supports(ZZ, IInterfaceY, NewYY);

Here, ZZ is an interface implemented by the instance of TObjectB which does not implement IInterfaceY. Hence Supports returns False and NewYY is nil.

XE2 and earlier

The design changes between XE2 and XE3 coincide with the introduction of the mobile ARM compiler and there were many low-level changes to support ARC. Clearly some of these changes apply to the desktop compilers too.

The behavioural difference that I can find concerns delegation of interface implementation to class type properties. And specifically when the class type in question supports IInterface. In that scenario, in XE2, the reference counting is performed by the inner object. That differs from XE3 which has the reference counting performed by the outer object.

Note that for a class type that does not support IInterface, the reference counting is performed by the outer object in all versions. That makes sense since there's no way for the inner object to do it.

Here's my example code to demonstrate the difference:

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  Intf1 = interface
    ['{56FF4B9A-6296-4366-AF82-9901A5287BDC}']
    procedure Foo;
  end;

  Intf2 = interface
    ['{71B0431C-DB83-49F0-B084-0095C535AFC3}']
    procedure Bar;
  end;

  TInnerClass1 = class(TObject, Intf1)
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    procedure Foo;
  end;

  TInnerClass2 = class
    procedure Bar;
  end;

  TOuterClass = class(TObject, Intf1, Intf2)
  private
    FInnerObj1: TInnerClass1;
    FInnerObj2: TInnerClass2;
  public
    constructor Create;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    property InnerObj1: TInnerClass1 read FInnerObj1 implements Intf1;
    property InnerObj2: TInnerClass2 read FInnerObj2 implements Intf2;
  end;

function TInnerClass1.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TInnerClass1._AddRef: Integer;
begin
  Writeln('TInnerClass1._AddRef');
  Result := -1;
end;

function TInnerClass1._Release: Integer;
begin
  Writeln('TInnerClass1._Release');
  Result := -1;
end;

procedure TInnerClass1.Foo;
begin
  Writeln('Foo');
end;

procedure TInnerClass2.Bar;
begin
  Writeln('Bar');
end;

constructor TOuterClass.Create;
begin
  inherited;
  FInnerObj1 := TInnerClass1.Create;
end;

function TOuterClass.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TOuterClass._AddRef: Integer;
begin
  Writeln('TOuterClass._AddRef');
  Result := -1;
end;

function TOuterClass._Release: Integer;
begin
  Writeln('TOuterClass._Release');
  Result := -1;
end;

var
  OuterObj: TOuterClass;
  I1: Intf1;
  I2: Intf2;

begin
  OuterObj := TOuterClass.Create;

  Supports(OuterObj, Intf1, I1);
  Supports(OuterObj, Intf2, I2);

  I1.Foo;
  I2.Bar;

  I1 := nil;
  I2 := nil;

  Readln;
end.

The output on XE2 is:

TInnerClass1._AddRef
TOuterClass._AddRef
Foo
Bar
TInnerClass1._Release
TOuterClass._Release

The output on XE3 is:

TOuterClass._AddRef
TOuterClass._AddRef
Foo
Bar
TOuterClass._Release
TOuterClass._Release

Discussion

Why did the design change? I cannot answer that definitively, not being privy to the decision making. However, the behaviour in XE3 feels better to me. If you declare a class type variable you would expect its lifetime to be managed as any other class type variable would be. That is, by explicit calls to destructor on the desktop compilers, and by ARC on the mobile compilers.

The behaviour of XE2 on the other hand feels inconsistent. Why should the fact that a property is used for interface implementation delegation change the way its lifetime is managed?

So, my instincts tell me that this was a design flaw, at best, in the original implementation of interface implementation delegation. The design flaw has led to confusion and lifetime management troubles over the years. The introduction to ARC forced Embarcadero to review this issue and they changed the design. My belief is that the introduction of ARC required a design change because Embarcadero have a track record of not changing behaviour unless absolutely necessary.

The paragraphs above are clearly speculation on my part, but that's the best I have to offer!

OTHER TIPS

You are mixing object pointers and interface pointers, which is always a recipe for disaster. TObjectA is not incrementing the reference count of its inner objects to ensure they stay alive for its entire lifetime, and TestInterfaces() is not incrementing the reference count of AA to ensure it survives through the entire set of tests. Object pointers DO NOT participate in reference counting! You have to manage it manually, eg:

procedure TObjectA.AfterConstruction;
begin
  inherited;
  FObjectB := TObjectB.Create;
  FObjectB._AddRef;
  FObjectC := TObjectC.Create;
  FObjectC._AddRef;
  FObjectC.FTest := 'Testing';
end;

procedure TObjectA.BeforeDestruction;
begin
  FObjectC._Release;
  FObjectB._Release;
  inherited;
end;

AA := TObjectA.Create;
AA._AddRef;

Needless to say, manual reference counting undermines the use of interfaces.

When dealing with interfaces, you need to either:

  1. Disable reference counting completely to avoid premature destructions. TComponent, for instance, does exactly that.

  2. Do EVERYTHING using interface pointers, NEVER with object pointers. This ensures proper reference counting across the board. This is generally the preferred solution.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top