Hello Old New Leaky Friend
Developers that extensively use interfaces in Delphi probably know about a long-standing issue with the compiler, where interface parameters declared as const
or
[ref]
will cause a memory leak if a reference-counted object instance is
constructed directly at the call site.
Specifically, the following code would create a leak:
procedure LeakTest(const Intf: IInterface);
begin
end;
procedure Run;
begin
LeakTest(TInterfacedObject.Create);
end;
begin
ReportMemoryLeaksOnShutdown := True;
Run;
end.
The problem arises because the compiler does not create a hidden interface reference that would properly initialize the reference counting mechanism and keep the object instance alive during the call. In other words, such an object would never be assigned to a strong interface reference.
Because const
and [ref]
parameters don't trigger reference counting, if
there is no other code within the called routine, the object instance will never
be automatically released.
If the parameter is declared as a normal, value parameter, automatic reference counting will be triggered on the instance when entering the routine, and cleared on exit, resulting in the release of the object instance.
In other words, the following code would not result in a memory leak:
procedure LeakTest(Intf: IInterface);
begin
end;
procedure Run;
begin
LeakTest(TInterfacedObject.Create);
end;
begin
ReportMemoryLeaksOnShutdown := True;
Run;
end.
The issue was reported in Quality portal as https://quality.embarcadero.com/browse/RSP-10100, and there is an even older QC report https://web.archive.org/web/20171220161156/http://qc.embarcadero.com/wc/qcmain.aspx?d=90482.
Because const
parameters are widely used as a performance optimization,
precisely because they don't trigger the reference counting mechanism which is
unnecessary for instances that are already assigned to some strong reference,
using value parameters is not an optimal solution for this problem.
Furthermore, this solution is also very fragile, as a simple change in parameter declaration can suddenly cause problems.
A recently reported regression in the 64-bit Windows compiler has additionally exposed how brittle that solution actually is.
Namely, a value parameter in 64-bit Windows compiler does not trigger reference counting, either as reported in https://embt.atlassian.net/servicedesk/customer/portal/1/RSS-1121.
This is a regression in the 64-bit Windows compiler that happened somewhere in the 10.4.x versions, while the 32-bit compiler does not leak on a non-const parameter. I haven't run tests on non-Windows platforms.
Now, the good news is that this value parameter leak seems to happen only if the reference is not used in the routine. If it is not used then bumping the count is redundant, and because you generally you wouldn't have a parameter that is never used, this does not seem like much of a problem.
Nevertheless, using parameters alone to fix the reference initialization problem is a poor solution, and this was just a reminder that there are better ways to solve the problem.
The simplest solution is explicitly casting the newly created instance as an interface reference, which will create the hidden interface reference that the compiler failed to create automatically for such code.
LeakTest(TInterfacedObject.Create as IInterface);
While this solution is simple, it is something you can easily forget doing. Even
more so, if you only do it for const
or [ref]
parameters. It is also
rather verbose, and if the routine takes multiple parameters, it is not very
readable.
In situations where such subtle code can be the difference between correct and incorrect behavior, having an explicitly declared interface reference that will hold such an object instance is a much less fragile and more readable solution.
procedure Run;
var
Intf: IInterface;
begin
Intf := TInterfacedObject.Create;
LeakTest(Intf);
end;
Remembering that you should never construct any object instance directly when you are passing it as a parameter is a much simpler rule than remembering when you should not do that.
However, introduction of inline variables in Delphi 10.3 Rio, complicates this
rule a bit. If you move the Intf
declaration to make it an inline variable, the leak will
appear again.
procedure Run;
begin
var Intf := TInterfacedObject.Create;
LeakTest(Intf);
end;
What is going on here?
This problem is caused by type inference, which will infer the type from the type
that precedes the constructor which is a TInterfacedObject
, and the resulting
compiler-generated code will be the equivalent of:
var Intf: TInterfacedObject := TInterfacedObject.Create;
In this code you are assigning a reference-counted object instance to an object reference, and object references don't represent strong references. In other words, they don't trigger the reference counting mechanism either.
The solution for this would be either explicitly declaring the reference type or typecasting:
var Intf: IInterface := TInterfacedObject.Create;
or
var Intf := TInterfacedObject.Create as IInterface;
With those solutions, we haven't made much of an improvement from direct typecasting within the procedure call:
LeakTest(TInterfacedObject.Create as IInterface);
When you consider the above options, the least fragile one is adding a static factory function in such classes that will return interface references, and when you use such a function, the reference-counted object instance will always be correctly initialized.
class function TFoo.New: IFoo;
begin
Result := TFoo.Create;
end;
This kind of solution has been used for constructing comparers in
System.Generics.Defaults
, which have a static factory function Construct
. You
can name such functions any way you like. I prefer using New
, as it is shorter.
Comments
Post a Comment