【问题标题】:C++ union in C# — weird behaviourC# 中的 C++ 联合——奇怪的行为
【发布时间】:2016-06-04 00:03:57
【问题描述】:

我正在尝试使用 C# 中的 VHD API 创建一些 vhd/vhdx 文件。

有一个 C++ 联合,如下所示:

typedef struct _CREATE_VIRTUAL_DISK_PARAMETERS
{
    CREATE_VIRTUAL_DISK_VERSION Version;

    union
    {
        struct
        {
            GUID                  UniqueId;
            ULONGLONG             MaximumSize;
            ULONG                 BlockSizeInBytes;
            ULONG                 SectorSizeInBytes;
            PCWSTR                ParentPath;
            PCWSTR                SourcePath;
        } Version1;

        struct
        {
            GUID                   UniqueId;
            ULONGLONG              MaximumSize;
            ULONG                  BlockSizeInBytes;
            ULONG                  SectorSizeInBytes;
            ULONG                  PhysicalSectorSizeInBytes;
            PCWSTR                 ParentPath;
            PCWSTR                 SourcePath;
            OPEN_VIRTUAL_DISK_FLAG OpenFlags;
            VIRTUAL_STORAGE_TYPE   ParentVirtualStorageType;
            VIRTUAL_STORAGE_TYPE   SourceVirtualStorageType;
            GUID                   ResiliencyGuid;
        } Version2;

        struct
        {
            GUID                   UniqueId;
            ULONGLONG              MaximumSize;
            ULONG                  BlockSizeInBytes;
            ULONG                  SectorSizeInBytes;
            ULONG                  PhysicalSectorSizeInBytes;
            PCWSTR                 ParentPath;
            PCWSTR                 SourcePath;
            OPEN_VIRTUAL_DISK_FLAG OpenFlags;
            VIRTUAL_STORAGE_TYPE   ParentVirtualStorageType;
            VIRTUAL_STORAGE_TYPE   SourceVirtualStorageType;
            GUID                   ResiliencyGuid;
            PCWSTR                 SourceLimitPath;
            VIRTUAL_STORAGE_TYPE   BackingStorageType;
        } Version3;
    };
} CREATE_VIRTUAL_DISK_PARAMETERS, *PCREATE_VIRTUAL_DISK_PARAMETERS;

我正在尝试将其转换为 C#,但运气不佳。我对第 3 版根本不感兴趣,所以我把它排除在外。

我已经尝试了很多事情,我能做到的最好的事情就是让 Version2 工作(通过做一些非常奇怪的事情),但我从来没有设法让 Version1 和 Version2 同时工作。

