Click here to Skip to main content
15,860,844 members
Articles / Programming Languages / Delphi

Fixing Delphi's Interface Limitations

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
12 Jul 2018CPOL8 min read 33.2K   7   19
Delphi has some big gotchas with interfaces. Learn how to bypass them.

Delphi's interfaces are rooted in COM interop, but in most cases are used for non COM purposes. Unfortunately, there are some big limitations with Delphi interfaces, and in a critical area where interfaces are the most useful.

Exception, Not the Rule

I rarely use interfaces. I find that in many cases, they add complexity, rather than solve it. However, there are valid cases for interfaces such as IEnumerable, data binding, IList, etc. Interfaces allow much of what is often sought after with multiple inheritance, but without multiple inheritance's problems.

Object Interface Support

The goal of using an interface is to allow a common API among objects that otherwise do not share a common ancestor other than TObject. Interfaces can also be useful in allowing limited exposure of private members.

In Delphi however, not all objects can be used with interfaces. To use an object with interfaces, one must add special code for reference counting, or descend from (directly or indirectly) one of the specialized classes that already supports interfaces. These classes are:

  • TInterfacedObject
  • TInterfacedPersistent
  • TComponent

There may be others as well, but these are the primary players. Likely it was done this way due to concerns of adding additional baggage to the entire object tree. Unfortunately, this causes some problems as well.

They are declared as follows:

  • TObject
    • TInterfacedObject
    • TPersistent
      • TInterfacedPersistent
      • TComponent

If all of the objects that you wish to use a specified interface on all descend from the same class that adds interface support, all is well. However if you have objects that inherit their interface support from different classes, then there is a big problem. It does not work.

For example, imagine we have an interface ILife and we have 2 classes. One class inherits from TComponent, another from TInterfacedPersistent. Each can implement ILife, but we cannot simply get an ILife interface using the common ancestor TPersistent.

Using TComponent

First, let me show you a simple example where three classes all descend from TComponent.

  • TComponent
    • TComponentA
    • TComponentB
      • TComponentC
Delphi
unit UnitA;

interface

uses
  System.SysUtils, System.Classes;

type
  ILife = interface
    ['{BDC73295-9F45-4BEC-B726-77DEC9B9EAAC}']
    function GetAnswer: Integer;
  end;

  TComponentA = class(TComponent, ILife)
    function GetAnswer: Integer;
  end;
  TComponentB = class(TComponent, ILife)
    function GetAnswer: Integer;
  end;
  TComponentC = class(TComponentB, ILife)
  end;

procedure TestA;

implementation

procedure TestIntf(aComp: TComponent);
var
  i: integer;
  xILife: ILife;
begin
  xILife := aComp as ILife;
  i := xILife.GetAnswer;
  WriteLn('Answer: ' + i.ToString);
end;

procedure TestA;
var
  xCompA, xCompB, xCompC: TComponent;
begin
  WriteLn('TestA');

  xCompA := TComponentA.Create(nil); try
    TestIntf(xCompA);
  finally xCompA.Free; end;

  xCompB := TComponentB.Create(nil); try
    TestIntf(xCompB);
  finally xCompB.Free; end;

  xCompC := TComponentC.Create(nil); try
    TestIntf(xCompC);
  finally xCompC.Free; end;

  WriteLn;
end;

function TComponentA.GetAnswer: Integer;
begin
  Result := 42;
end;

function TComponentB.GetAnswer: Integer;
begin
  Result := 22;
end;

end.

This code works fine and produces the expected output:

Delphi
TestA
Answer: 42
Answer: 22
Answer: 22

The Problem

The problem is that this only works if all of the classes which use ILifeA all descend from TComponent. This limitation largely defeats one of the primary purposes of interfaces. Let's change one of the classes to descend from TInterfacedObject instead. TInterfacedObject is a direct descendant of TObject, and exists only to add interface support.

For example, this will not work:

Delphi
type
  ILife = interface
    ['{CED2DFC6-4E8E-42F2-A724-2E3D8539192F}']
    function GetAnswer: Integer;
  end;

  TObjectB1 = class(TObject, ILifeB)
    function GetAnswer: Integer;
  end;

