I'm looking for ways to obtain the offset of a field in a Delphi record. These 2 following methods work but i was hoping for a cleaner way. Basically i would have liked the third showmessage to work. Any ideas?
type
rec_a=record
a:longint;
b:byte;
c:pointer;
end;
{$warnings off}
function get_ofs1:longint;
var
abc:^rec_a;
begin
result:=longint(@abc.c)-longint(abc);
end;
{$warnings on}
function get_ofs2:longint;
asm
mov eax,offset rec_a.c
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
showmessage(inttostr(get_ofs1));
showmessage(inttostr(get_ofs2));
// showmessage(inttostr(longint(addr(rec_a.c)))); // is there a way to make this one work?
end;
edit: Alright, the below answer works fine, thanks! For reference, here's the assembler output for the various options:
---- result:=longint(@abc.c)-longint(abc); ----
lea edx,[eax+$08]
sub edx,eax
mov eax,edx
---- mov eax,offset rec_a.c ----
mov eax,$00000008
---- result:=longint(@rec_a(nil^).c); ----
xor eax,eax
add eax,$08
edit2: looks like this is a duplicate of a previous question: previous similar question as noted below by RRUZ. As shown there, another method is to declare a global variable and use it as follows. Strangely enough the compiler still isnt able to assign the proper value at compile time as is seen in the assembler output, so for both efficiency and readability it`s better to use the nil method.
---- var ----
---- rec_a_ofs:rec_a; ----
---- ... ----
---- result:=longint(@rec_a_ofs.c)-longint(@rec_a_ofs); ----
mov eax,$0045f5d8
sub eax,$0045f5d0
edit3: Ok revised code with all know ways to accomplish this. Note that the assembler code generated for the 3rd, 4th, and 5th (class method) ways is identical, whether they are inlined or not. Choose your favorite way when you get to do this stuff!
type
prec_a=^rec_a;
rec_a=record
a:longint;
b:byte;
c:pointer;
class function offset_c:longint;static;inline;
end;
//const
// rec_a_field_c_offset=longint(@rec_a(nil^).c); // no known way to make this work
{$warnings off}
function get_ofs1:longint;inline;
var
abc:^rec_a;
begin
result:=longint(@abc.c)-longint(abc);
end;
{$warnings on}
function get_ofs2:longint;
asm
mov eax,offset rec_a.c
end;
function get_ofs3:longint;inline;
begin
result:=longint(@rec_a(nil^).c);
end;
function get_ofs4:longint;inline;
begin
result:=longint(@prec_a(nil).c);
end;
class function rec_a.offset_c:longint;
begin
result:=longint(@prec_a(nil).c);
end;
var
rec_a_ofs:rec_a;
function get_ofs6:longint;inline;
begin
result:=longint(@rec_a_ofs.c)-longint(@rec_a_ofs);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
showmessage(inttostr(get_ofs1));
showmessage(inttostr(get_ofs2));
showmessage(inttostr(get_ofs3));
showmessage(inttostr(get_ofs4));
showmessage(inttostr(rec_a.offset_c));
showmessage(inttostr(get_ofs6));
// showmessage(inttostr(rec_a_field_c_offset));
end;
I always use this approach:
Offset := Integer(@rec_a(nil^).c);
Don't let the use of nil^
put you off, it's perfectly safe. And don't worry about 64 bit pointer truncation. If you have a record whose size is >4GB then you have bigger problems!
You could also use a generic approach:
uses
System.SysUtils,TypInfo,RTTI;
function GetFieldOffset( ARecordTypeInfo : PTypeInfo;
const ARecordFieldName : String) : Integer;
var
MyContext: TRttiContext;
MyField: TRttiField;
begin
if (ARecordTypeInfo.Kind <> tkRecord) then
raise Exception.Create('Not a record type');
for MyField in MyContext.GetType(ARecordTypeInfo).GetFields do
if MyField.Name = ARecordFieldName then
begin
Exit(MyField.Offset);
end;
raise Exception.Create('No such field name:'+ARecordFieldName);
end;
And call it like this:
ShowMessage( IntToString( GetFieldOffset( TypeInfo(rec_a),'c')));
Not as fast as your other alternatives, but gives a unified generic solution.
Looking at your options here for a clean solution, it seems the best is to declare a generic function:
function GetFieldOffset( const P : Pointer) : Integer; Inline;
// Example calls :
// GetFieldOffset( @PMyStruct(nil).MyParameter);
// GetFieldOffset( @TMyStruct(nil^).MyParameter);
begin
Result := Integer( P);
end;
So even if the call looks awkward, the function name tells you what's going on. Inlining the call removes the function call overhead, so it will work as a code beautifier.
It is possible to get constant values for a record base and field address:
const
cStruct : MyStruct = ();
cMyInteger3Offs : Pointer = @cStruct.MyInteger3;
cMyStructBase : Pointer = @cStruct;
But this will not make code look cleaner.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With