Delphi "Supports" increments reference count on [weak] or [unsafe] interfaces
Asked Answered
H

1

11

When I am using Delphi Berlin 10.1 [weak] (and [unsafe]) reference, the "Supports" function and "QueryInterface" both are incrementing the reference count when given an interface variable marked with the "weak" attribute (same behavior with "unsafe" attribute).

program WeakReferences;

{$APPTYPE CONSOLE}

{$R *.res}

uses System.SysUtils;

type
   IAnInterfacedObject = interface
   ['{351DFDA3-42CA-4A1D-8488-494CA454FD9C}']
   end;

   TAnInterfacedObject = class(TInterfacedObject, IAnInterfacedObject)
     protected

      function  GetTheReferenceCount : integer;

     public
      constructor Create;
      destructor  Destroy; override;
      property    TheReferenceCount : integer read GetTheReferenceCount;
   end;

   constructor TAnInterfacedObject.Create;

   begin
      inherited Create;
      writeln('(create AIO instance)');
   end;

   destructor TAnInterfacedObject.Destroy;

   begin
      writeln('(destroy AIO instance)');

      inherited Destroy;
   end;

   function TAnInterfacedObject.GetTheReferenceCount : integer;

   begin
      Result := FRefCount;
   end;

   procedure WithoutSupports;

   var
      AIOinstance : TAnInterfacedObject;

      AIOinterfaced : IAnInterfacedObject;

      [Weak]
      WeakAIOinterfaced : IAnInterfacedObject;

   begin
      AIOinstance := TAnInterfacedObject.Create;
      writeln('created AIO instance; refcount: '+AIOinstance.TheReferenceCount.ToString);

      AIOinterfaced := AIOinstance;
      writeln('create AIO interfaced; refcount: '+AIOinstance.TheReferenceCount.ToString);

      WeakAIOinterfaced := AIOinstance;
      writeln('create WEAK AIO interfaced; refcount: '+AIOinstance.TheReferenceCount.ToString);
   end;

   procedure WithSupports_Weak;

   var
      AIOinstance : TAnInterfacedObject;

      AIOinterfaced : IAnInterfacedObject;

      [Weak]
      WeakAIOinterfaced : IAnInterfacedObject;

   begin
      AIOinstance := TAnInterfacedObject.Create;
      writeln('created AIO instance; refcount: '+AIOinstance.TheReferenceCount.ToString);

      AIOinterfaced := AIOinstance;
      writeln('create AIO interfaced; refcount: '+AIOinstance.TheReferenceCount.ToString);

      Supports(AIOinstance, IAnInterfacedObject, WeakAIOinterfaced);
      writeln('create WEAK AIO interfaced with SUPPORTS; refcount: '+AIOinstance.TheReferenceCount.ToString);
   end;

   procedure WithSupports_Unsafe;

   var
      AIOinstance : TAnInterfacedObject;

      AIOinterfaced : IAnInterfacedObject;

      [Unsafe]
      UnsafeAIOinterfaced : IAnInterfacedObject;

   begin
      AIOinstance := TAnInterfacedObject.Create;
      writeln('created AIO instance; refcount: '+AIOinstance.TheReferenceCount.ToString);

      AIOinterfaced := AIOinstance;
      writeln('create AIO interfaced; refcount: '+AIOinstance.TheReferenceCount.ToString);

      Supports(AIOinstance, IAnInterfacedObject, UnsafeAIOinterfaced);
      writeln('create UNSAFE AIO interfaced with SUPPORTS; refcount: '+AIOinstance.TheReferenceCount.ToString);
   end;

   procedure WithQueryInterface_Weak;

   var
      AIOinstance : TAnInterfacedObject;

      AIOinterfaced : IAnInterfacedObject;

      [Weak]
      WeakAIOinterfaced : IAnInterfacedObject;

   begin
      AIOinstance := TAnInterfacedObject.Create;
      writeln('created AIO instance; refcount: '+AIOinstance.TheReferenceCount.ToString);

      AIOinterfaced := AIOinstance;
      writeln('create AIO interfaced; refcount: '+AIOinstance.TheReferenceCount.ToString);

      AIOinterfaced.QueryInterface(IAnInterfacedObject, WeakAIOinterfaced);
      writeln('create WEAK AIO interfaced with QUERYINTERFACE; refcount: '+AIOinstance.TheReferenceCount.ToString);
   end;

begin
  try
     writeln('--Without "Supports"-------------------');
     WithoutSupports;
     writeln;
     writeln('--With "Supports" - weak-------------------');
     WithSupports_Weak;
     writeln;
     writeln('--With "Supports" - unsafe-------------------');
     WithSupports_Unsafe;
     writeln;
     writeln('--With "QueryInterface" - weak-------------------');
     WithQueryInterface_Weak;

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  readln;
end.

What have I missed here? Is there a "WeakSupports" function? Is this a bug or just a shortcoming of the new "weak" interfaces feature?

