That stupid garbage collector

I’ve heard this phrase a few times in regards to Delphi’s reference counting mechanics. Before continuing with this post please note — Delphi doesn’t have a Garbage Collector (as Java or .NET)! It uses Reference Counting as part of it’s automatic resource cleanup process. Even if reference counting is a “type of garbage collection” — in today’s world these concepts are totally different and present a separate set of problems which need to be solved.

I will not describe the differences between them, you can check this article to find out more.

OK, moving on … So what types are reference counted in Delphi?

  • Interfaces. Because all interfaces in Delphi are “heavy” (COM) they all derive from IUnknown interface which by default includes 2 methods used for reference counting: AddRef and Release. Delphi developers took a good decision in the early days and add compiler logic to support that.
  • Dynamic Arrays. I can’t tell for sure but I think the decision to reference count these objects had something to do with Strings (which are also dynamic arrays). Anyway, it also removes a lot of burden from the developer.
  • Strings. Pascal language had it’s “static/short string” that was fixed in size but was also just another kind of structure. Those strings were normally declared in the stack – so no cleanup was necessary. I think that was a thing that most Pascal developers loved – so Delphi needed to simulate the same thing but also make strings of any given length. This was probably the motivation to make strings reference counted.
  • Managed Method References. This is a new feature in Delphi 2009. I tried to explain in the previous article why are they reference counted.

It’s all good till now. But what happens when we have a more complicated scenario? Consider the following code:

var
  Arr : array of String;
  S : String;
begin
  { Extend array to 1 element }
  SetLength(Arr, 1);

  { Assign a string }
  S := 'Hello World';

  { Assign a new string (copy) to Arr[0] }
  Arr[0] := S + '. Boom!';
end;

This should probably leak memory you would think. At the function end the cleanup call for the array will be inserted which would free the memory allocated by it. Will the string reference contained in the first element be lost? No – the reason for this is simple: RTTI. The cleanup function for the array will inspect it’s type information and will decide if it must cleanup each element separately. If the element of the array is another reference counted type it will perform specific tasks on it too, and only then the memory of the array will be freed.

So far so good. Consider the next example:

type
  TMyRec = record
    S : String;
  end;

var
  Arr : array of TMyRec;
  S : String;
begin
  { Extend array to 1 element }
  SetLength(Arr, 1);

  { Assign a string }
  S := 'Hello World';

  { Assign a new string (copy) to Arr[0].S }
  Arr[0].S := S + '. Boom!';
end;

No leaks in this case either – records also have RTTI attached so array cleanup function can inspect the structure of that record and detect that another reference counted type is present in it and perform the necessary cleanup steps for it.

If you really want to achieve a leak, this is what you do:

type
  TMyRec = record
    S : String;
  end;

var
  Arr : array of ^TMyRec;
  S : String;

begin
  { Extend array to 1 element }
  SetLength(Arr, 1);

  { Assign a string }
  S := 'Hello World';

  { Assign a new string (copy) to Arr[0]^.S }
  New(Arr[0]);
  Arr[0]^.S := S + '. Boom!';
end;

In this case a pointer to a record will not be cleaned up so you’ve got yourself a leak.

So why do people refer to this technique as stupid? Well, …, some results of reference counting are not that intuitive at first. For example combining referenced counted types and pointers is the worst thing anyone can do in Delphi:

var
  p : ^String;

procedure InitString;
var
  s, x : String;
begin
  s := 'A cool string!';
  x := s + '. Append something to make a copy in' + 
             'memory and generate a new string.';

  p := @x;
end;

begin
  { Call a function that will generate a string }
  InitString();

  { Write the value of the string (pointed to by p) }
  WriteLn(p^);

  { Wait for a key press }
  ReadLn;
end.

Can you spot the problem? Yes, I bet you’re thinking that passing a pointer to the string declared in a function to a global pointer is a bad idea. The fact is that strings are kept in the heap so that should be no problem. What goes wrong here is that s will be disposed upon exit from the function InitString because there are no more references to it anywhere else. The Pointer to String does not count as a reference! This isΒ  just one of the reasons some people would consider Delphi to be stupid.

Another thing you would want to avoid is “playing” with reference counting in interfaces directly:

procedure Test();
var
  Obj : IInterface;

begin
  Obj := TInterfacedObject.Create();
  Obj._Release;
end;

This can result in big problems because at the end of the function another _Release will be added (but the one called directly would already kill the object).

General conclusions are simple:

  1. Either use Delphi’s reference counting mechanics the way these were supposed to be used, or always ensure there are another references to the object.
  2. Do not mix pointers and reference counted types. Pointers to those types do not count as references so you’re going to point disposed memory after it leaves the scope.
  3. If you want pure C-like strings, arrays or classes, use PChar, typed pointers, pointer arithmetic and do not use interfaces. If you do need interfaces, do not use TInterfacedObject as the base class but rather a new one that will not implement reference counting.

P.S. See System._DynArrayClear and System._FinalizeArray functions for more information on how arrays are being cleaned up automatically by Delphi (calls to functions like this are being automatically inserted by the compiler in the code).

Ok, that should be all for tonight πŸ™‚

2 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.