直到现在,我都理所当然地认为 Marshal.SizeOf 是计算非托管堆上 blittable 结构的内存大小的正确方法(这似乎是 SO 以及网络上几乎所有其他地方的共识)。
但是在阅读了一些针对 Marshal.SizeOf 的警告(这篇文章在“但是有一个问题......”之后)我尝试了一下,现在我完全困惑了:
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 对于非托管结构来说是错误的大小运算符?
我没有找到任何可以用我理解的语言来解释这一点的内容。除了“...结构布局是不可发现的...”和“...在大多数情况下...”之类的软弱无力的东西。
sizeof
是一个运算符,仅在非安全代码上下文中支持,标记为unsafe
。
返回给定类型的变量占用的字节数。
sizeof
运算符的参数必须是托管类型的名称或限制为非托管类型的类型参数。
当我们谈论仅限于非托管的类型时,我们的意思是这样的:
unsafe void SomeMethod<T>() where T : unmanaged
{
// Type T is restricted to be unmanaged, so it is supported
// for the sizeof operator.
int size = sizeof(T);
}
sizeof
不支持引用类型,并且比Marshal.SizeOf<T>
更高效,因为它是在编译时评估的。此外,它仅适用于可以直接复制到内存中而无需转换的值类型(结构体、枚举)。
但是,下表显示了那些不需要不安全上下文的
sizeof
表达式:
Marshal.SizeOf
是类 Marshal
的静态方法,它返回对象的非托管大小(以字节为单位)。
与在编译期间计算的
sizeof
不同,Marshal.SizeOf
在运行时计算,这使得它有点慢,但它更灵活,因为它支持引用类型和值类型,因此,您可以计算更复杂的数据类型:
class MyClass {}
struct MyStruct {}
// This is valid.
MyClass obj = new MyClass();
int size = Marshal.SizeOf(obj);
// This is valid.
MyStruct obj = new MyStruct();
int size = Marshal.SizeOf(obj);
Marshal
类的命名空间是System.Runtime.InteropServices
。
请注意,您可以使用
Marshal.Sizeof
来确定要使用 AllocHGlobal
和 AllocCoTaskMem
方法分配的非托管内存量。
我认为您仍然没有回答的一个问题是您的特定情况下发生了什么:
&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++;
}
字段解引用运算符“->”采用什么内存布局?
无论 CLI 决定如何
“->”是否是用于寻址非托管结构的正确运算符?
这是一个模糊的概念。非托管内存中存在通过 CLI 访问的结构:这些结构遵循 CLI 规则。有些结构只是访问同一内存的非托管代码(可能是 C/C++)的名义名称。这遵循该框架的规则。编组通常指的是 P/Invoke,但这不一定适用于此。
或者 Marshal.SizeOf 对于非托管结构来说是错误的大小运算符吗?
Unsafe.SizeOf<T>
,本质上是 sizeof(T)
- 对于 CLI/IL 来说是完美定义的(包括填充规则等),但在 C# 中是不可能的。
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
表示无符号“整数”2 个字节长,这使其相当于 C 标头中的 wchar_t
类型。
通过关注细节,可以将 C 结构无缝移植到 .NET,并为与本机库的互操作打开许多大门。