2

I have a legacy application written in Delphi 2007 which generates byte arrays like this:

command_data = packed record
    direction     : word;
    name          : array [0..9] of char;
end;

command_header = packed record
    length      : word;
    data1       : word;
    data2       : cardinal;
end;

command_data_container = packed record
    data          : command_data;
    containerId   : word;
end;

function Generate(name: string)boolean;
var
  header  : command_header;
  container : command_data_container;
  charArrayName: array [0..9] of char;

begin
  charArrayName = array [0..9] of char;

  for I := 0 to Length(name) - 1 do begin
    charArrayName[i] := name[i+1];
  end;
  charArrayName[i+1] := #0;

  header.length := sizeof(header) + sizeof(container);
  header.data1 := 0;
  header.data2 := 1;

  container.data.direction := 1;
  container.data.name      := charArrayName;
  container.containerId    := 1;

  stream := TMemoryStream.Create;
  stream.WriteBuffer(header, SizeOf(header));
  stream.WriteBuffer(container, SizeOf(container));
  //...
 end;

I need to rewrite this part in C#. I got this so far:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
unsafe struct command_data
{
    public ushort direction;
    public fixed char name[10];
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
unsafe struct command_header
{
    public ushort length;
    public ushort data1;
    public ulong data2;
}    

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct command_data_container
{
    public command_data data;
    public ushort containerId;
} 

 public bool Generate(string name)
 {
    name = name + Char.MinValue; // Add null terminator.
    char[] charArrayName = agentId.ToCharArray();

    unsafe
    {
        command_header header;
        command_data_container command;

        header.data1 = 0;
        header.data2 = 1;
        header.length = (ushort)(sizeof(command_header) + sizeof(command_data_container));


        command.data.direction = 1;
        *command.data.name = charArrayName[0];

        for (int i = 1; i < charArrayName.Length; i++)
        {
            *(command.data.name + i) = charArrayName[i];
        }
        command.containerId = 1;

        var headerBytes = StructToBytes<command_header>(header);
        var commandBytes = StructToBytes<command_data_container>(command);

        byte[] combined = new byte[headerBytes.Length + commandBytes.Length];
        Buffer.BlockCopy(headerBytes, 0, combined, 0, headerBytes.Length);
        Buffer.BlockCopy(commandBytes, 0, combined, headerBytes.Length, commandBytes.Length);

        //combined should have the same data as the stream in the delphi code

    }
 }


public static byte[] StructToBytes<T>(T structure) where T : struct
{
    int size = Marshal.SizeOf(structure);
    byte[] rawData = new byte[size];
    GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned);
    try
    {
        IntPtr rawDataPtr = handle.AddrOfPinnedObject();
        Marshal.StructureToPtr(structure, rawDataPtr, false);
    }
    finally
    {
        handle.Free();
    }
    return rawData;
}

I tried several methods to convert the struct to a byte array, but none of them reproduced the same result as the Delphi code.

The StructToBytes method is borrowed from here: C# performance - Using unsafe pointers instead of IntPtr and Marshal

I also tried some others marshalling methods from SO, but nothing worked. What did I do wrong?

morcibacsi
  • 43
  • 7
  • You know that Delphi 2007 Char is AnsiChar and in C# it is Unicode? – Sir Rufo Nov 06 '17 at 23:10
  • Char in c# are two bytes while in c++ they are one byte. So in c# use : new byte[10]. The array also must be terminated with a '\0' (0x00). I usually use : Encoding.UTF8.GetBytes("JohnSmith\0"); – jdweng Nov 07 '17 at 01:01
  • 1
    If you must **rewrite** this in C#, then why do you care about interop and marshalling? Just generate the same number of bytes. Can't be too hard. – Rudy Velthuis Nov 07 '17 at 07:31
  • I agree with @Rudy here. It's simpler to write directly to byte streams. Obviously the big problem here is the size issue around char as already identified. I'd also point out that your Delphi code is ripe for buffer overrun. – David Heffernan Nov 07 '17 at 08:19
  • @jdweng `Encoding.Default` is appropriate here to match the Delphi code. I see no evidence that the input string is UTF-8 encoded. ANSI is the norm for Delphi 2007. – David Heffernan Nov 07 '17 at 09:29
  • I never us Default and let the Net library do whatever it feels like. UTF8 will not change any characters while ANSI will remove items. – jdweng Nov 07 '17 at 09:33
  • @jdweng It's irrelevant whether or not you use ANSI. The Delphi code uses ANSI. If you change one side of the interface to use UTF-8, you'd need to change the other to match. This is how binary interop works. You program to an agreed interface. You don't get to arbitrarily change one side of the interface and ignore the impact. – David Heffernan Nov 07 '17 at 10:38
  • If you only use ANSI characters then there isn't an issue. – jdweng Nov 07 '17 at 10:40
  • @jdweng Nope. If you only use ASCII characters then ANSI and UTF-8 encodings are the same. Beyond that range, then ANSI and UTF-8 diverge. Are you ever wrong? – David Heffernan Nov 07 '17 at 10:48

1 Answers1

2
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
struct command_data
{
    public ushort direction;
    [MarshalAs( UnmanagedType.ByValTStr, SizeConst = 10)]
    public string name;
}
Sir Rufo
  • 18,395
  • 2
  • 39
  • 73