delphi - dinamico - ¿Por qué no pueden dos TBytes usar datos superpuestos?
length of array delphi (4)
Déjame guiarte a través de las formas en que falla este código y cómo el compilador te permite dispararte en el pie.
Si recorres el código utilizando el depurador, puedes ver qué sucede.
Después de la inicialización de Thing1
, puede ver que FData
se llena con todos los ceros.
Curiosamente, Thing2
está bien.
Por lo tanto, el error está en CreateThing
. Vamos a investigar más a fondo ...
En el constructor de nombre extraño CreateThing
tiene la siguiente línea:
FData := @FBuf[1];
Esto parece una tarea simple, pero es realmente una llamada a 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.
Una de las comprobaciones que realiza DynArrayAsg
es verificar si la matriz dinámica de origen está vacía o no.
DynArrayAsg
también hace algunas otras cosas que debe tener en cuenta.
Primero veamos la estructura de una matriz dinámica ; ¡No es solo un simple puntero a una matriz!
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]
se está metiendo con los campos de prefijo de la matriz dinámica.
Los 4 bytes delante de @Fbuf[1]
se interpretan como la longitud.
Para Thing1 estos son:
-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''s a zero length.
Vaya, cuando DynArrayAsg
comienza a investigar, ve que lo que cree que es la fuente de la asignación tiene una longitud de cero, es decir, piensa que la fuente está vacía y no asigna nada. Deja FData
sin cambios!
¿ Thing2
funciona según lo previsto?
Parece que sí, pero en realidad falla de una manera bastante mala, déjame mostrarte.
Has engañado con éxito el tiempo de ejecución para que @FBuf[1]
es una referencia válida a una matriz dinámica.
Debido a esto, el puntero FData
se ha actualizado para apuntar a FBuf[1]
(hasta el momento bueno), y el recuento de referencia de FData se ha incrementado en 1 (no es bueno), también el tiempo de ejecución ha aumentado el bloque de memoria que contiene la dinámica arreglo a lo que cree que es el tamaño correcto para FData
(mal).
-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'' ..............
Oops FData
ahora tiene un número de referencia de 318,767,105 y una longitud de 16,777,216 bytes.
FBuf
también ha aumentado su longitud, pero su referencia ahora es de 257.
Es por eso que necesita la llamada a SetLength
para deshacer la SetLength
masiva de la memoria. Esto todavía no soluciona los recuentos de referencia sin embargo.
La ubicación general puede causar errores de falta de memoria (especialmente en 64 bits) y los recuentos extraños causan una pérdida de memoria porque sus arreglos nunca se liberarán.
La solución
De acuerdo con la respuesta de David: habilitar los punteros {$TYPEDADDRESS ON}
: {$TYPEDADDRESS ON}
Puede corregir el código definiendo FData
como un PAnsiChar
o PByte
normal.
Si se asegura de terminar siempre sus asignaciones a FBuf
con un FData de doble cero, funcionará como se esperaba.
Haz de FData
un TBuffer
así:
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;
Reescribe CreateThing
manera:
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;
No entiendo toda esta mierda acerca de tratar de ser más astuto que el compilador.
¿Por qué no declarar FData así?
type
TMyData = record
DataType: byte;
Buffer: Ansistring;
....
Y trabajar con eso.
Considere el siguiente código XE6. La intención es que ThingData
se escriba en la consola para Thing1
y Thing2
, pero no lo está. ¿Porqué es eso?
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.
El problema se puede ver fácilmente habilitando los punteros de tipo revisado. Agregue esto a la parte superior de su código:
{$TYPEDADDRESS ON}
La documentation dice:
La directiva $ T controla los tipos de valores de puntero generados por el operador @ y la compatibilidad de los tipos de puntero.
En el estado {$ T-}, el resultado del operador @ es siempre un puntero sin tipo (Puntero) que es compatible con todos los demás tipos de punteros. Cuando se aplica @ a una referencia de variable en el estado {$ T +}, el resultado es un puntero escrito que es compatible solo con el puntero y con otros punteros al tipo de la variable.
En el estado {$ T-}, los distintos tipos de punteros que no sean Pointer son incompatibles (incluso si son punteros al mismo tipo). En el estado {$ T +}, los punteros al mismo tipo son compatibles.
Con ese cambio tu programa no se compila. Esta línea falla:
FData := @FBuf[1];
El mensaje de error es:
E2010 Tipos incompatibles:
''System.TArray<System.Byte>''
y''Pointer''
Ahora, FData
es de tipo TArray<Byte>
pero @FBuf[1]
no es una matriz dinámica sino un puntero a un byte en medio de una matriz dinámica. Los dos no son compatibles. Al operar en el modo predeterminado, donde los punteros no se verifican, el compilador le permite cometer este terrible error. Bastante por qué este es el modo predeterminado es completamente más allá de mí.
Una matriz dinámica es más que un puntero al primer elemento: también hay metadatos, como la longitud y el recuento de referencias. Los metadatos se almacenan en un desplazamiento desde el primer elemento. Por lo tanto, todo su diseño es defectuoso. Almacene el código de tipo en una variable separada, y no como parte de la matriz dinámica.
Los arreglos dinámicos son punteros internamente y compatibles con la asignación de los punteros; pero los únicos punteros correctos en el lado derecho de la asignación son nil
u otra matriz dinámica. FData := @FBuf[1];
obviamente está mal, pero es interesante que FData := @FBuf[0];
es probable que esté bien, incluso si $TYPEDADDRESS
está habilitado.
El siguiente código compila y funciona como se espera en 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.
Parece que el compilador "sabe" que @A[0]
es una matriz dinámica, no solo un puntero.
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;