If you try to compile this, the following error will occur:

Delphi
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface.QueryInterface
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface._AddRef
[dcc32 Error] UnitB.pas(14): E2291 Missing implementation of interface method IInterface._Release
[dcc32 Error] UnitB.pas(27): E2015 Operator not applicable to this operand type
[dcc32 Error] UnitB.pas(38): E2034 Too many actual parameters
[dcc32 Fatal Error] Project1.dpr(9): F2063 Could not compile used unit 'UnitB.pas'

This is because TObject does not have the necessary scaffolding required for interfaces. We can solve this easily by changing the ancestor to TInterfacedObject instead of TObject:

Delphi
type
  ILife = interface
    ['{26621188-5BE3-42B4-A906-2963B480C1F4}']
    function GetAnswer: Integer;
  end;

  TObjectC1 = class(TInterfacedObject, ILife)
  private
    function GetAnswer: Integer;
  public
    function GetFoo: integer;
  end;

All should be good now right? Well, no. There are MORE problems. Look at this code:

Delphi
procedure TestIntf(aObj: TInterfacedObject);
var
  i: integer;
  xILife: ILifeC;
begin
  xILife := aObj as ILife;
  i := xILife.GetAnswer;
  WriteLn('Answer: ' + i.ToString);
end;

procedure TestC_1;
var
  xObjC1: TInterfacedObject;
begin
  WriteLn('TestC_1');

  xObjC1 := TObjectC1.Create; try
    TestIntf(xObjC1);
  finally xObjC1.Free; end;

  WriteLn;
end;

On quick look, one would expect this should work fine. However, it does not because TInterfacedObject changes how objects work when interfaces are actually used, but not when they are not. This code will crash on this statement:

Delphi
xObjC1.Free;

Why? Because when we use the ILife interface and we are done with it, the compiler uses reference counting and frees the whole object. You can see this in action by adding a dummy destructor to TObjectC1. Then set a breakpoint and look at the call stack. The destructor will be called after TestIntf is called.

New Problem

Interfaces when used with classes that descend from TComponent do not exhbit this behavior that for Delphi is non standard in non ARC compilers (Windows). So now objects act differently when used with interfaces depending on their ancestor.

So now we have to just think about sometimes destroy sometimes not? Well, it is not that simple either. Now we don't have to free the object, except sometimes! If no interface is used, then we must NOT free it. If an interface is not used, we MUST free it. The problem is, as the object is passed around to other methods, how are we to know if any code takes an interface for it or not?

If an interface is used, anywhere down the line...

Delphi
xObjC1 := TObjectC1.Create;
TestIntf(xObjC1);
// Do NOT free xObjC1. Delphi freed it for us.

Do we free it or not?

Delphi
xObjC1 := TObjectC1.Create; try
  // GetFoo is not on interface, must call from object.
  // But if GetFoo or anything it calls uses an interface.. then we don't free it.
  // How do we know?
  i := xObjC1.GetFoo;
// We didnt use interface, we have to free it.
finally xObjC1.Free; end;

New hard to find bugs:

Delphi
xObjC1 := TObjectC1.Create;
TestIntf(xObjC1);
// Runs, but runs on "left over memory" and could crash if memory gets modified
// as xObjC1 has already been freed.
i := xObjC1.GetFoo;

When your code logic becomes deeper and you add multiple interfaces to a class, the problem gets even worse.

Some of this can be addressed using unsafe and/or weak directives. However, this isn't simply declared on the declaration, and must be used in user code references. A bad solution in my opinion as well.

The Solution?

The supposed solution is to use interface references instead of object references everywhere for such objects. But this defeats much of the reason that interfaces are used and when multiple interfaces on an object are used, the problem only gets worse.

The Tree Problem

A primary use of interfaces is to expose a common interface from disparate objects. Yet, in many cases, this is not workable in Delphi.

