Jump to content
Anders Melander

function returning interface

Recommended Posts

Once again I'm struggling with the fact that the compilers Return Value Optimization of interfaces produces code that keeps interfaces alive beyond the scope of the block in which they are referenced.

For example let's say that I have a function CreateFoo that creates a reference counted object and returns an interface to this object:

begin
  var Foo := CreateFoo;
  ...
  Foo := nil;
  ...
end;

One would think it was reasonable to assume that, given no other references to the object besides the Foo variable, once I nill the Foo reference then the reference count goes to zero and the object is destroyed. Right?... Nope.

 

It seems that what the compiler actually produces is something like this:

var
  Gotcha: IUnknown;
begin
  Gotcha := nil;
  try
    begin
      Gotcha := CreateFoo;
      var Foo := Gotcha as IUnknown;
      ...
      Foo := nil;
      ...
    end;
  finally
    Cotcha := nil;
  end;
end;

 

Since this is an old, known problem I wonder if anyone has been able to come up with a work around.

I have mostly been able to work around it in my own code by simply not relying on reference counted objects being destroyed inside local blocks but I have a few places where I simply either have to make this work properly or abandon reference counting.

 

FWIW, the following reproduces the problem:

program FooFail;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  Classes;

type
  TFoo = class(TInterfacedObject)
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TFoo.Create;
begin
  inherited Create;
  WriteLn('  Create');
end;

destructor TFoo.Destroy;
begin
  WriteLn('  Destroy');
  inherited;
end;

function CreateFoo: IUnknown;
begin
  Result := TFoo.Create;
end;

begin
  WriteLn('Begin');
  begin
    var Foo1: IUnknown := TFoo.Create;
    Foo1 := nil; // Foo1 destroyed here
  end;

  begin
    var Foo2: IUnknown := CreateFoo;
    Foo2 := nil;
  end;
  WriteLn('End');
end. // Foo2 destroyed here

The expected desired output is:

Begin
  Create
  Destroy
  Create
  Destroy
End

The actual output is:

Begin
  Create
  Destroy
  Create
End
  Destroy

 

Share this post


Link to post
2 minutes ago, Der schöne Günther said:

With Delphi 11.1, my actual output is exactly what you expect.

Thanks very much. Works in Delphi 11.0 too it seems.

 

This was driving me absolutely crazy because I was so sure that it wasn't a problem anymore but I just couldn't get it working. Some of my projects are in Delphi 10.3 and some are in 11.0 so that explains the confusion. I'll bet that in a year or so I will be in this situation again but at least there's a chance I will find this thread then 🙂

Share this post


Link to post

There are two separate concerns here.

 

First, main application code block can exhibit some weird behavior for global variables declared there, including inline variables. If you want to make proper test of some behavior, I suggest wrapping the code in additional procedure. 

 

Second, inline variables had (and maybe still have) some problems and sometimes using them does not work as expected. If you encounter an issue, usually solution is to declare local variable instead of inline.

 

Your text case does not work correctly in 10.3.3 when code runs in main code block. If moved to the procedure, then it behaves as it should. In 10.4.2 and and 11.2 it works correctly in all scenarios, so the issue was fixed in the meantime.

  • Like 1

Share this post


Link to post
On 9/17/2022 at 11:36 AM, Dalija Prasnikar said:

In 10.4.2 and and 11.2 it works correctly in all scenarios, so the issue was fixed in the meantime.

Not quite fixed it seems 😞

Casting a function result with the interface as operator and the old behavior is back. Reproduced with Delphi 11.2.

 

This works:

begin
  var Foo := FunctionReturningInterface; // RefCount = 1
  Foo := nil; // RefCount = 0
  ...
end;

but this doesn't:

begin
  var Foo := FunctionReturningInterface as IFoo; // RefCount = 2
  Foo := nil; // RefCount = 1
  ...
end; // RefCount = 0

 

Workaround:

