Search code examples
delphidelphi-xe6

How to pass dynamic array as untyped const?


I am calling a function that takes:

  • untyped const
  • byte length

as is a common Delphi pattern. For example:

procedure DoStuff(const Data; DataLen: Integer);

In this example test case, all DoStuff does it makes sure it receives the four bytes:

0x21 0x43  0x65  0x87

For testing purposes, we will interpret that byte sequence as a 32-bit unsigned integer on our little-endian Intel machine: 0x87654321. This makes the full test function:

procedure DoStuff(const Data; DataLen: Integer);
var
    pba: PByteArray;
    plw: PLongWord;
begin
    //Interpret data as Longword (unsigned 32-bit)
    plw := PLongWord(@Data);
    if plw^ <> $87654321 then
        raise Exception.Create('Fail');

    //Interpret data as array of bytes
    pba := PByteArray(@Data);
    if (pba[0] <> $21) or (pba[1] <> $43) or (pba[2] <> $65) or (pba[3] <> $87) then
        raise Exception.Create('Fail');

//  ShowMessage('Success');
end;

Error checking has been elucidated for expository purposes.

Testing

I can start testing that i can correctly pass bytes to my DoStuff function.

//Pass four bytes in a LongWord
var lw: LongWord;
lw := $87654321;
DoStuff(lw, 4); //works

And so the way to pass a LongWord to a function that takes an untyped const is to just pass the variable. I.e.:

  • Wrong: @lw
  • Wrong: Addr(lw)
  • Bad: Pointer(@lw)^
  • Correct: lw

I can also pass an 8-byte type; since i only read the first four bytes:

//Pass four bytes in QuadWord
var qw: Int64;
qw := $7FFFFFFF87654321;
DoStuff(qw, 4); //works

Arrays

Now we get something slightly more tricky: passing arrays:

//Pass four bytes in an array
var data: array[0..3] of Byte;
data[0] := $21;
data[1] := $43;
data[2] := $65;
data[3] := $87;
DoStuff(data[0], 4); //Works

//Pass four bytes in a dynamic array
var moreData: TBytes;
SetLength(moreData, 4);
moreData[0] := $21;
moreData[1] := $43;
moreData[2] := $65;
moreData[3] := $87;
DoStuff(moreData[0], 4); //works

These both work correctly. But before some of you balk, let's look at the more tricky cases:

//Pass four bytes at some point in an array
var data: array[0..5] of Byte;
data[2] := $21;
data[3] := $43;
data[4] := $65;
data[5] := $87;
DoStuff(data[2], 4); //Works

//Pass four bytes at some point in a dynamic array
var moreData: TBytes;
SetLength(moreData, 6);
moreData[2] := $21;
moreData[3] := $43;
moreData[4] := $65;
moreData[5] := $87;
DoStuff(moreData[2], 4); //works

Because the untyped const operator implicitly passes by reference, we are passing a reference starting at the 3rd byte in the array (and we have no wasteful temporary array copy).

From this i could infer the rule that if i want to pass an array to an untyped const you pass an indexed array:

DoStuff(data[n], ...);

Two problems

The first problem is that how would i pass an empty dynamic array to an untyped const function. For example:

var
   data: TBytes;
begin
   data := GetData;
   DoStuff(data[0], Length(0));

This general code fails if data is empty (due to a range check error).

The other problem is a critique some have with syntax of passing data[0] rather than simply using data:

//Pass four bytes in an array without using the array index notation
data[0] := $21;
data[1] := $43;
data[2] := $65;
data[3] := $87;
DoStuff(data, 4); //works

That does work. It does mean i lose the ability i had before: passing an indexed array. But a real problem is that it fails when used with a dynamic array:

//Pass four bytes in a dynamic array without using the array index notation
SetLength(moreData, 4);
moreData[0] := $21;
moreData[1] := $43;
moreData[2] := $65;
moreData[3] := $87;
DoStuff(moreData, 4); //FAILS

The problem is that there is an internal implementation detail of how dynamic arrays are implemented. A dynamic array is actually a pointer, whereas an array is actually an array.

Does this mean that if i'm passing an an array i have to figure out which kind it is, and use a different workaround syntax?

//real array
DoStuff(data, 4); //works for real array
DoStuff(data, 4); //fails for dynamic array
DoStuff(Pointer(data)^, 4); //works for dynamic array

Is that really what i am supposed to do? Isn't there a more correct way?

Bonus Workaround

Because i wouldn't want to lose the ability to index an array:

DoStuff(data[67], 4);

i could keep the indexing notation:

DoStuff(data[0], 4);

and simply be sure to handle the edge case:

if Length(data) > 0 then
   DoStuff(data[0], 4)
else
   DoStuff(data, 0); //in this case data is dummy variable

The entire point of all this is to not make copies of data in memory; but to pass it around by reference.


Solution

  • It's quite simple really.

    DoStuff(data, ...);           // for a fixed length array
    DoStuff(Pointer(data)^, ...); // for a dynamic array
    

    is the right way to do this. A dynamic array is a pointer to the first element, or nil if the array is empty.

    Once you abandon type safety and use untyped parameters, it's only reasonable to expect a little more friction when calling such a function.