Casting to record types and use of absolute

Everyone knows the possibility to cast the types explicitly in Delphi. Often we cast the Integer to Pointer, TObject or one class to another class etc. But in simple cases cast operations are relatively simple to understand and you even do not need to go into details about how it works. Some can implement class operator for implicit or explicit casting as well and that’s fine.

But when it comes to casting to record types the thing becomes interesting and not-so-obvious sometimes (maybe even freaky a bit 😉 ).

Record

type
  TStr3Bytes = record
    A, B, C: Byte;
  end;

var
  S: String[2];
  X: TStr3Bytes;
begin
  S := 'A1';
  X := TStr3Bytes(S);
  Writeln(X.A, ' ', X.B, ' ', X.C);
  Readln;
end.

The example above compiles fine and outputs you the following values:

2 65 49

And that happened because when the cast happened the X value received the copy of the memory of the S variable and as we know shortstring contains the length in the first byte (from there you have 2). Other two numbers are just ASCI codes for the A and 1 letters.

But such cast from non-record type to record type works only if the size of the record fields storage in bytes is equal to the source type size. In our case String[2] has size of exactly 3 bytes as our record.

Once the record size does not match the source type size the compilation error will be thrown: E2089 Invalid typecast.

Absolute

The same behavior can be achieved using the old good absolute keyword:

var
  S: String[2];
  X: array [0..2] of Byte absolute S;
begin
  S := 'A1';
  Writeln(X[0], ' ', X[1], ' ', X[2]);
end.

The resulting output of this program will be the same as above. But the way how it works is absolutely different. First of all you are not assigning the value (so there is no copy) to variable X as it is just mapped to the same memory offset as the variable S. So once you read or write to X variable – you are writing to the S variable memory. And one other important difference is that the size checking is not performed – you need to take that risk. So for example if you increase the size of the X array and write values beyond the size of the S you will overrun the buffer by corrupting the return address:

var
  S: String[2];
  X: array [0..7] of Byte absolute S;
  i: Integer;
begin
  S := 'A1';
  Writeln(X[0], ' ', X[1], ' ', X[2]);
  for i := 3 to 7 do
    X[i] := 0;
end.

So you should use the absolute directive with caution and also take into account the target platform of your application as the type sizes differs there. You will get the compilation-time check if you stick with the record approach!

But for the risk you take you also get the bonus – a speed. The absolute does not perform any operations (copy, move etc..) so it is much faster than a casting to record type.

Record to record

When it comes to casting record to record the process becomes a bit more complicated. You cannot always cast one record of a certain size to the another same sized record type!

So the following two records cannot be casted:

type
  T1 = record
    Extension: String[3];
    Index: Integer;
  end;

  T2 = record
    A, B: Byte;
    Data: Integer;
    W: Word;
  end;

var
  R1: T1;
  R2: T2;
begin
 R1.Extension := 'pas';
 R1.Index := 1;
 R2 := T2(R1); -> [dcc32 Error] E2089 Invalid typecast
end.

As you see they are of the same size, but Delphi does not allow you to cast them (there you can use absolute keyword without a problem at all). You can ask why? The answer is quite simple: when Delphi perform record type casting it checks if the fields are not overlapping when mapped. So when you cast T1 to T2 what happens – Extension fills the memory of the A, B fields of T2 type and fills part of the memory of the Data field! So to make the cast possible the fields should never overlap between the record type fields.

So following types will cast just fine:

type
  T1 = record
    Extension: String[3];
    Index: Integer;
  end;

  T2 = record
    A, B: Byte;
    W: Word;
    Data: String[3];
  end;

var
  R1: T1;
  R2: T2;
begin
  R1.Extension := 'pas';
  R1.Index := 1;
  R2 := T2(R1);
end.

You think that’s all weird stuff? No 🙂

The following code will work:

type
  T1 = record
    Extension: String[3];
    Index: Integer;
  end;

  T2 = record
    A, B: Byte;
    Data: String[3];
    W: Word;
  end;

var
  R1: T1;
  R2: T2;
begin
  R1.Extension := 'pas';
  R1.Index := 1;
  R2 := T2(R1);
end.

But why? The Data overlaps two fields in the T1 – the Extension and Index fields partly!

Now we conclude that the following example will not work:

type
  T1 = record
    Extension: Integer;
    Index: Integer;
  end;

  T2 = record
    A, B: Byte;
    Data: Integer;
    W: Word;
  end;

var
 R1: T1;
 R2: T2;
begin
 R2 := T2(R1); -> [dcc32 Error] E2089 Invalid typecast
end.

As Data overlaps the “halfs” of Extension and Index fields memory.

Hope you found something interesting in this post.

May the Pascal be with you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s