为什么智能指针的这种优化不起作用?

Why does this optimization of a smartpointer not work?

我一直在研究 Sergey Antonov 的智能指针示例,请参阅:http://blog.barrkel.com/2008/11/reference-counted-pointers-revisited.html(在评论的某处)。

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 解释说:

TFunc = reference to function: T;

is directly equivalent to:

TFunc = interface function Invoke: T; end;

except that locations of a method reference type are assignable using values of a function, method or anonymous method.

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

目标
我想优化智能指针的创建。
为此,我手工制作了 VMT 并使用它来模拟界面。

如果我这样定义 wrap 函数:

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;

一切正常。

如果我将其优化为:

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;

几乎 可以工作,但是一旦调用函数关闭就会给出 AV。

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; 

您希望 AV 发生在 _Release 中,但事实并非如此,实际上发生在这之前。

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

这里EAX = 0018FF40

00419F0E 8B10             mov edx,[eax]
00419F10 FF520C           call dword ptr [edx+[=15=]c]
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 [=15=]419f35
00419F25 8D45FC           lea eax,[ebp-]

这里EAX = 0018FF6C显然应该和之前一样。 它不是的事实是以下 AV 的原因:

00419F28 E87BF6FEFF       call @IntfClear    <<-- AV

IntfClear AV 的调用,因为它无法为 _Release 找到合适的目标。 IOW 呼叫永远不会到达 _Release,而是跳入未知。

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 [=17=]4095bc
System.pas.36039: MOV     DWORD PTR [EAX],0
004095AE C70000000000     mov [eax],[=17=]000000
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+]  <<-- AV here

为什么会这样?我需要调整什么才能使优化版本正常工作?

有效的代码捕获局部变量h。这意味着它的寿命延长了。编译器通过在堆上分配变量 h 来做到这一点。

您的代码没有任何变量捕获。这意味着 h 被分配在堆栈上,其生命周期在函数 returns 时结束。因此,您随后对 h 的引用无效。

扩展@David 的回答:

问题
我 forgot/got 被 TFunc... 实际上是一个指针这一事实吓了一跳。
所以赋值 Result:= TFunc<T>(@h); return 是指向超出范围的局部变量的指针。
@ 标志是我们正在处理指针的死赠品( 也错过了 ))。

现在我们 return 一个超出范围的指针,AV 迟早会跟进。
在这种情况下,稍后会导致 Test 函数成功,但对 _Release 的(隐藏)调用失败。

解决方案
答案是将东西移到堆中并调整 _Release 以进行清理。

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;

现在可以使用了: