Marshal.SizeOf 和 sizeof 之间的区别,我只是不明白

Difference between Marshal.SizeOf and sizeof, I just don't get it

直到现在我还想当然地认为 Marshal.SizeOf 是计算非托管堆上可 blittable 结构的内存大小的正确方法(这似乎是 SO 和几乎所有其他地方的共识在网络上)。

但是在阅读了一些针对 Marshal.SizeOf 的注意事项之后(this article 在 "But there's a problem..." 之后)我试了一下,现在我完全糊涂了:

public struct TestStruct
{
    public char x;
    public char y;
}

class Program
{
    public static unsafe void Main(string[] args)
    {
        TestStruct s;
        s.x = (char)0xABCD;
        s.y = (char)0x1234;

        // this results in size 4 (two Unicode characters)
        Console.WriteLine(sizeof(TestStruct));

        TestStruct* ps = &s;

        // shows how the struct is seen from the managed side... okay!      
        Console.WriteLine((int)s.x);
        Console.WriteLine((int)s.y);

        // shows the same as before (meaning that -> is based on 
        // the same memory layout as in the managed case?)... okay!
        Console.WriteLine((int)ps->x);
        Console.WriteLine((int)ps->y);

        // let's try the same on the unmanaged heap
        int marshalSize = Marshal.SizeOf(typeof(TestStruct));
        // this results in size 2 (two single byte characters)
        Console.WriteLine(marshalSize);

        TestStruct* ps2 = (TestStruct*)Marshal.AllocHGlobal(marshalSize);

        // hmmm, put to 16 bit numbers into only 2 allocated 
        // bytes, this must surely fail...
        ps2->x = (char)0xABCD;
        ps2->y = (char)0x1234;

        // huh??? same result as before, storing two 16bit values in 
        // only two bytes??? next will be a perpetuum mobile...
        // at least I'd expect an access violation
        Console.WriteLine((int)ps2->x);
        Console.WriteLine((int)ps2->y);

        Console.Write("Press any key to continue . . . ");
        Console.ReadKey(true);
    }
}

这里出了什么问题?字段取消引用运算符 '->' 假定的内存布局是什么? “->”是否是解决非托管结构的正确运算符?或者 Marshal.SizeOf 是非托管结构的错误大小运算符?

我没有找到任何可以用我理解的语言解释这一点的内容。除了“...struct layout is undiscoverable...”和“...in most cases...”之类的废话。

不同之处在于:sizeof 运算符采用类型名称并告诉您需要为该 struct.This 的实例分配多少字节的托管内存不一定是栈内存;当结构是数组元素、class 的字段等时,它们会在堆外分配。相比之下,Marshal.SizeOf 采用类型对象或类型实例,并告诉您需要分配多少字节的非托管内存。由于各种原因,这些可能会有所不同。类型的名称为您提供了线索:Marshal.SizeOf 旨在将结构编组到非托管内存时使用。

两者的另一个区别是sizeof运算符只能取非托管类型的名称;也就是说,一个结构类型,其字段只有整数类型、布尔值、指针等。 (有关确切定义,请参阅规范。)相比之下,Marshal.SizeOf 可以采用任何 class 或结构类型。

What memory layout does the field dereferencing operator '->' assume?

无论 CLI 决定什么

Is '->' even the right operator for addressing unmanaged structs?

这是一个模棱两可的概念。通过 CLI 访问非托管内存中的结构:这些结构遵循 CLI 规则。还有一些结构仅仅是非托管代码(可能是 C/C++)访问同一内存的名义名字。这遵循该框架的规则。编组通常是指P/Invoke,但不一定适用于此。

Or is Marshal.SizeOf the wrong size operator for unmanaged structs?

我默认为 Unsafe.SizeOf<T>,本质上是 sizeof(T) - 对于 CLI/IL(包括填充规则等),它是完美的 well-defined,但不是在 C# 中不可能。

我认为您仍然没有回答的一个问题是在您的特定情况下发生了什么:

&ps2->x
0x02ca4370  <------
    *&ps2->x: 0xabcd 'ꯍ'
&ps2->y
0x02ca4372  <-------
    *&ps2->y: 0x1234 'ሴ'

您正在写入和读取(可能)未分配的内存。由于您所在的内存区域,未检测到它。

这将重现预期的行为(至少在我的系统上,YMMV):

  TestStruct* ps2 = (TestStruct*)Marshal.AllocHGlobal(marshalSize*10000);

  // hmmm, put to 16 bit numbers into only 2 allocated 
  // bytes, this must surely fail...
  for (int i = 0; i < 10000; i++)
  {
    ps2->x = (char)0xABCD;
    ps2->y = (char)0x1234;
    ps2++;
  }

A char 默认封送到 ANSI 字节。这允许与大多数 C 库进行互操作,并且是 .NET 运行时操作的基础。

我认为正确的解决方案是将 TestStruct 更改为:

public struct TestStruct
{
    [System.Runtime.InteropServices.MarshalAs(UnmanagedType.U2)]
    public char x;
    [System.Runtime.InteropServices.MarshalAs(UnmanagedType.U2)]
    public char y;
}

UnmanagedType.U2 表示 unsigned 'integer' 2 个字节长,相当于 C 头文件中的 wchar_t 类型。

只要注意细节,就可以将 C 结构无缝移植到 .NET,并为与本机库的互操作打开许多大门。