Why is this smartpointer optimization not working?

I've been playing around with the smartpointer example by Sergey Antonov, see: http://blog.barrkel.com/2008/11/reference-counted-pointers-revisited.html (somewhere in the comments).

SSCCE:

program TestSmartPointer;
{$APPTYPE CONSOLE}    
uses
  System.SysUtils;

type
TInjectType<T> = record
public
  VMT: pointer;
  unknown: IInterface;
  RefCount: integer;
  AValue: T;
end;

TInject<T> = class
public type
  TInjectType = TInjectType<T>;
  PInjectType = ^TInjectType;
end;

PInjectObjectType = TInject<TObject>.PInjectType;


TSmartPointer<T: class> = class
  class function Wrap(const AValue: T): TFunc<T>; static;
end;

function Trick_Release(const obj:  PInjectObjectType): Integer; stdcall; forward;
function Trick_AddRef(const obj: PInjectObjectType): Integer; stdcall; forward;
function Invoke(var obj): TObject; forward;

const
  PSEUDO_VMT: array [0 .. 3] of pointer = (nil, @Trick_AddRef, @Trick_Release, @Invoke);

function Trick_AddRef(const obj: PInjectObjectType): Integer; stdcall;
begin
  Result:= AtomicIncrement(Obj^.RefCount);
end;

function Trick_Release(const obj:  PInjectObjectType): Integer; stdcall;
begin
  Result:= AtomicDecrement(Obj^.RefCount);
  if Result = 0 then obj^.AValue.Free;
end;

function Invoke(const obj:  PInjectObjectType): TObject;
begin
  Result:= obj^.AValue;
end;

class function TSmartPointer<T>.Wrap(const AValue: T): TFunc<T>;
var
  h: TInjectType<T>;
begin
  h.RefCount:= 1;
  pointer(h.unknown):= @h;
  h.VMT:= @PSEUDO_VMT;
  h.AValue:= AValue;
  //Alternative A, this fails
  Result:= TFunc<T>(@h);   
  Inc(h.RefCount);          
  ////Alternative B, this works
  //Result:= function: T      
  //  begin
  //    Result:= h.AValue;
  //  end;
end;

type
  TTestObject = class(TObject)
    procedure Test;
    destructor Destroy; override;
  end;


{ TTestObject }

procedure TTestObject.Test;
begin
  WriteLn('Test');
end;

destructor TTestObject.Destroy;
begin
  WriteLn('Free');
  inherited;
end;

procedure Test;
var
  TestObject: TFunc<TTestObject>;
begin
  TestObject:= TSmartPointer<TTestObject>.Wrap(TTestObject.Create);
  TestObject.Test;
  ReadLn;   //Works up to this point.
  <<<--- generates a AV here.
end;

begin
  WriteLn('Start');
  Test;
  WriteLn('End');
  ReadLn;
end.

      

Barry Kelly explains that:

TFunc = function reference: T;

is a direct equivalent to:

TFunc = interface function Invoke: T; end;

except that the location of the method reference type is assigned using the values ​​of the function, method, or anonymous method.

Anonymous methods are implemented as interfaces that look just like a method reference in a hidden class. Capturing a location is implemented as moving (for local users) and copying (for parameters) to the fields of the hidden class. Any accesses to the captured locations in the main procedure are translated to access the fields of the hidden class; a local variable called $ frame points to an instance of this hidden class.

Objective
I want to optimize the creation of smartpointer.
For this I am using VMT and using it to emulate the interface.

If I define my function wrap

like this:

class function TSmartPointer<T>.Wrap(const AValue: T): TFunc<T>;
var
  h: TInjectType<T>;
begin
  pointer(h.unknown):= @h;
  h.VMT:= @PSEUDO_VMT;
  h.AValue:= AValue;
  Result:= function: T      
    begin
      Result:= h.AValue;
    end;
end;

      

Everything is working.

If I optimize it do the following:

class function TSmartPointer<T>.Wrap(const AValue: T): TFunc<T>;
var
  h: TInjectType<T>;
