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.

Illustration: Sandra Prasnikar       

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

Popular posts from this blog

Delphi 12.1 & New Quality Portal Released

Assigning result to a function from asynchronous code

Coming in Delphi 12: Disabled Floating-Point Exceptions