到目前为止,效果最好的解决方案就是这个,但肯定有问题,因为 Version1 根本不起作用,而且 Version1 中的 SectorSizeInBytesulong 而不是 uint (如果我将其更改为 uint 应该是这样,我会破坏 Version2 并且 Version1 仍然无法正常工作!)

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParameters
{
    [FieldOffset(0)] public CreateVirtualDiskParametersVersion1 Version1;

    [FieldOffset(0)] public CreateVirtualDiskParametersVersion2 Version2;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParametersVersion1
{
    public CreateVirtualDiskVersion Version;
    public Guid UniqueId;
    public ulong MaximumSize;
    public uint BlockSizeInBytes;
    public ulong SectorSizeInBytes;
    public string ParentPath;
    public string SourcePath;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParametersVersion2
{
    public CreateVirtualDiskVersion Version;
    public Guid UniqueId;
    public ulong MaximumSize;
    public uint BlockSizeInBytes;
    public uint SectorSizeInBytes;
    public uint PhysicalSectorSizeInBytes;
    public string ParentPath;
    public string SourcePath;
    public OpenVirtualDiskFlags OpenFlags;
    public VirtualStorageType ParentVirtualStorageType;
    public VirtualStorageType SourceVirtualStorageType;
    public Guid ResiliencyGuid;
}

我知道理论上 Version 字段应该设置在 Version 结构之外,我也尝试过,但它只会让事情变得更有趣......

那么,有人可以建议如何将上面的内容正确地翻译成 C#,而省去不需要的 Version3 结构吗?

【问题讨论】:

  • ULONGLONG (64 bit)ULONG (32 bit) 似乎都映射到 ulong (64 bit)
  • 您是否尝试过打印出 C++ 结构成员的偏移量?
  • @TheodorosChatzigiannakis 不太清楚我会怎么做,因为它包含枚举/结构和字符串。我的意思是,它们会有多大?我愿意尝试,请告诉我要使用的值,我会试一试。
  • @cogumel0 我的意思是使用offsetof 宏。让它为您打印偏移量并在 C# 端明确定义这些偏移量。
  • 一个问题是ParentPathSourcePath 是本机结构中的原始指针,但C# 代码中的string 类中包含不止一个指针。跨度>

标签: c# c++ pinvoke


【解决方案1】:

使用Pack = 1StructLayout 属性消除了struct 成员之间的任何填充。 在 TCP 连接中,结构体通常在没有填充的情况下传递,以便所有使用该结构体的程序都可以就其在内存中的布局达成一致。

然而,正如@David Heffernan 指出的那样,将结构传递给 Windows DLL 时可能并非如此。我没有测试对CreateVirtualDisk 的实际调用,因为这似乎有点冒险,因为我以前没有使用过这个调用,并且如果我犯了错误也不想破坏我的磁盘。根据以下引用,看起来 8 字节的默认打包(默认为 Pack = 0Pack = 8)可能是正确的设置。

64-bit Windows API struct alignment caused Access Denied error on named pipe

Windows SDK 期望打包为 8 个字节。来自Using the Windows Headers

项目应该编译为使用默认的结构打包,目前是 8 字节,因为最大的整数类型是 8 字节。这样做可确保头文件中的所有结构类型都以 Windows API 期望的相同对齐方式编译到应用程序中。它还确保具有 8 字节值的结构正确对齐,并且不会在强制数据对齐的处理器上导致对齐错误。

Version 移动到CreateVirtualDiskParameters 的顶部。 两个工会紧随其后。两者具有相同的偏移量sizeof(CREATE_VIRTUAL_DISK_VERSION)

还有SectorSizeInBytesuint 而不是ulong

你可以让marshaller做使用属性填充string成员的工作,例如

[MarshalAs(UnmanagedType.LPWStr)] public string ParentPath;

或者,您可以将它表示为它在内存中出现的样子,它是一个指向 Unicode 字符串的指针:

public IntPtr ParentPath;

然后自己提取字符串

Marshal.PtrToStringAuto(vdp.Version1.ParentPath)

如果您将 C# 结构传递给外部 DLL,请使用非托管字符串填充它

vdp.Version1.ParentPath = (IntPtr)Marshal.StringToHGlobalAuto("I am a managed string");

完成后释放非托管字符串

Marshal.FreeHGlobal(vdp.Version1.ParentPath);

试试这个。

public enum CREATE_VIRTUAL_DISK_VERSION
{
    CREATE_VIRTUAL_DISK_VERSION_UNSPECIFIED = 0,
    CREATE_VIRTUAL_DISK_VERSION_1 = 1,
    CREATE_VIRTUAL_DISK_VERSION_2 = 2
};
public enum OPEN_VIRTUAL_DISK_FLAG
{
    OPEN_VIRTUAL_DISK_FLAG_NONE = 0x00000000,
    OPEN_VIRTUAL_DISK_FLAG_NO_PARENTS = 0x00000001,
    OPEN_VIRTUAL_DISK_FLAG_BLANK_FILE = 0x00000002,
    OPEN_VIRTUAL_DISK_FLAG_BOOT_DRIVE = 0x00000004,
    OPEN_VIRTUAL_DISK_FLAG_CACHED_IO = 0x00000008,
    OPEN_VIRTUAL_DISK_FLAG_CUSTOM_DIFF_CHAIN = 0x00000010
};

[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
public struct VIRTUAL_STORAGE_TYPE
{
    uint DeviceId;
    Guid VendorId;
};

[StructLayout(LayoutKind.Explicit, Pack = 8, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParameters
{
    [FieldOffset(0)]
    public CREATE_VIRTUAL_DISK_VERSION Version;

    [FieldOffset(8))]
    public CreateVirtualDiskParametersVersion1 Version1;

    [FieldOffset(8))]
    public CreateVirtualDiskParametersVersion2 Version2;
}

[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParametersVersion1
{
    public Guid UniqueId;
    public ulong MaximumSize;
    public uint BlockSizeInBytes;
    public uint SectorSizeInBytes;
    //public IntPtr ParentPath;   // PCWSTR in C++ which is a pointer to a Unicode string
    //public IntPtr SourcePath;   //string
    [MarshalAs(UnmanagedType.LPWStr)] public string ParentPath;
    [MarshalAs(UnmanagedType.LPWStr)] public string SourcePath;
}

[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
public struct CreateVirtualDiskParametersVersion2
{
    public Guid UniqueId;
    public ulong MaximumSize;
    public uint BlockSizeInBytes;
    public uint SectorSizeInBytes;
    public uint PhysicalSectorSizeInBytes;
    //public IntPtr ParentPath;   //string
    //public IntPtr SourcePath;   //string
    [MarshalAs(UnmanagedType.LPWStr)] public string ParentPath;
    [MarshalAs(UnmanagedType.LPWStr)] public string SourcePath;
    public OPEN_VIRTUAL_DISK_FLAG OpenFlags;
    public VIRTUAL_STORAGE_TYPE ParentVirtualStorageType;
    public VIRTUAL_STORAGE_TYPE SourceVirtualStorageType;
    public Guid ResiliencyGuid;
}

【讨论】:

  • 谢谢 John D,这很好,但我能问一下为什么是 [FieldOffset(sizeof(CREATE_VIRTUAL_DISK_VERSION))] 吗? CREATE_VIRTUAL_DISK_VERSION 是一个枚举(int),所以无论位数如何,它的大小都应该是 4 字节?
  • 结构通常是对齐的而不是打包的。你只是在编造这个吗?
  • @cogumeIO 目前确实如此——它试图处理CREATE_VIRTUAL_DISK_VERSION 可能发生变化的情况(例如,主要和次要版本号)。在这种情况下,您可以将其替换为struct。 C++ 结构定义使用CREATE_VIRTUAL_DISK_VERSION 而不是ULONG,所以这是可能的。
  • @David Heffernan 我用 Visual Studio 2013 C++ 和 Framework 4 对其进行了测试,结构打包是一个问题。我使用过 TCP 连接,其中请求和响应在结构中定义,并且在我所在的区域中,流中的结构被假定为已打包。有时会添加填充字节以强制对齐字边界。这似乎是让发送者和接收者就如何解释结构达成一致的最简单方法,因为它们可能由具有不同架构的机器上的不同编译器生成。
  • 如果你想猜测,如果提问者想相信你,那很好。就个人而言,我认为最好根据事实来回答。
猜你喜欢
  • 2021-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多