Delphi
type
  ILife = interface
    ['{7C8E0C18-F8A5-43DF-8999-BF17D6EC961C}']
    function GetAnswer: Integer;
  end;

  TComponentA = class(TComponent, ILife)
    function GetAnswer: Integer;
  end;
  TObjectA = class(TInterfacedObject, ILife)
    function GetAnswer: Integer;
  end;
  TPersistentA = class(TInterfacedPersistent, ILife)
    function GetAnswer: Integer;
  end;

Unfortunately, these are largely unusable to obtain an interface in a generic way. This will not compile:

Delphi
procedure TestIntf(aObj: TObject);
var
  i: integer;
  xILife: ILife;
begin
  xILife := aObj as ILife;
  i := xILife.GetAnswer;
  WriteLn('Answer: ' + i.ToString);
end;

Now the obvious solution is to pass the interface instead. However, this is not always practical and again eliminates one of the major benefits of interfaces. There are some work arounds, but then we have the problem that if a class descends from:

  • TComponent - we MUST free it or use Owner to free it.
  • TInterfacedPersistent, we MUST free it.
  • TInterfacedObject
    • If any code anywhere used an interface, we must NOT free it.
    • If no code used an interface, we MUST free it.

Seriously? Someone thought this is a good idea?

TInterfacedObject Misnomer

If TInterfacedObject really must work this way, it should have been called TARCObject or something distinctive. TInterfacedPersistent is TPersistent with interface support, yet TInterfacedObject is an TObject with interface support AND non standard lifecycle management? TInterfacedObject documentation references Memory Management of Interface Objects, but this topic only has two short paragraphs and barely a hint at the problems it introduces.

But Just Use Interfaces!

Yeah. I get it. As discussed prior, there are "workarounds" to using TInterfacedObject by using only interface references. But if you are still thinking this is a "solution", you have totally missed the point.

Using only interface references causes complexity in accessing non interface members. There is also a lack of documentation on this issue. And then, we have the life cycle issues should the executed code paths not use an interface, and multiple interfaces require extra code as well.

A Persistent Problem

TInterfacedPersistent does not have the free/maybe free problem that TInterfacedObject does.

TComponent inherits from TPersistent, but not TInterfacedPersistent.

Delphi
TComponent = class(TPersistent, IInterface, IInterfaceComponentReference)

The VCL declares them like this:

  • TPersistent
    • TInterfacedPersistent
    • TComponent

This means that if we have:

Delphi
type
  ILife = interface
    ['{26621188-5BE3-42B4-A906-2963B480C1F4}']
    function GetAnswer: Integer;
  end;

  TObjectC1 = class(TInterfacedObject, ILife)
  private
    function GetAnswer: Integer;
  public
    function GetFoo: integer;
  end;

  TPersistentC1 = class(TInterfacedPersistent, ILife)
  private
    function GetAnswer: Integer;
  public
    destructor Destroy; override;
  end;

We still cannot use the interface from a TPersistent reference, even though both of them have TPersistent as a base class. If they had declared them this way:

  • TPersistent
    • TInterfacedPersistent
      • TComponent

At least we could have used interfaces properly between TComponent and TInterfacedPersistent. The interface scaffolding is slightly different between the two though and prevents this. Interfaces and TComponent have issues as well. Although TInterfacedPersistent and TComponent have different interface scaffolding, TComponent could still have been changed to inherit from TInterfacedPersistent, and the interface scaffolding (implemented as methods) could have been overridden. This would have provided an easy solution to this problem.

You can also create your own interface scaffolding on your objects or a base to use, however it doesn't solve the tree problem.

Hacking a Solution

There is a hacky solution - one that should not be needed. This could also be done using RTTI (Runtime reflection for non Delphi developers reading this), but not exactly optimal either. To get an interface crossing different interface scaffolding entry points in the tree, one can do:
Delphi
function GetALife(aObject: TPersistent): ILife;
begin
  if aObject is TInterfacedPersistent then begin
    Result := TInterfacedPersistent(aObject) as ILife;
  end else if aObject is TComponent then begin
    Result := TComponent(aObject) as ILife;
  end else begin
    raise Exception.Create('Cannot obtain interface.');
  end;
