Jump to content
ertank

C# struct to Delphi conversion help

Recommended Posts

Hello,

 

I need to convert a C# project into Delphi. This project is using a C++ DLL and passing a struct named Members to the DLL.

 

I could not make same size record in Delphi. There is a 4 byte difference and Delphi is smaller.

 

The Members struct has more than 80 string variables, one enum type and three other structs. These three other struct sizes are exactly match to Delphi conversions. They all have StructLayout as Sequential defined. But, Members struct has no StructLayout at all. Actually, it was defined as Sequential before but now that code is remarked. There are only MarshalAs for strings like below

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string StartRNo;

I searched on the web and I think I should convert above to Delphi as following

StartRNo: Array[0..31] of AnsiChar;

This Delphi definition give me exact size for three other structs. They have only string variables.

 

DLL function calls are CharSet.Ansi for all, but the only one using the Members struct is defined as CharSet.Unicode. If I am to use Char instead of AnsiChar on Delphi side for strings (just for the Members record), difference is a lot more like double.

 

I am unable to figure why there is a 4 byte difference and how to make both C# Members struct and Delphi record identical.

 

Any help is appreciated.

 

Thanks & Regards,

Ertan

Share this post


Link to post

Data alignment in records (struct) are different depending on compiler and alignment directive.

Look at Delphi documentation https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Align_fields_(Delphi)

To avoid that problem, I always use a packed record and insert dummy data between fields to align used fields the same way as in C++. I use offsetof macro in C/C++ to get the offset of each struct field and a similar Delphi function I wrote to get the offset of a record member.

 

function OffsetOf(const Base : Pointer; const Field) : Integer;
begin
    Result := Integer(UIntPtr(@Field) - UIntPtr(Base));
end;

And you can call it like this:

var O0 : Integer := OffsetOf(@bitmapProperties, bitmapProperties.pixelFormat);

 

  • Like 2
  • Thanks 1

Share this post


Link to post

I used x86 target on C# project and Delphi side is also 32bit application. Using your offset function revealed some differences. C# struct "Members" has following remarked offsets (Layout is indeed commented in the project code. I removed remark for testing and this didn't change total struct size)

//[StructLayout(LayoutKind.Sequential)]
public struct Members
{
    public Tags ResponsedTag; // Offset 0

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string StartRNo; // Offset 4
    // continues and ends as following
    public GrupDF02 groupDF02;
    public GrupDF41 groupDF41;
    public GrupDF6F groupDF6F;
}

However, Delphi record has following offsets. I intentionally didn't use packed record as I am completely unsure of the C# side.

TMembers = record 
  ResponsedTag: TTags; // Offset 0

  StartRNo: Array[0..31] of AnsiChar; // Offset 1
  // continues and ends as following
  groupDF02: TGrupDF02;
  groupDF41: TGrupDF41;
  groupDF6F: TGrupDF6F;  
end;

Tags is an enum and takes up 4 bytes in C# but 1 byte in Delphi. That explains 3 bytes difference. I'm not sure about the reason of this difference.

 

These last three item's sizes on C# and Delphi are the same. When I compare the very last item "groupDF6F" offsets, the difference is still 3 bytes. So, I could not find a reason for total difference of 4 bytes.

 

I don't know how I can modify Delphi to match C# just for this enum difference.

I could not find where this remaining 1 byte difference is coming from.

Share this post


Link to post
22 minutes ago, ertank said:

I don't know how I can modify Delphi to match C# just for this enum difference.

As I said, use a packed record and dummy data like this:

TMembers = packed record 
  ResponsedTag: TTags; 
  Dummy1: array [0..2] of Byte;
  StartRNo: Array[0..31] of AnsiChar; 
  // continues and ends as following
  groupDF02: TGrupDF02;
  groupDF41: TGrupDF41;
  groupDF6F: TGrupDF6F;  
end;

Also, you must taken into account the sizeof the whole record and mayba add more dummy data at the end to reflect the sizeof in C#/C/C++.

Share this post


Link to post
3 hours ago, FPiette said:

To avoid that problem, I always use a packed record and insert dummy data between fields to align used fields the same way as in C++. I use offsetof macro in C/C++ to get the offset of each struct field and a similar Delphi function I wrote to get the offset of a record member.

