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
|