Horizon answered 17/2, 2017 at 16:44 Comment(0)
A
9

Delphi Supports (one of overloads - but all other are the same considering out parameter) function is declared as

function Supports(const Instance: IInterface; const IID: TGUID; out Intf): Boolean;

and QueryInterface as

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

Both out parameters are not marked as [weak]. That means that you cannot pass [weak] or [unsafe] interface reference to them. You can only pass strong reference to such parameter to keep reference counting in order.

From documentation Weak References:

Note: You can only pass a [Weak] variable to a var or out parameter that is also marked as [Weak]. You cannot pass a regular strong reference to a [Weak] var or out parameter.

Note: You can only pass an [Unsafe] variable to a var or out parameter that is also marked as [Unsafe]. You cannot pass a regular strong reference to an [Unsafe] var or out parameter.


Additionally your test case has another issue. Unless you are using full-ARC Delphi compilers (currently Android and iOS) you cannot store reference counted object to object reference or you will mess up reference counting. To be more precise, object references under non-ARC compilers do not contribute to reference counting, and you can only use them as temporary access points to perform some operations on object not accessible through declared interfaces. You should always have at least one interface reference to that object instance to keep reference counting in order, and prevent premature object destruction or memory leaks.

Instead of

 var
   AIOinstance : TAnInterfacedObject;
   ...
   AIOinstance := TAnInterfacedObject.Create;

you should always use

var
  AIOinstance : IAnInterfacedObject;
  ...
  AIOinstance := TAnInterfacedObject.Create;
Amoral answered 17/2, 2017 at 18:52 Comment(16)
This is why I find ARC difficult to mix with non-ARC code... so why I never switched to ARC for business code, and still doubt I will ever use the Delphi linux compiler.Vincentvincenta
@ArnaudBouchez I am not sure what you mean. Mixing object references and interface references issues is exactly the thing ARC compiler solves. Of course, for cross-platform code you still have to follow non-ARC compiler rules and never mix those two.Amoral
Dalija, there are valid reasons to create an object with a local object instance and then assign it to an interface later. We often create and initialize an object and only then assign it to an interface.Mariejeanne
@Mariejeanne If class is reference counted, like TinterfacedObject is, storing it's instances into object reference breaks reference counting. Don't do that. You are asking for trouble. If you don't need reference counting then disable reference counting, like TComponent class does. Then you can mix the two.Amoral
I am not saying keep the object, I am saying use it to create and setup the object. For example, begin MyObj := TMyObject.Create; MyObj.Init(xxxx); Result := MyObj; end; where the function returns IMyObject - Your comment at the end "you should always use" is not necessarily practical. Of course, you could create an interface to setup your object but that's unnecessary.Mariejeanne
That code is broken on different level. Even if you get pass potential premature destruction of object, which you most likely will if you only call Init like in your example, you will still end up with wrong reference count when your function returns. That object instance will not get released because it will have reference count of 2 instead of 1.Amoral
I am missing something. How will you get an incorrect reference count? This is a very common pattern with factories. pastebin.com/nYWgdfSeMariejeanne
Use the constructor to initialize, which is also the more idiomatic way to do it on OOPCatcher
@Mariejeanne I guess it is one of those cases where you should not test things on global level because compiler behaves differently than on local level. Your factory works on non-global variables :(Amoral
In fact, I agree with @Mariejeanne that sometimes, I initialize a class instance, then call some tuning methods, then assign the class into an interface, for abstract use. This is pretty common when working with SOLID code.Vincentvincenta
@DalijaPrasnikar This non consistent behavior between ARC and non ARC makes it very difficult to work with, in practice, if you have to maintain also some non-ARC code. And the Free/DisposeOf trick. And the need of explicit [weak] references, including for function parameters. I find it finally more verbose and difficult to debug than Delphi explicit memory management, via Free, or TComponent ownership, or interfaces reference counting (including the smartpointer trick for short-living class instances on stack). But it is very personal.Vincentvincenta
@ArnaudBouchez writing cross-platform code is a bit messy, that we agree. But this question covers non-ARC compiler and all the mess here comes within that compiler itself.Amoral
@Mariejeanne there is semantic difference here. In your initialization examples mixing works because you are actually using object reference only temporary. That is no different than retrieving object from interface reference to do some immediate work. Only thing I would change in mentioned initialization pattern is assigning object to its interface reference immediately after creating it. That will prevent any possible premature destruction.Amoral
In general terms mixing interface and object references considers that you are actually using object reference for permanent storage of object instance and that is wrong.Amoral
I understand and agree which is why I upvoted your answer. My concern was just with the last comment "you should always use" which readers could confuse as something that should be done all of the time. I was trying to clarify that it's not always the case.Mariejeanne
@AgustinOrtu Creating a new constructor is not always ideal in Delphi. How your objects are constructed (e.g. plugins) is important.Mariejeanne

© 2022 - 2024 — McMap. All rights reserved.