begin
  var Foo: IFoo;
  begin
    var := TempFoo := FunctionReturningInterface; // RefCount = 1
    Foo := TempFoo as IFoo; // RefCount = 2
  end; // RefCount = 1
  Foo := nil; // RefCount = 0
  ...
end;

or

function FooFunc: IFoo;
begin
  Result := FunctionReturningInterface as IFoo; // RefCount = 2
end; // RefCount = 1

begin
  var Foo := FooFunc; // RefCount = 1
  Foo := nil; // RefCount = 0
  ...
end;

 

  • Like 1

Share this post


Link to post
49 minutes ago, Der schöne Günther said:

Shouldn't var Foo: IFoo := FunctionReturningInterface() suffice?

Not if FunctionReturningInterface returns an IBar interface or anything other than IFoo.

 

The actual code is an object factory where I pass an interface ID (i.e. a GUID) to a method and get an instance that implements that interface back. Something like this:

var MyDialog := DialogManager.CreateDialog(IMyDialog) as IMyDialog; // CreateDialog returns an IInterface
MyDialog.Execute;

DialogManager itself is also an interface so I can't use generics on it. If it had been an object then I could have done like this instead:

var MyDialog := DialogManager.CreateDialog<IMyDialog>;  // CreateDialog returns an IMyDialog
MyDialog.Execute;

 

Edited by Anders Melander

Share this post


Link to post

I think this should work:

 

var F: IFoo;

Supports(DialogManager.CreateDialog(IMyDialog), IFoo, F);

Share this post


Link to post
1 minute ago, Keesver said:

I think this should work:

Yes, probably. There are plenty of workarounds but that wasn't really the point.

Share this post


Link to post
5 hours ago, Anders Melander said:

Casting a function result with the interface as operator and the old behavior is back. Reproduced with Delphi 11.2.

I suppose you tested those outside the main code block. Have you filed QP report?

Share this post


Link to post
8 minutes ago, Dalija Prasnikar said:

I suppose you tested those outside the main code block.

Actually, I didn't - until now. Same problem, I'm afraid.

I haven't QP'd it and will probably not have time to do so anytime soon; We've just updated our main product suite to Delphi 11.2 (from 10.3) and there's plenty of other stuff that needs my attention.

 

Here's the original test case updated to exhibit the observed behavior:

program FooFailMore;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  Classes;
type
  IFoo = interface
    ['{28036C58-4E0E-422C-AAE1-7DDEFF51C75D}']
    procedure DoFoo;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  private
    procedure DoFoo;
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TFoo.Create;
begin
  inherited Create;
  WriteLn('Create');
end;

destructor TFoo.Destroy;
begin
  WriteLn('Destroy');
  inherited;
end;

procedure TFoo.DoFoo;
begin
  WriteLn('Foo');
end;

function CreateFoo: IUnknown;
begin
  Result := TFoo.Create;
end;

procedure Test;
begin
  WriteLn('Begin');
  begin
    var Foo1 := TFoo.Create as IFoo;
    Foo1.DoFoo;
    Foo1 := nil; // Foo1 destroyed here
  end;

  begin
    var Foo2 := CreateFoo as IFoo;
    Foo2.DoFoo;
    Foo2 := nil;
  end;
  WriteLn('End');
end; // Foo2 destroyed here

begin
  Test;
  ReadLn;
end.

Expected output:

Quote

Begin
Create
Foo
Destroy

Create
Foo

Destroy
End

Actual output:

Quote

Begin
Create
Foo
Destroy

Create
Foo

End
Destroy

 

Share this post


Link to post
1 hour ago, Anders Melander said:

I haven't QP'd it and will probably not have time to do so anytime soon; We've just updated our main product suite to Delphi 11.2 (from 10.3) and there's plenty of other stuff that needs my attention.

I just looked at that code and the problem is not in the inline variable. Casting as interface creates hidden interface reference no matter what and that reference hinders release. This behavior is not a regression and it is not related to inline variables so I wouldn't hope it will be fixed soon.

 

Reported as https://quality.embarcadero.com/browse/RSP-40166 

  • Like 1
  • Thanks 2

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×