Should the compiler hint/warn when passing a new instance of an object to a method having a const interface parameter of an interface that the object's class implements?
Edit: The sample of course is simple to illustrate the issue. But in real life it gets way more complex: What if the creation and usage is in code that is far apart (different units, different classes, different projects)? What if it is maintained by different people? What if a non-const parameter becomes a const one, and not all calling code can be checked (because the person changing the code does not have access to all calling code)?
Code like below crashes, and it is very hard to find the cause.
First the log:
1.Run begin
1.RunLeakCrash
2.RunLeakCrash begin
NewInstance 1
AfterConstruction 0
3.LeakCrash begin
_AddRef 1
4.Dump begin
4.Dump Reference=10394576
4.Dump end
_Release 0
_Release Destroy
BeforeDestruction 0
3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it
_AddRef 1
4.Dump begin
4.Dump Reference=10394576
4.Dump end
_Release 0
_Release Destroy
BeforeDestruction 0
3.LeakCrash end with exception
1.Run end
EInvalidPointer: Invalid pointer operation
Then the code that prematurely releases the object instance implementing an interface:
//{$define all}
program InterfaceConstParmetersAndPrematureFreeingProject;
{$APPTYPE CONSOLE}
uses
SysUtils,
Windows,
MyInterfacedObjectUnit in '..\src\MyInterfacedObjectUnit.pas';
procedure Dump(Reference: IInterface);
begin
Writeln(' 4.Dump begin');
Writeln(' 4.Dump Reference=', Integer(PChar(Reference)));
Writeln(' 4.Dump end');
end;
procedure LeakCrash(const Reference: IInterface);
begin
Writeln(' 3.LeakCrash begin');
try
Dump(Reference); // now we leak because the caller does not keep a reference to us
Writeln(' 3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it');
Dump(Reference); // we might crash here
except
begin
Writeln(' 3.LeakCrash end with exception');
raise;
end;
end;
Writeln(' 3.LeakCrash end');
end;
procedure RunLeakCrash;
begin
Writeln(' 2.RunLeakCrash begin');
LeakCrash(TMyInterfacedObject.Create());
Writeln(' 2.RunLeakCrash end');
end;
procedure Run();
begin
try
Writeln('1.Run begin');
Writeln('');
Writeln('1.RunLeakCrash');
RunLeakCrash();
finally
Writeln('');
Writeln('1.Run end');
end;
end;
begin
try
Run();
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
The EInvalidPointer will manifest itself inside the second call to Dump(Reference);
.
The reason is that the reference count of the underlying object exposing Reference is already zero, so the underlying object got already destroyed.
A few notes on reference counting code inserted or omitted by the compiler:
const
(like in procedure Dump(Reference: IInterface);
) get implicit try/finally blocks to perform reference counting.const
(like in procedure LeakCrash(const Reference: IInterface);
) do not get any reference counting codeLeakCrash(TMyInterfacedObject.Create());
) does not generate any reference counting codeAlone all of the above compiler behaviours are very logical, but combined they can cause an EInvalidPointer.
The EInvalidPointer manifests itself only in a very narrow usage pattern.
The pattern is easy to recognize by the compiler, but very hard to debug or find the cause when you trapped into it.
The workaround is pretty simple: cache the result of TMyInterfacedObject.Create()
in an intermediate variable, then pass it on to LeakCrash()
.
Should the compiler hint or warn you about this usage pattern?
Finally the code I used to trace all the _AddRef/_Release/etcetera calls:
unit MyInterfacedObjectUnit;
interface
type
// Adpoted copy of TInterfacedObject for debugging
TMyInterfacedObject = class(TObject, IInterface)
protected
FRefCount: Integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
class function NewInstance: TObject; override;
property RefCount: Integer read FRefCount;
end;
implementation
uses
Windows;
procedure TMyInterfacedObject.AfterConstruction;
begin
InterlockedDecrement(FRefCount);
Writeln(' AfterConstruction ', FRefCount);
end;
procedure TMyInterfacedObject.BeforeDestruction;
begin
Writeln(' BeforeDestruction ', FRefCount);
if RefCount <> 0 then
System.Error(reInvalidPtr);
end;
class function TMyInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TMyInterfacedObject(Result).FRefCount := 1;
Writeln(' NewInstance ', TMyInterfacedObject(Result).FRefCount);
end;
function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
Writeln(' QueryInterface ', FRefCount);
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TMyInterfacedObject._AddRef: Integer;
begin
Result := InterlockedIncrement(FRefCount);
Writeln(' _AddRef ', FRefCount);
end;
function TMyInterfacedObject._Release: Integer;
begin
Result := InterlockedDecrement(FRefCount);
Writeln(' _Release ', FRefCount);
if Result = 0 then
begin
Writeln(' _Release Destroy');
Destroy;
end;
end;
end.
--jeroen
It's a bug. The conversion from instance to interface reference in RunLeakCrash should be to a temporary variable, keeping it alive for the duration of RunLeakCrash.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With