21

我们必须与本机代码进行很多互操作,在这种情况下,使用不需要编组的不安全结构要快得多。但是,当结构包含非原始类型的固定大小缓冲区时,我们不能这样做。为什么 C# 编译器要求固定大小的缓冲区仅属于基本类型?为什么固定大小的缓冲区不能由结构组成,例如:

[StructLayout(LayoutKind.Sequential)]
struct SomeType
{
  int Number1;
  int Number2;
}
4

3 回答 3

21

C# 中的固定大小缓冲区是通过称为“不透明类”的 CLI 功能实现的。Ecma-335的第 I.12.1.6.3 节描述了它们:

一些语言提供多字节数据结构,其内容由地址算术和间接操作直接操作。为了支持此功能,CLI 允许创建具有指定大小的值类型,但不提供有关其数据成员的信息。这些“不透明类”的实例的处理方式与任何其他类的实例完全相同,但不应使用 ldfld、stfld、ldflda、ldsfld 和 stsfld 指令来访问它们的内容。

“没有关于其数据成员的信息”和“不得使用 ldfld/stfld”是问题所在。第二条规则将 kibosh 放在结构上,您需要 ldfld 和 stfld 才能访问它们的成员。C# 编译器无法提供替代方案,结构的布局是运行时实现细节。Decimal 和 Nullable<> 已被淘汰,因为它们也是结构。IntPtr 已失效,因为它的大小取决于进程的位数,这使得 C# 编译器难以为用于访问缓冲区的 ldind/stind 操作码生成地址。引用类型引用已失效,因为 GC 需要能够找到它们并且不能通过第一条规则。枚举类型的大小取决于它们的基本类型;听起来像是一个可以解决的问题,但不完全确定他们为什么跳过它。

只剩下 C# 语言规范中提到的那些:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double 或 bool。只是具有明确定义大小的简单类型。

于 2013-09-30T21:33:05.383 回答
5

什么是固定缓冲区?

来自 MSDN:

在 C# 中,您可以使用 fixed 语句在数据结构中创建具有固定大小数组的缓冲区。这在您使用现有代码时很有用,例如用其他语言编写的代码、预先存在的 DLL 或 COM 项目。固定数组可以采用常规结构成员允许的任何属性或修饰符。唯一的限制是数组类型必须是bool、byte、char、short、int、long、sbyte、ushort、uint、ulong、float 或 double

关于为什么必须使用固定缓冲区,我只想引用 Hans Passant 先生的话unsafe。您可能会看到为什么固定大小的缓冲区(数组)一定是不安全的?了解更多信息。

因为“固定缓冲区”不是真正的数组。它是一种自定义值类型,是我所知道的用 C# 语言生成一个值类型的唯一方法。CLR 无法验证数组的索引是否以安全的方式完成。该代码也无法验证。最生动的演示:

using System;

class Program {
    static unsafe void Main(string[] args) {
        var buf = new Buffer72();
        Console.WriteLine(buf.bs[8]);
        Console.ReadLine();
    }
}
public struct Buffer72 {
    public unsafe fixed byte bs[7];
}

在此示例中,您可以任意访问堆栈帧。标准的缓冲区溢出注入技术可用于恶意代码修补函数返回地址并强制您的代码跳转到任意位置。

是的,这很不安全。

为什么固定缓冲区不能包含非原始数据类型?

西蒙怀特提出了一个有效的观点:

我将使用“增加编译器的复杂性”。编译器必须检查没有 .NET 特定功能应用于应用于可枚举项的结构。例如,泛型、接口实现,甚至是非原始数组的更深层次的属性等。毫无疑问,运行时也会遇到一些与这类事情相关的互操作问题。

和伊巴萨:

“但这已经由编译器完成了。” 只是部分。编译器可以进行检查以查看是否管理类型,但这不负责生成代码以将结构读/写到固定缓冲区。它可以完成(在 CIL 级别没有什么可以阻止它)它只是没有在 C# 中实现。

最后,迈赫达德:

我认为这实际上是因为他们不希望您使用固定大小的缓冲区(因为他们希望您使用托管代码)。使与本机代码的互操作变得过于容易使您不太可能将 .NET 用于所有内容,并且他们希望尽可能地推广托管代码。

答案似乎是响亮的“它只是没有实施”。

为什么没有实施?

我的猜测是,成本和实施时间对他们来说不值得。开发人员宁愿推广托管代码而不是非托管代码。它可能会在 C# 的未来版本中完成,但当前的 CLR 缺乏很多所需的复杂性。

另一种选择可能是安全问题。如果固定缓冲区在您的代码中实施不当,则极易受到各种问题和安全风险的影响,因此我可以理解为什么不鼓励使用它们而不是 C# 中的托管代码。为什么要在您不鼓励使用的东西上投入大量工作?

于 2013-09-30T21:30:10.400 回答
0

我理解你的观点……另一方面,我想这可能是微软保留的某种前向兼容性。您的代码被编译为 MSIL,特定的 .NET Framework 和操作系统将其布局在内存中。

我可以想象它可能来自英特尔的新 CPU,它需要将变量布局到每 8 个字节才能获得最佳性能。在这种情况下,将来会需要,在某些未来的 .NET Framework 6 和某些未来的 Windows 9 中以不同的方式布局这些结构。在这种情况下,您的示例代码将迫使 Microsoft 在未来不要更改内存布局,也不要将 .NET 框架加速到现代硬件。

这只是猜测...

您是否尝试设置 FieldOffset?请参阅C# 中的 C++ 联合

于 2013-09-30T19:40:15.460 回答