I am calling a function that takes:
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.
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.:
@lw
Addr(lw)
Pointer(@lw)^
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
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], ...);
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?
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.
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.