4

I was testing a block of code for a comment here on StackOverflow and I encountered a situation where the implicit interface variable reared it's head. What I can't see is what is causing it in this case. I have a mini factory that's returning an interface for a newly created object. If I call the method in a procedure block then I only get a reference count of 1. If I call it from the main program block then I get a reference count of 2.

I tested this with Delphi 10 Seattle. Is there a resource that contains the rules for implicit interface creation and is the pattern where an interface is returned by a factory not a reliable pattern?

program TestRefCount;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Vcl.Dialogs;

type
  IMyInterface = interface(IInterface)
    ['{62EB2C46-9B8A-47CE-A881-DB96E6F6437D}']
    procedure DoSomething;
    function GetRefCount: Integer;
  end;

  TMyObject = class(TInterfacedObject, IMyInterface)
  strict private
    FMyValue: Integer;
  public
    procedure Init;
    procedure DoSomething;
    function GetRefCount: Integer;
  end;

  TMyFactory = class(TObject)
  private
    function CreateMyInt: IMyInterface;
  end;

procedure TMyObject.DoSomething;
begin
  MessageDlg(IntToStr(FMyValue), mtInformation, [mbok], 0);
end;

function TMyObject.GetRefCount: Integer;
begin
  Result := FRefCount;
end;

procedure TMyObject.Init;
begin
  FMyValue := 100;
end;

function TMyFactory.CreateMyInt: IMyInterface;
var
  myObject: TMyObject;
begin
  myObject := TMyObject.Create;
  Assert(myObject.GetRefCount = 0);
  myObject.Init;
  Assert(myObject.GetRefCount = 0);
  Result := myObject;
  Assert(myObject.GetRefCount = 1);
  Assert(Result.GetRefCount = 1);
end;

procedure WorkWithIntf;
var
  myFactory: TMyFactory;
  myInt: IMyInterface;
begin
  myFactory := TMyFactory.Create;
  try
    myInt := myFactory.CreateMyInt;
    Assert(myInt.GetRefCount = 1);
    myInt.DoSomething;
    Assert(myInt.GetRefCount = 1);
  finally
    myFactory.Free;
  end;
end;

var
  myFactory: TMyFactory;
  myInt: IMyInterface;
begin
  try
    // This case doesn't have an implicit interface variable
    WorkWithIntf;
    // This case does have an implicit interface variable
    myFactory := TMyFactory.Create;
    try
      myInt := myFactory.CreateMyInt;
      Assert(myInt.GetRefCount = 1); // This fails because the refcount is 2
      myInt.DoSomething;
      Assert(myInt.GetRefCount = 1);
    finally
      myFactory.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Here is the first blocks where there is no implict interface variable:

TestRefCount.dpr.67: myInt := myFactory.CreateMyInt;
005C6A5A 8D55F8           lea edx,[ebp-$08]
005C6A5D 8B45FC           mov eax,[ebp-$04]
005C6A60 E83BFEFFFF       call TMyFactory.CreateMyInt
TestRefCount.dpr.68: Assert(myInt.GetRefCount = 1);
005C6A65 8B45F8           mov eax,[ebp-$08]

Here is the second block where we can see the implicit interface variable:

TestRefCount.dpr.86: myInt := myFactory.CreateMyInt;
005CF513 8D55EC           lea edx,[ebp-$14]
005CF516 A19CB75D00       mov eax,[$005db79c]
005CF51B E88073FFFF       call TMyFactory.CreateMyInt
005CF520 8B55EC           mov edx,[ebp-$14]
005CF523 B8A0B75D00       mov eax,$005db7a0
005CF528 E8C7E3E3FF       call @IntfCopy
TestRefCount.dpr.87: Assert(myInt.GetRefCount = 1); // This fails because the refcount is 2
005CF52D A1A0B75D00       mov eax,[$005db7a0]
Graymatter
  • 6,529
  • 2
  • 30
  • 50

1 Answers1

0

As I understand it, and it's not written down in any documentation, the compiler treats local and global variables differently. With a local, it trusts that the assignment to the local will succeed. Therefore no implicit local is needed. That's your first case.

For a global variable the compiler is more circumspect. It views global variables with suspicion. It emits defensive code in case the assignment to the variable fails. The compiler first assigns to an implicit local, an assignment that it is sure will succeed. Therefore the interface will have its reference count incremented. Then it assigns to the global. If that fails then at least the implicit local has a reference and is able to decrement it and release the interface properly.

You might wonder why the compiler is nervous about assigning to your global variable. You know it is safe, and cannot fail, what is the compiler scared of? Its concept of a global variable is broader. It would consider a pointer to an interface reference, for instance, to be a global. The compiler is trying to defend against that pointer being invalid, the assignment failing, and nobody taking a reference to the interface. The compiler just considers two cases: local and global. It trusts assignment to local variables, and everything else is lumped in with potentially risky globals. Including your perfectly safe global.

In my view the compiler is being over cautious. If the programmer said that the variable can be assigned to, I don't think that it is the compiler's place to doubt it. If the programmer made a mistake, then surely the programmer should be prepared to accept the consequences. Be they leaks as well as runtime memory access failures. But the designers took a different, more conservative approach.

Another scenario where you see an implicit local variable is when you use the as operator on a function return value. For instance:

Foo := GetBar as IFoo;

More on that here: The mysterious case of the unexpected implicit interface variable.

That case is clear cut though. The implicit local variable is essential because it is perfectly reasonable for as to raise an exception.

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490