end;
This provides a reasonably usable solution, although it should not be necessary in the first place. This method requires a function to be added for each interface. I have avoided implementing TInterfacedObject as I will be avoiding it like the plague, but it can be made to work as well but will of course introduce its lifecycle management problems which will differ from when ILife is returned from TInterfacedPersistent and TComponent.

Conclusion

As if I didn't have enough reasons to avoid interfaces, the way Delphi implements them only adds to the baggage and has made interfaces almost completely useless for me as they add far more code and risk than they help. The net gain for me is strongly negative except in rare cases.

Without adding heavy baggage to TObject, it would have been better to build support into it to allow getting a single interface even if there are separate scaffolding implementations. An interface should be an interface, not dependent on specifics of the class itself to determine compatibility as well as lifecycle management. Further more, the life management of TInterfacedObjects as it is implemented makes them a very dangerous class to use and "proper use" of them severely limits their usefulness.

Unless you absolutely need TInterfacedObject, I suggest to completely avoid it and use TInterfacedPersistent instead. TInterfacedPersistent adds RTTI support. If you do not wish to have that overhead, you can make your own TInterfacedObjectThatIsntDrunk by cloning TInterfacedPersistent but changing it to inherit from TObject instead of TPersistent, and making minor adjustments.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Cyprus Cyprus
Chad Z. Hower, a.k.a. Kudzu
"Programming is an art form that fights back"

I am a former Microsoft Regional DPE (MEA) covering 85 countries, former Microsoft Regional Director, and 10 Year Microsoft MVP.

I have lived in Bulgaria, Canada, Cyprus, Switzerland, France, Jordan, Russia, Turkey, The Caribbean, and USA.

Creator of Indy, IntraWeb, COSMOS, X#, CrossTalk, and more.

Comments and Discussions

 
GeneralInterface Handling Pin
Member 1052334722-Oct-19 2:03
Member 1052334722-Oct-19 2:03 
GeneralMy vote of 5 Pin
Member 1433972827-Apr-19 23:52
Member 1433972827-Apr-19 23:52 
AnswerEasy to solve. Pin
Bobo Konijn8-Aug-18 5:28
Bobo Konijn8-Aug-18 5:28 
QuestionYour relation to Embarcadero Pin
degski19-Jul-18 19:35
degski19-Jul-18 19:35 
AnswerRe: Your relation to Embarcadero Pin
Chad Z. Hower aka Kudzu20-Jul-18 3:11
Chad Z. Hower aka Kudzu20-Jul-18 3:11 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 3:28
degski20-Jul-18 3:28 
GeneralRe: Your relation to Embarcadero Pin
Chad Z. Hower aka Kudzu20-Jul-18 3:46
Chad Z. Hower aka Kudzu20-Jul-18 3:46 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 5:05
degski20-Jul-18 5:05 
GeneralRe: Your relation to Embarcadero Pin
Chad Z. Hower aka Kudzu20-Jul-18 5:38
Chad Z. Hower aka Kudzu20-Jul-18 5:38 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 5:51
degski20-Jul-18 5:51 
GeneralRe: Your relation to Embarcadero Pin
Chad Z. Hower aka Kudzu20-Jul-18 5:56
Chad Z. Hower aka Kudzu20-Jul-18 5:56 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 5:58
degski20-Jul-18 5:58 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 6:20
degski20-Jul-18 6:20 
GeneralRe: Your relation to Embarcadero Pin
Chad Z. Hower aka Kudzu20-Jul-18 6:26
Chad Z. Hower aka Kudzu20-Jul-18 6:26 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 17:32
degski20-Jul-18 17:32 
GeneralRe: Your relation to Embarcadero Pin
degski20-Jul-18 18:23
degski20-Jul-18 18:23 
GeneralWorking around it Pin
Olivier Sannier13-Jul-18 3:38
Olivier Sannier13-Jul-18 3:38 
GeneralRe: Working around it Pin
Chad Z. Hower aka Kudzu13-Jul-18 6:51
Chad Z. Hower aka Kudzu13-Jul-18 6:51 
QuestionFeel my pain!!! XD Pin
nortee12-Jul-18 20:54
nortee12-Jul-18 20:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.