用C#中的并集模拟C++嵌套结构

Mimick C++ nested structs with union in C#

本文关键字:模拟 C++ 嵌套 结构      更新时间:2023-10-16

我知道这个问题以前被问过很多次,我试着通读了以前的所有问题,但运气不好。

我正在尝试将下面的C++结构转换为C#,用于套接字通信。

enum class packet_type
{
read_mem,
get_base_addr,
get_pid,
completed
};
struct copy_mem
{
unsigned int dest_process_id;
unsigned long long dest_address;
unsigned int src_process_id;
unsigned long long src_address;
unsigned int size;
};
struct get_base_addr
{
unsigned int process_id;
};
struct get_pid
{
size_t len;
wchar_t name[256];
};
struct completed
{
unsigned long long result;
};
struct PacketHeader
{
//uint32_t   magic;
packet_type type;
};
struct Packet
{
PacketHeader header;
union
{
copy_mem     copy_memory;
get_base_addr get_base_address;
get_pid get_pid;
completed        completed;
} data;
};

这是我目前的C#实现

public enum PacketType
{
read_mem = 0,
get_base_addr = 1,
get_pid = 2,
completed = 3
}
[StructLayout(LayoutKind.Sequential)]
public struct PacketHeader
{
public PacketType type;
}
[StructLayout(LayoutKind.Sequential)]
public struct get_base_addr
{
uint process_id;
};
[StructLayout(LayoutKind.Sequential)]
public struct get_pid
{
public ulong len;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string name;
}
[StructLayout(LayoutKind.Sequential)]
public struct copy_mem
{
public uint dest_process_id;
public ulong dest_address;
public uint src_process_id;
public ulong src_address;

public uint size;
}
[StructLayout(LayoutKind.Sequential)]
public struct completed
{
public ulong result;
};
[StructLayout(LayoutKind.Explicit, Pack = 0, CharSet = CharSet.Unicode)]
public struct Packet
{
[FieldOffset(0)] //
public PacketHeader header;
[FieldOffset(4)]
public copy_mem CopyMem; //28
[FieldOffset(32)]
public get_base_addr GetBaseAddress;
[FieldOffset(36)]
public get_pid GetPid;
[FieldOffset(300)]
public completed Completed;
}

然后,我使用这种方法将结构转换为用于套接字传输的字节数组:

public static byte[] RawSerialize(T item)
{
int rawSize = Marshal.SizeOf(typeof(T));
IntPtr buffer = Marshal.AllocHGlobal(rawSize);
var a = Marshal.SizeOf(item);
var b = Marshal.SizeOf(buffer);
Marshal.StructureToPtr(item, buffer, false);
byte[] rawData = new byte[rawSize];
Marshal.Copy(buffer, rawData, 0, rawSize);
Marshal.FreeHGlobal(buffer);
return rawData;
}

问题是var a = Marshal.SizeOf(item);报告的大小为312,但当我在C++中执行sizeof(Packet)时,实际结构应该是528字节

您的假设似乎是错误的。首先,wchar_t类型在不同的机器上可能具有不同的长度。在我的x64 Linux盒子中,它是4字节的——仅此一点就使get_pid成为1032字节大小的结构。您可能有兴趣使用char16_tchar32_t类型(例如,请参见此处(。

由于Packet中的union与所有字段重叠,这也使Packet成为1040字节大小的结构:PacketHeader4字节,get_pid1032字节,这是";"最长";struct和4字节用于填充。遗憾的是,填充是特定于平台的。

要从C/C++编译器中去除填充,您需要使用GCC的__attribute__ ((packed))或Visual C++的#pragma pack(1)等属性(例如,请参见此SO答案(。

不过,请注意,C#中的字段偏移也是错误的:除了标头,Packet中的所有字段偏移都必须是[FieldOffset(4)],因为在C++中,它是从字节4开始的union(假设零填充(。

为了便于移植,还要注意unsigned long long也是特定于平台的,唯一的保证是至少为64位长。如果您需要恰好64位,则可能需要使用uint64_t(例如,请参见此处(。


这是我用来确定大小的代码(Linux x64,GCC 9.3(:

int main() {
std::cout << "packet_type:   " << sizeof(packet_type) << std::endl;
std::cout << "copy_mem:      " << sizeof(copy_mem) << std::endl;
std::cout << "get_base_addr: " << sizeof(get_base_addr) << std::endl;
std::cout << "get_pid:       " << sizeof(get_pid) << std::endl;
std::cout << "completed:     " << sizeof(completed) << std::endl;
std::cout << "PacketHeader:  " << sizeof(PacketHeader) << std::endl;
std::cout << "Packet:        " << sizeof(Packet) << std::endl;
std::cout << "wchar_t:       " << sizeof(wchar_t) << std::endl;
return 0;
}

带填充(默认结构(:

packet_type:   4
copy_mem:      40
get_base_addr: 4
get_pid:       1032
completed:     8
PacketHeader:  4
Packet:        1040
wchar_t:       4

无填充(__attribute__ ((packed))(:

packet_type:   4
copy_mem:      28
get_base_addr: 4
get_pid:       1032
completed:     8
PacketHeader:  4
Packet:        1036
wchar_t:       4

正如注释中所指出的,将Packet结构的GetPid字段设置为[FieldAlign(4)]将导致以下运行时错误:

Unhandled exception. System.TypeLoadException: Could not load type 'Packet' from assembly 'StructSize, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field.

解决此问题的一种方法是定义get_pid结构,如下所示:

[StructLayout(LayoutKind.Sequential, Pack = 0)]
public unsafe struct get_pid
{
public ulong len;
public fixed byte name[256];
}

这仍然假设名称字符串为128个字符长,每个字符都是2字节的Unicode。这样一来,name属性现在的类型为byte*。要返回字符串,以下两种方法应该有效:

public static unsafe string GetName(get_pid gp) => 
new string((sbyte*) gp.name, 0, 256, Encoding.Unicode);
public static unsafe string GetName(get_pid gp) =>
Marshal.PtrToStringUni(new IntPtr(gp.name), 256);