Подтвердить что ты не робот

Почему два TBytes не могут использовать перекрывающиеся данные?

Рассмотрим следующий код XE6. Предполагается, что ThingData должен быть записан на консоль как для Thing1, так и Thing2, но это не так. Почему это?

program BytesFiddle;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TThing = class
  private
    FBuf : TBytes;
    FData : TBytes;
    function GetThingData: TBytes;
    function GetThingType: Byte;
  public
    property ThingType : Byte read GetThingType;
    property ThingData : TBytes read GetThingData;

    constructor CreateThing(const AThingType : Byte; const AThingData: TBytes);
  end;

{ TThing1 }

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + 1);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));

  FData := @FBuf[1];
  SetLength(FData, Length(FBuf) - 1);
end;

function TThing.GetThingData: TBytes;
begin
  Result := FData;
end;

function TThing.GetThingType: Byte;
begin
  Result := FBuf[0];
end;

var
  Thing1, Thing2 : TThing;

begin
  try
    Thing1 := TThing.CreateThing(0, TEncoding.UTF8.GetBytes('Sneetch'));
    Thing2 := TThing.CreateThing(1, TEncoding.UTF8.GetBytes('Star Belly Sneetch'));

    Writeln(TEncoding.UTF8.GetString(Thing2.ThingData));
    Writeln(Format('Type %d', [Thing2.ThingType]));

    Writeln(TEncoding.UTF8.GetString(Thing1.ThingData));
    Writeln(Format('Type %d', [Thing1.ThingType]));

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

Ответ 1

Позвольте мне рассказать вам, как этот код выходит из строя и как компилятор позволяет вам стрелять в ногу.

Если вы пройдете через код с помощью отладчика, вы увидите, что произойдет.

введите описание изображения здесь

После инициализации Thing1 вы можете видеть, что FData заполняется всеми нулями.
Как ни странно, Thing2 отлично.
Поэтому ошибка находится в CreateThing. Пусть далее исследуют...

В нечетно названном конструкторе CreateThing у вас есть следующая строка:

FData := @FBuf[1];

Это выглядит как простое назначение, но на самом деле это вызов DynArrayAssign

Project97.dpr.32: FData := @FBuf[1];
0042373A 8B45FC           mov eax,[ebp-$04]
0042373D 83C008           add eax,$08
00423743 8B5204           mov edx,[edx+$04]
00423746 42               inc edx
00423747 8B0DE03C4000     mov ecx,[$00403ce0]
0042374D E8E66DFEFF       call @DynArrayAsg      <<-- lots of stuff happening here.  

Одна из проверок DynArrayAsg выполняется, чтобы проверить, пуст ли исходный динамический массив или нет.
DynArrayAsg также делает несколько других вещей, о которых вам нужно знать.

Сначала рассмотрим структуру динамического массива; это не просто простой указатель на массив!

Offset 32/64  |   Contents     
--------------+--------------------------------------------------------------
-8/-12        | 32 bit reference count
-4/-8         | 32 or 64 bit length indicator 
 0/ 0         | data of the array.

Выполняя FData = @FBuf[1], вы испортили префиксные поля динамического массива.
4 байта перед @Fbuf[1] интерпретируются как длина.
Для Thing1 это:

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 00 00 00  08 00 00 00  00  'S' 'n' .....
FData:    00 00 00 08  00 00 00 00  .............. //Hey that a zero length.

К сожалению, когда DynArrayAsg начинает расследование, он видит, что то, что, по его мнению, является источником для назначения, имеет длину, равную нулю, то есть полагает, что источник пуст и ничего не назначает. Он оставляет FData неизменным!

Работает ли Thing2 по назначению?
Похоже, это так, но на самом деле это не так плохо, позвольте мне показать вам.

введите описание изображения здесь

Вы успешно обманули среду выполнения, полагая, что @Fbuf[1] является действительной ссылкой на динамический массив.
Из-за этого указатель FData был обновлен, чтобы указать на FBuf[1] (пока это так хорошо), а счетчик ссылок FData был увеличен на 1 (не очень хорошо), а также время выполнения увеличило блок памяти, динамический массив соответствует тому, что он считает правильным размером для FData (плохой).

          -8 (refcnt)  -4 (len)     0 (data)
FBuf:     01 01 00 00  13 00 00 00  01  'S' 'n' .....
FData:    01 00 00 13  00 00 00 01  'S' .............. 

Упс FData теперь имеет коэффициент пересчета 318 767 105 и длину 16 777 216 байт.
FBuf также имеет увеличенную длину, но его refcount теперь составляет 257.

