Why does the compiler insist my function is inline

2019-04-09 19:27发布

问题:

Why am I getting

[DCC Error] ProjectCOWArray.dpr(23): E2426 Inline function must not have asm block

program ProjectCOWArray;

{$APPTYPE CONSOLE}

{$R *.res}

type
  PRefCount = ^TRefCount;
  TRefCount = array[0..1] of integer;


  TCOWArray<T> = record
  private
    fData: TArray<T>;
  private
    procedure IncRefCount;  <<-- not inline  
  end;


{ TCOWArray<T> }

procedure TCOWArray<T>.IncRefCount;
asm
  {$if defined(win32)}
  mov eax,fData;
  lock inc dword ptr [eax - 8];
  {$ifend}
  {$if defined(win64)}
  mov rax,fData;
  lock inc dword ptr[rax -12];
  {$ifend}
end;

begin
end.

Delphi XE2 does not have AtomicIncrement, so how do I solve this issue?
It'd like to retain the assembler, because otherwise I cannot have the lock prefix in and I do not want to use InterlockedIncrement because that's a WinAPI function and I do not want that kind of overhead.

回答1:

This is because the generics functionality is implemented on top of the inlining engine. The same restrictions that apply to inline functions apply to generic functions. The compiler writers just haven't taken the extra step to make the error messages specific to generics rather than inline functions.

I think that calling InterlockedIncrement is probably your best option, for Delphi versions that don't have the AtomicIncrement intrinsic. Or, alternatively, create your own version of AtomicIncrement, that is only defined in versions of Delphi that do not include it. And that function can be written in asm. Well, it clearly has to be written in asm of course.

{$IFNDEF AtomicFunctionsAvailable}
function AtomicIncrement(var Target: Integer): Integer;
asm
  ....
end;
{$ENDIF}

Or as @TLama suggests, you can use TInterlocked from the System.SyncObjs unit to provide atomic operations.

With all that said, I see no need to meddle with the internals in this way. Implement a copy of write array by calling SetLength(...) whenever you write to the array. For instance, here's a very simple copy on write array implementation:

unit COWArray;

interface

type
  TCOWArray<T> = record
  private
    FItems: TArray<T>;
    function GetLength: Integer;
    procedure SetLength(Value: Integer);
    function GetItem(Index: Integer): T;
    procedure SetItem(Index: Integer; const Value: T);
  public
    class function New(const Values: array of T): TCOWArray<T>; static;
    property Length: Integer read GetLength write SetLength;
    property Items[Index: Integer]: T read GetItem write SetItem; default;
  end;

implementation

function TCOWArray<T>.GetLength: Integer;
begin
  Result := System.Length(FItems);
end;

procedure TCOWArray<T>.SetLength(Value: Integer);
begin
  System.SetLength(FItems, Value); // SetLength enforces uniqueness
end;

function TCOWArray<T>.GetItem(Index: Integer): T;
begin
  Result := FItems[Index];
end;

procedure TCOWArray<T>.SetItem(Index: Integer; const Value: T);
begin
  System.SetLength(FItems, System.Length(FItems)); // SetLength enforces uniqueness
  FItems[Index] := Value;
end;

class function TCOWArray<T>.New(const Values: array of T): TCOWArray<T>;
var
  i: Integer;
begin
  System.SetLength(Result.FItems, System.Length(Values));
  for i := 0 to high(Values) do
    Result.FItems[i] := Values[i];
end;

end.


回答2:

Even if you could call InterlockedIncrement, you'd still have the obstacle of attaining a reference to the array's reference-count field. You don't really need it, though. Instead, you can let the compiler do the work for you. Declare another array variable and assign to it to increment the count. Then type-cast it to a non-managed type and set it back to a null pointer. The reference count will remain intact even after the variable goes out of scope.

var
  dummy: TArray<T>;
begin
  dummy := fData; // increment reference count
  Pointer(dummy) := nil; // circumvent decrement
end;

This technique works for incrementing the reference count of anything, including arrays, strings, and interfaces.

To decrement is just one line:

begin
  Pointer(dummy) := fData;
  // reference count decrements automatically at scope's end
end;


回答3:

Thanks David for explaining the all important why

A possible solution is to lift the AtomicIncrement out of the generic record and into a hack.

program ProjectCOWArray;

{$APPTYPE CONSOLE}

{$R *.res}

type
  PRefCount = ^TRefCount;
  TRefCount = array[0..1] of integer;

  TCOWArray<T> = record
  private
    fData: TArray<T>;
  private
    procedure IncRefCount;
  end;

{ TCOWArray<T> }

type TAnyOldDynArray = array of integer;

procedure AtomicIncrementDynArrayRefCount(const AnArray: TAnyOldDynArray);
asm
  {$if defined(win32)}
  mov eax,AnArray;
  lock inc dword ptr [eax - 8];
  {$else}
  mov rax,AnArray;
  lock inc dword ptr [rax -12];
  {$ifend}
end;

procedure TCOWArray<T>.IncRefCount;
begin
  AtomicIncrementDynArrayRefCount(TAnyOldDynArray(fData));
end;

begin
end.