【问题标题】:Mimick C++ nested structs with union in C#在 C# 中使用 union 模拟 C++ 嵌套结构
【发布时间】:2020-10-29 09:47:18
【问题描述】:

我知道这个问题之前已经被问过很多次了,我尝试通读了之前的所有问题,但运气不佳。

我正在尝试将以下 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 字节

【问题讨论】:

  • C++ struct 中的所有数据字段共享同一个存储空间,你如何在 C# 中实现呢?
  • 我不知道,这就是我来这里的原因:)
  • 它们具有相同的偏移量。枚举类也不是跨语言兼容的......网络协议明确定义了字段大小。

标签: c# c++ struct


【解决方案1】:

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

由于Packet 中的union 与所有字段重叠,这也使得Packet 成为1040 字节大小的结构:4 字节为PacketHeader1032 字节为get_pid - 其中是迄今为止“最长”的结构 - 和 4 字节用于填充。遗憾的是,填充是特定于平台的。

要摆脱 C/C++ 编译器的填充,您需要使用诸如 GCC 的 __attribute__ ((packed)) 或 Visual C++ 的 #pragma pack(1) 之类的属性(参见例如 this SO 答案)。

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

为了可移植性,还要注意unsigned long long 也是特定于平台的,唯一的保证是至少 64 位长。如果您需要完全 64 位,您可能需要改用uint64_t(参见例如here)。


这是我用来确定大小的代码(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

正如 cmets 中所指出的,将 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);

【讨论】:

  • 感谢您的详细回复。如果我为Packet 中的所有结构更改FieldOffset,我会在运行应用程序时得到这个 - Packet is invalid .... because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field.'
  • 罪魁祸首是 get_pid 结构的 name 字段,我可以说这么多。
猜你喜欢
  • 2015-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-18
相关资源
最近更新 更多