begin
  h.RefCount:= 1;
  pointer(h.unknown):= @h;
  h.VMT:= @PSEUDO_VMT;
  h.AValue:= AValue;
  //Alternative A, this fails
  Result:= TFunc<T>(@h);   
  Inc(h.RefCount);          
end;

      

It almost works, but gives AV as soon as the calling function closes.

procedure Test;
var
  TestObject: TFunc<TTestObject>;
begin
  TestObject:= TSmartPointer<TTestObject>.Wrap(TTestObject.Create);
  TestObject.Test;
  ReadLn;   
  //Works up to this point.
  <<<--- generates a AV here.
end; 

      

You expect AV to appear in _Release, but it doesn't, in fact it will happen before then.

    TestNewStringHelper.dpr.98: TestObject.Test;
00419F0B 8B45FC           mov eax,[ebp-$04]        

      

Here EAX = 0018FF40

00419F0E 8B10             mov edx,[eax]
00419F10 FF520C           call dword ptr [edx+$0c]
00419F13 E82CFFFFFF       call TTestObject.Test
TestNewStringHelper.dpr.100: end;
00419F18 33C0             xor eax,eax
00419F1A 5A               pop edx
00419F1B 59               pop ecx
00419F1C 59               pop ecx
00419F1D 648910           mov fs:[eax],edx
00419F20 68359F4100       push $00419f35
00419F25 8D45FC           lea eax,[ebp-$04]

      

Here it is EAX = 0018FF6C

obvious that it should be the same as before. The fact that this is not the case is the reason for AV:

00419F28 E87BF6FEFF       call @IntfClear    <<-- AV

      

Calling IntfClear

AV because it cannot find a suitable target for _Release

. The IOW call never reaches _Release, but goes into unknown.

System.pas.36036: MOV     EDX,[EAX]
004095A8 8B10             mov edx,[eax]
System.pas.36037: TEST    EDX,EDX
004095AA 85D2             test edx,edx
System.pas.36038: JE      @@1
004095AC 740E             jz $004095bc
System.pas.36039: MOV     DWORD PTR [EAX],0
004095AE C70000000000     mov [eax],$00000000
System.pas.36043: PUSH    EAX
004095B4 50               push eax
System.pas.36044: PUSH    EDX
004095B5 52               push edx
System.pas.36045: MOV     EAX,[EDX]
004095B6 8B02             mov eax,[edx]
System.pas.36046: CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release
004095B8 FF5008           call dword ptr [eax+$08]  <<-- AV here

      

Why does it do this and what do I need to tweak for the optimized version to work?

+3


source to share


2 answers


The code that works captures a local variable h

. This means that its service life is extended. The compiler does this by allocating the variable h

on the heap.



There is no variable capture in your code. This means that it h

is allocated on the stack and its lifetime ends when the function returns. Thus, your subsequent links are h

invalid.

+2


source


Expanding on @ David's answer:

Problem
I forgot / got blind for what is TFunc...

really a pointer.
Thus, assignment Result:= TFunc<T>(@h);

returns a pointer to a local variable out of scope.
(The sign @

is a dead sale that we are dealing with pointers (missed that too)).

Now, when we return an out-of-scope pointer, AVs are bound to follow sooner or later.
In this case, the execution of the function was later called Test

, but the call (hidden) _Release

failed.

Solution The
answer is to move the material to the heap and set it up _Release

for cleaning.



class function TSmartPointer<T>.Wrap(const AValue: T): TFunc<T>;
type
  TS = TSmartPointer<T>;
  PS = ^TS;
var
  p: PS;
begin
  P:= GetMemory(SizeOf(TS));
  p.RefCount:= 1;
  pointer(p.unknown):= p;
  p.VMT:= @PSEUDO_VMT;
  p.AValue:= AValue;
  pointer(Result):= pointer(TFunc<T>(p));
end;

function Trick_Release(const obj:  PInjectObjectType): Integer; stdcall;
begin
  Result:= AtomicDecrement(Obj^.RefCount);
  WriteLn('Release '+IntToStr(Obj.RefCount));
  if Result = 0 then begin
    obj^.AValue.Free;
    FreeMem(obj);
  end;
end;

      

Now it works:

enter image description here

+1


source







All Articles