Caves Travel Diving Graphics Mizar Texts Cuisine Lemkov Contact Map RSS Polski
Trybiks' Dive Texts Delphi Objects, interfaces, and memory management in Delphi YAC Software
  Back

List

Charsets

Charts

DBExpress

Delphi

HTML

Intraweb

MSTest

PHP

Programming

R

Rhino Mocks

Software

Testing

UI Testing

VB.NET

VCL

WPF

Objects, interfaces, and memory management in Delphi
It's pretty well known the you shouldn't mix object and interface references in Delphi. Or, more precisely, manual object memory management with interface based reference counted memory management.

BTW, just that requirement there proves that Delphi interfaces are basically botched from start to end... When I have tons of old code (going back even to early Turbo Pascal days) and want to introduce interfaces in some of the places, I can't do that without risking major problems (and yes, I wish I had automated tests for all of my code, but I don't and it just won't happen anytime soon... but I'm trying!).

For instance, consider these declarations:
  type
    I = interface
    end;
  
    T = class(TInterfacedObject, I)
    end;
  
    R = class
      procedure Check(AI: I);
      procedure Test;
    end;
And this code (I'm skipping try..finally sections here just to make the code easier to follow):
  procedure R.Test;
  var
    LR: R;
    LT: T;
  begin
    LR := R.Create;
    LT := T.Create;
    LR.Check(LT);
    LT.Free;
    LR.Free;
  end;
You'll get an access violation exception at LT.Free - because of the interface reference counting mechanism of automated memory management. LT's count starts at 0, is incremented by 1 on the call to LR.Check(LT) and decremented by 1 right after the call setting it back to zero and triggering a Free operation on the LT object. And then, trying to do an LT.Free; again (with LT not being NIL) throws the access violation exception.

A pretty standard example, well know and documented on many blogs. But not that easy to address when you're trying to refactor tons of legacy code... And, actually, not that easy to figure out when you first start using interfaces.

The first solution is to keep reference counted memory management and modify the code to take advantage of it:
  procedure R.Test;
  var
    LR: R;
    LT: T;
  begin
    LR := R.Create;
    LT := T.Create;
    LR.Check(LT);
    LR.Free;
  end;
Even though LT is not freed explicitly in the above, there's no memory leak because LT is deallocated automatically; but then, try to find all such occurrences in a large code base; in code with many object hierarchies, references, etc. When should the object be freed manually? Where exactly does it get freed automatically?

It's not that trivial at all...

So, I much more prefer the second standard solution - turn reference counting off and keep to the old and tested non-reference counted, explicit allocation and deallocations:
  T = class(TInterfacedObject, I)
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;
  
  function T._AddRef: Integer;
  begin
    Result := -1;
  end;
  
  function T._Release: Integer;
  begin
    Result := -1;
  end;
Now the original code works correctly:
  procedure R.Test;
  var
    LR: R;
    LT: T;
  begin
    LR := R.Create;
    LT := T.Create;
    LR.Check(LT);
    LT.Free;
    LR.Free;
  end;
The problem is that, in complex code edited by many developers over many years, things never are that easy...

Just now, I have spent many hours trying to hunt down an access violation bug. It was happening in code similar to this one (for simplicity, I'll be using the declarations introduced above):
  procedure R.Test;
  var
    LR: R;
    LT: T;
  begin
    LR := R.Create;
    LT := T.Create;
    LR.Check(LT as I);
    LT.Free;
    LR.Free;
  end;
Can you spot the difference?

And the problem was, of course, that the exception would be thrown only once in a while, and usually not while trying to debug the thing...

But, finally, I managed to track it down to this innocuous type cast:
  LR.Check(LT as I);
Doesn't look like it should break anything, does it? After all, even though the cast is not strictly needed, the code should run perfectly anyway...

Not so.

What the Delphi compiler does behind the scenes, when it sees the cast, is it introduces a hidden local variable of type I (let's call it LI). And generates code more or less equivalent to this:
  procedure R.Test;
  var
    LR: R;
    LT: T;
    LI: I;
  begin
    LR := R.Create;
    LT := T.Create;
    LI := LT as I; // LI's count incremented to 1
    LR.Check(LI); // LI's count incremented to 2 and decremented to 1
    LT.Free;
    LR.Free;
    LI := NIL; // LI's count decremented to 0
  end;
So, where's the problem?

It's actually with that last assignment: LI := NIL;!

Even though we had turned reference counting off, those _AddRef and _ReleaseRef functions still get called! So, LI := NIL; calls _ReleaseRef on LT, but LT is already freed!

If you try to run this code, you may or may not get an error (depends on the memory manager and what happens in the meantime). To make this reproducible, I usually use code similar to the code below (I have a helper function to do this, actually - here, it's unrolled for clarity). It just makes sure that any freed memory is reallocated and reset to zero:
  procedure R.Test;
  var
    LR: R;
    LT: T;
    k, LSize: Integer;
    p: Pointer;
  begin
    LR := R.Create;
    LT := T.Create;
    LR.Check(LT as I);
    LT.Free;
    LR.Free;
    for k := 1 to 100 do
    begin
      LSize := 4 + Random(100);
      GetMem(p, LSize);
      FillChar(p^, LSize, $FF);
    end;
  end;
After the change, you should always get an AV at the last end; - where the code tries to execute the hidden instruction LI._ReleaseRef; on an invalid pointer to LI/LT...

A bit contrived? Perhaps, though our actual code was much more complex and actually required the cast. Hard to debug? You bet! Lots of time wasted? Oh, yeah! Did I become a fan of Delphi interfaces after that? No, not at all. I have much better things to do with my time than chasing compiler design bugs...

And too bad! I really like the idea of interfaces as contracts. But just contracts, nothing else! Why oh why, did they mix that up with memory management? Or, at the very least, why hadn't they introduced COM interfaces and interface contracts as separate constructs?

Oh well, I don't believe they'll do anything about this anyway. But I hope this text helps somebody in understanding the problems with interfaces in Delphi and will save hours of debugging...

Happy coding!

Top

Comments
#1
AgoraC wrote on 2014-08-23 19:24:57
\"And too bad! I really like the idea of interfaces as contracts. But just contracts, nothing else! Why oh why, did they mix that up with memory management? Or, at the very least, why hadn\'t they introduced COM interfaces and interface contracts as separate constructs? \"

I second that opinion...!!

The worst thing about Delphi. Really annoying
#2
Holden wrote on 2017-01-11 07:32:41
A very good demonstration of Delphi interface problems. I have hunted a mysterious bug for a week, until I have found Your article. It was a revelation to see that other people already encountered the same problem I struggled. Then from Your work it became clear that interfaces are very dangerous things and really annoying bugs may come on using them. But books and Embarcadero do not speak a word from this danger! They introduce interfaces as a good memory management tool but later after a hard bug hunting You realize that You were fooled! And this is very disappointing. And I really hate them who disclaim that garbage collection is unnecessary in Delphi or in Free Pascal. NO! It is the most important thing that would be made in the first place.

Top

Add a comment (fields with an asterisk are required)
Name / nick *
Mail (will remain hidden) *
Your website
Comment (no tags) *
Enter the text displayed below *
 

Top

Tags

Delphi


Related pages

Delphi interfaces... again

Checking "Dangling" Event Handlers in Delphi Forms

Drag-n-drop files onto the application window

Intraweb and MaxConnections

A Case for FreeAndNIL

Intraweb as an Apache DSO module

"Device not supported" in Intraweb

Automated GUI Testing

Rounding and precision on the 8087 FPU

SessionTimeout in Intraweb

Using TChart with Intraweb

Unknown driver: MySQL

TIdMessage's CharSet

Software Guarantees

Automated Testing of Window Forms

TChart - Missing Labels in Axes

Memory Leaks and Connection Explosions in DBExpress

Controlling Conditional Defines and Compilation Switches

Detecting Memory Leaks with DUnit

last_insert_id() and DBExpress

Registering Extensions

DBExpress and Thread Safety

Forms as Frames

Checking Dangling Pointers vs. the New Memory Manager

Accessing Protected Members