Вот почему вам нужен вызов SetLength, чтобы отменить массовое перекрытие памяти. Это все еще не фиксирует количество ссылок. Общееобнаружение может привести к ошибкам в памяти (особенно на 64-битной основе), а причудливые пересчеты вызовут утечку памяти, потому что ваши массивы никогда не будут освобождены.

Решение
В соответствии с ответом Дэвида: включите типизированные проверенные указатели: {$TYPEDADDRESS ON}

Вы можете исправить код, указав FData как обычный PAnsiChar или PByte.
Если вы всегда выполняете свои назначения до FBuf с двойным нулем, FData будет работать как ожидалось.

Сделайте FData a TBuffer следующим образом:

TBuffer = record
private
  FData : PByte;
  function GetLength: cardinal;
  function GetType: byte;
public
  class operator implicit(const A: TBytes): TBuffer;
  class operator implicit(const A: TBuffer): PByte;
  property Length: cardinal read GetLength;
  property DataType: byte read GetType;
end;

Перепишите CreateThing так:

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
begin
  SetLength(FBuf, Length(AThingData) + Sizeof(AThingType) + 2);
  FBuf[0] := AThingType;
  Move(AThingData[0], FBuf[1], Length(AThingData));
  FBuf[Lengh(FBuf)-1]:= 0;
  FBuf[Lengh(FBuf)-2]:= 0;  //trailing zeros for compatibility with pansichar

  FData := FBuf;  //will call the implicit class operator.
end;

class operator TBuffer.implicit(const A: TBytes): TBuffer;
begin
  Result.FData:= PByte(@A[1]);
end;

Я не понимаю все это, когда вы пытаетесь перехитрить компилятор.
Почему бы просто не объявить FData так:

type
  TMyData = record
    DataType: byte;
    Buffer: Ansistring;  
    ....

И работайте с этим.

Ответ 2

Проблему можно легко увидеть, включив указатели с типом. Добавьте это в начало своего кода:

{$TYPEDADDRESS ON}

В документации говорится:

Директива $T управляет типами значений указателя, генерируемых @и совместимость типов указателей.

В состоянии {$ T-} результат оператора @всегда является нетипизированным указатель (Pointer), который совместим со всеми другими типами указателей. Когда @применяется к ссылке на переменную в состоянии {$ T +}, результатом является типизированный указатель, который совместим только с указателем и с другими указателями на тип переменной.

В состоянии {$ T-} различные типы указателей, отличные от указателя, несовместимы (даже если они являются указателями на один и тот же тип). в {$ T +}, указатели на один и тот же тип совместимы.

С этим изменением ваша программа не скомпилируется. Эта строка не работает:

FData := @FBuf[1];

Сообщение об ошибке:

E2010 Несовместимые типы: 'System.TArray<System.Byte>' и 'Pointer'

Теперь FData имеет тип TArray<Byte>, но @FBuf[1] не является динамическим массивом, а скорее указателем на байт в середине динамического массива. Эти два несовместимы. При работе в режиме по умолчанию, когда указатели не проверяются по типу, компилятор позволяет совершить эту ужасную ошибку. Весьма почему это режим по умолчанию совершенно вне меня.

Динамический массив больше, чем указатель на первый элемент - есть также метаданные, такие как длина и счетчик ссылок. Эти метаданные хранятся со смещением от первого элемента. Следовательно, вся ваша конструкция ошибочна. Сохраните код типа в отдельной переменной, а не как часть динамического массива.

Ответ 3

Динамические массивы являются внутренними указателями и совместимы с указателями; но единственными правильными указателями в правой части присваивания являются nil или другой динамический массив. FData := @FBuf[1]; явно ошибочно, но интересно, что FData := @FBuf[0];, вероятно, ОК, даже если $TYPEDADDRESS включен.

Следующий код компилируется и работает как ожидалось в Delphi XE:

program Project19;

{$APPTYPE CONSOLE}
{$TYPEDADDRESS ON}

uses
  SysUtils;

procedure Test;
var
  A, B: TBytes;

begin
  A:= TBytes.Create(11,22,33);
  B:= @A[0];
  Writeln(B[1]);
end;

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

Похоже, компилятор "знает", что @A[0] - это динамический массив, а не только указатель.

Ответ 4

constructor TThing.CreateThing(const AThingType : Byte; const AThingData: TBytes);
var
  Buffer : array of Byte;
begin
  SetLength(Buffer, Length(AThingData) + Sizeof(AThingType));
  Buffer[0] := AThingType;
  Move(AThingData[0], Buffer[1], Length(AThingData));

  SetLength(FBuf, Length(Buffer));
  Move(Buffer[0], FBuf[0], Length(Buffer));
  SetLength(FData, Length(AThingData));
  Move(Buffer[1], FData[0], Length(AThingData));
end;