Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cast static array to pointer in Delphi?

Tags:

arrays

delphi

Recently when using Delphi arrays, I encounter some problems which make me consider them more carefully.

I write a test function:

procedure TForm1.Button2Click(Sender: TObject);
var
  MyArray1: array[0..0] of UInt64;
  MyArray2: array of UInt64;
  Value1, Value2: UInt64;
begin
  SetLength(MyArray2, 1);

  MyArray1[0] := 100;
  MyArray2[0] := 100;

  Value1 := PUInt64(@MyArray1)^;
  Value2 := PUInt64(@MyArray2)^;

  Value1 := PUInt64(@MyArray1[0])^;
  Value2 := PUInt64(@MyArray2[0])^;

  //Value1 := PUInt64(MyArray1)^;
  Value2 := PUInt64(MyArray2)^;
end;

In my understanding, the static array stores the value of the first element, second element, and so on. while the dynamic array store the address of the array.

Therefore, PUInt64(@MyArray2)^ will actually contain the address of the array in 64bit computer, and half of it contain the address in 32bit computer.

But why PUInt64(MyArray1)^ is an invalid cast?

Also it seems PUInt64(@MyArray2[0])^ is the safest cast since it works on both static and dynamic arrays. Is that correct?

like image 931
alancc Avatar asked Sep 27 '19 07:09

alancc


1 Answers

First let me start by complimenting you for writing such a well-researched question!

Basically, it seems like you got most of it right. In Delphi, a static array is a value type, like a single integer or a record of integers. Using sizeof on any such variable, you get the full size of the data. On the other hand, a dynamic array is a reference type. The variable stores only a single pointer to the actual array data. So sizeof on a dynamic array yields only the size of the pointer.

Of course, this also affects what happens on assignments: if you copy a static array using a := b, you copy all the data, and end up with two independent arrays. If you copy a dynamic array, you only copy the pointer, and end up with two pointers to the same data.

So, back to your code:

Value1 := PUInt64(@MyArray1)^;

Yes, since MyArray1 "is" the data that starts with 100, @MyArray1 is a pointer to the 100 value. The typecast is correct (since the data type in the array is UInt64, the type of a pointer to that value is PUInt64), and by dereferencing, you get 100.

Value2 := PUInt64(@MyArray2)^;

Yes, since MyArray2 is a pointer to the actual data, @MyArray2 is a pointer to the pointer to the actual data. In a 64-bit process, pointers are 64-bit integers, so the typecast is valid. By dereferencing, you get back the original pointer to the actual data. But it is a bug to do this in a 32-bit process.

More simply, you could have written (*)

Value2 := NativeUInt(MyArray2);

Let's continue with

Value1 := PUInt64(@MyArray1[0])^;

This is easy: MyArray1[0] is your 100, and you take the address and then dereference the pointer to get back the original value.

Value2 := PUInt64(@MyArray2[0])^;

Exact same reasoning here.

And

Value2 := PUInt64(MyArray2)^;

This is basically the case I mentioned above (at *), only dereferenced. It is valid in both 32-bit and 64-bit applications. (PUInt64 is of native size because of the P; it may be 32-bit.) Indeed, MyArray2 is a pointer to an UInt64, that is, a PUInt64, so by dereferencing it, you get that UInt64 value (100).

However,

Value1 := PUInt64(MyArray1)^;

is a bug. MyArray1 is, in your case, the exact same thing as a UInt64. It isn't a pointer to such a thing -- it isn't a PUInt64. Of course, you can lie to the compiler and tell it, "hey, treat this as a pointer to a UInt64". But by dereferencing it, you try to get the UInt64 at address 100, which will cause an access violation since you don't own that area in memory (most likely).

In a 64-bit process, the code compiles, since the sizes match. PUInt64 has the size of a pointer (64 bits), and MyArray1 has size 64 bits by your declaration.

In a 32-bit process, the code will not compile, since the sizes do not match. PUInt64 has the size of a pointer (32 bits), but MyArray1 has size 64 bits by your declaration.

Also it seems PUInt64(@MyArray2[0])^ is the safest cast since it works on both static and dynamic arrays. Is that correct?

Generally, I feel that static and dynamic arrays are two very different things, so I wouldn't attempt to force code to look the same in both cases.

Are you trying to get the first value in the array? If so, simply write MyArray2[0]. (Or MyArray1[0] in the static array case.) Are you trying to get the address of the first element? If so, I prefer NativeUInt(MyArray2) instead of NativeUInt(@MyArray2[0]) (or pointer, or PUInt64). Indeed, if the array is empty, the first method yields 0 or nil, while the other is a bug.

Update:

To clarify, if a is a static array, the address to the first element is simply @a (or @a[0]). If b is a dynamic array, the address to the first element is pointer(b) (or @b[0]). You may cast any pointer to some other pointer-sized type, like NativeUInt (integer type) or PUInt64 (specific pointer type) or PCardinal (specific pointer type) or ... . That will not change the actual value, only its compile-time interpretation.

like image 147
Andreas Rejbrand Avatar answered Nov 19 '22 20:11

Andreas Rejbrand