Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should the compiler hint/warn when passing object instances directly as const interface parameters?

Should the compiler hint/warn when passing a new instance of an object to a method having a const interface parameter of an interface that the object's class implements?

Edit: The sample of course is simple to illustrate the issue. But in real life it gets way more complex: What if the creation and usage is in code that is far apart (different units, different classes, different projects)? What if it is maintained by different people? What if a non-const parameter becomes a const one, and not all calling code can be checked (because the person changing the code does not have access to all calling code)?

Code like below crashes, and it is very hard to find the cause.

First the log:

1.Run begin

1.RunLeakCrash
 2.RunLeakCrash begin
     NewInstance 1
     AfterConstruction 0
   3.LeakCrash begin
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash end with exception

1.Run end
EInvalidPointer: Invalid pointer operation

Then the code that prematurely releases the object instance implementing an interface:

//{$define all}

program InterfaceConstParmetersAndPrematureFreeingProject;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Windows,
  MyInterfacedObjectUnit in '..\src\MyInterfacedObjectUnit.pas';

procedure Dump(Reference: IInterface);
begin
  Writeln('    4.Dump begin');
  Writeln('    4.Dump Reference=', Integer(PChar(Reference)));
  Writeln('    4.Dump end');
end;

procedure LeakCrash(const Reference: IInterface);
begin
  Writeln('   3.LeakCrash begin');
  try
    Dump(Reference); // now we leak because the caller does not keep a reference to us
    Writeln('   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it');
    Dump(Reference); // we might crash here
  except
    begin
      Writeln('   3.LeakCrash end with exception');
      raise;
    end;
  end;
  Writeln('   3.LeakCrash end');
end;

procedure RunLeakCrash;
begin
  Writeln(' 2.RunLeakCrash begin');
  LeakCrash(TMyInterfacedObject.Create());
  Writeln(' 2.RunLeakCrash end');
end;

procedure Run();
begin
  try
    Writeln('1.Run begin');

    Writeln('');
    Writeln('1.RunLeakCrash');
    RunLeakCrash();

  finally
    Writeln('');
    Writeln('1.Run end');
  end;
end;

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

The EInvalidPointer will manifest itself inside the second call to Dump(Reference);. The reason is that the reference count of the underlying object exposing Reference is already zero, so the underlying object got already destroyed.

A few notes on reference counting code inserted or omitted by the compiler:

  • parameters not marked with const (like in procedure Dump(Reference: IInterface);) get implicit try/finally blocks to perform reference counting.
  • parameters marked with const (like in procedure LeakCrash(const Reference: IInterface);) do not get any reference counting code
  • passing the result of an object instance creation (like LeakCrash(TMyInterfacedObject.Create());) does not generate any reference counting code

Alone all of the above compiler behaviours are very logical, but combined they can cause an EInvalidPointer.
The EInvalidPointer manifests itself only in a very narrow usage pattern.
The pattern is easy to recognize by the compiler, but very hard to debug or find the cause when you trapped into it.
The workaround is pretty simple: cache the result of TMyInterfacedObject.Create() in an intermediate variable, then pass it on to LeakCrash().

Should the compiler hint or warn you about this usage pattern?

Finally the code I used to trace all the _AddRef/_Release/etcetera calls:

unit MyInterfacedObjectUnit;

interface

type
  // Adpoted copy of TInterfacedObject for debugging
  TMyInterfacedObject = class(TObject, IInterface)
  protected
    FRefCount: Integer;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  public
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    class function NewInstance: TObject; override;
    property RefCount: Integer read FRefCount;
  end;

implementation

uses
  Windows;

procedure TMyInterfacedObject.AfterConstruction;
begin
  InterlockedDecrement(FRefCount);
  Writeln('     AfterConstruction ', FRefCount);
end;

procedure TMyInterfacedObject.BeforeDestruction;
begin
  Writeln('     BeforeDestruction ', FRefCount);
  if RefCount <> 0 then
    System.Error(reInvalidPtr);
end;

class function TMyInterfacedObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TMyInterfacedObject(Result).FRefCount := 1;
  Writeln('     NewInstance ', TMyInterfacedObject(Result).FRefCount);
end;

function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  Writeln('     QueryInterface ', FRefCount);
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TMyInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
  Writeln('     _AddRef ', FRefCount);
end;

function TMyInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  Writeln('     _Release ', FRefCount);
  if Result = 0 then
  begin
    Writeln('     _Release Destroy');
    Destroy;
  end;
end;

end.

--jeroen

like image 839
Jeroen Wiert Pluimers Avatar asked Dec 22 '10 12:12

Jeroen Wiert Pluimers


1 Answers

It's a bug. The conversion from instance to interface reference in RunLeakCrash should be to a temporary variable, keeping it alive for the duration of RunLeakCrash.

like image 173
Barry Kelly Avatar answered Oct 09 '22 16:10

Barry Kelly