This is bad advice. If you are using aligned records in Delphi, then the compiler uses the same alignment rules as for aligned C++ structs, and for aligned C# structs.

Share this post


Link to post
57 minutes ago, ertank said:

Tags is an enum and takes up 4 bytes in C# but 1 byte in Delphi.

In your Delphi code you should be using {$MINENUMSIZE 4} at the point where you define the enums.

 

An example from my own codebase:

 

//Dragon4
{$MINENUMSIZE 4}
type
  TDragon4DigitMode = (DigitMode_Unique, DigitMode_Exact);
  TDragon4CutoffMode = (CutoffMode_TotalLength, CutoffMode_FractionLength);
{$MINENUMSIZE 1}

 

I would however say that using offset methods to compare offsets is very good advice. But I definitely do not advise manual alignment. The compiler can do it perfectly well. You may as well get it to do that. Then, come the day you move to 64 bit, you won't have to change a thing.

  • Thanks 1

Share this post


Link to post
3 hours ago, David Heffernan said:

This is bad advice. If you are using aligned records in Delphi, then the compiler uses the same alignment rules as for aligned C++ structs, and for aligned C# structs. 

My experience shows it is NOT always the case.

Manual alignment ALWAYS work.

Edited by FPiette
  • Like 3

Share this post


Link to post
10 minutes ago, FPiette said:

My experience shows it is NOT always the case.

Show us an example, with modern Delphi. For sure some ancient versions didn't get alignment correct. That's no longer the case.

 

Compiler alignment always works for me, and I do a lot of it. And converting to 64 bit was trivial when I had to do that. 

Share this post


Link to post
3 hours ago, David Heffernan said:

For sure some ancient versions didn't get alignment correct.

You are now understanding...

My method works with ALL Delphi compilers.

Share this post


Link to post
48 minutes ago, FPiette said:

My method works with ALL Delphi compilers.

It would have been useful to say something like this, like you may need manual alignment if you need to use ancient compilers like Delphi 6. But even then, in most cases, aligned records work as expected. It's only in some very rare cases that this is not the case. Perhaps you have an example for us. Or has that knowledge been lost in the passage of time.

 

For people that don't need to support 20 year old compilers, feel free to use the compiler to do the alignment.

Share this post


Link to post

Even if I make it identical C# enum with Delphi enum, There is still one byte difference in total struct vs record comparison. I check even the last item offset in C# struct matches to Delphi and this last item size also matches on both C# and Delphi. It must be that C# has one byte more at the very bottom of its struct.

 

@FPiette advise to add it at the end of Delphi record as a dummy variable and I can do that.

 

I wonder if I absolutely need it?

If I do need it, I wonder if there is another way of handling that in newer Delphi versions like a compiler directive {$MINENUMSIZE 4}?

 

Thanks.

Share this post


Link to post
1 hour ago, ertank said:

Even if I make it identical C# enum with Delphi enum, There is still one byte difference in total struct vs record comparison. I check even the last item offset in C# struct matches to Delphi and this last item size also matches on both C# and Delphi. It must be that C# has one byte more at the very bottom of its struct.

 

@FPiette advise to add it at the end of Delphi record as a dummy variable and I can do that.

 

I wonder if I absolutely need it?

If I do need it, I wonder if there is another way of handling that in newer Delphi versions like a compiler directive {$MINENUMSIZE 4}?

 

Thanks.

Hard to comment on this without seeing you actual code. This is all about the detail and we have none of it. For all I know you are using packed records and manually inserting padding. In which case, if that's what you've decided to do then you know how to fix it. 

Edited by David Heffernan

Share this post


Link to post
2 hours ago, ertank said:

@FPiette advise to add it at the end of Delphi record as a dummy variable and I can do that. I wonder if I absolutely need it?

This is required to do that because there may be cases when sizeof() is used to create an array of structures, array which could be written to disk or send to another program or context where the sizeof the structure is different. The dummy data at the end of the structure will never be used except for padding the structure to have the exact required size.

 

Edited by FPiette

Share this post


Link to post

Of course the other problem with packing a record that should be aligned is that gives it alignment of 1 so they can be misaligned. These days that doesn't usually matter for x86/x64 but it can be a issue for some SSE2 instructions and for arm processors. 

Edited by David Heffernan

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×