为啥固定大小的缓冲区只能是原始类型?

Posted

技术标签:

【中文标题】为啥固定大小的缓冲区只能是原始类型?【英文标题】:Why can fixed size buffers only be of primitive types?为什么固定大小的缓冲区只能是原始类型? 【发布时间】:2013-09-21 06:09:01 【问题描述】:

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

[StructLayout(LayoutKind.Sequential)]
struct SomeType

  int Number1;
  int Number2;

【问题讨论】:

我将使用“增加编译器的复杂性”。编译器必须检查没有 .NET 特定功能应用于应用于可枚举项的结构。例如,泛型、接口实现,甚至非原始数组的更深层次的属性等等。毫无疑问,运行时也会有一些与这类事情的互操作问题。 但这已经由编译器完成了。如果您尝试创建一个指向包含任何这些内容的结构的指针,则会出现编译器错误:msdn.microsoft.com/en-us/library/x2estayf(v=vs.90).aspx 看来我完全误解了你的意思。 你可以看看tutorials.csharp-online.net/… @Mehrdad 这有点阴谋论,你不觉得吗? 【参考方案1】:

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

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

这只是猜测......

您是否尝试设置 FieldOffset?见C++ union in C#

【讨论】:

【参考方案2】:

什么是固定缓冲区?

来自 MSDN:

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

我只想引用 Hans Passant 先生关于为什么固定缓冲区必须unsafe。您可能会看到Why is a fixed size buffers (arrays) must be 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];

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

是的,这很不安全。

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

Simon White 提出了一个有效的观点:

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

还有伊巴萨:

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

最后,梅尔达德:

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

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

为什么没有实现?

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

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

【讨论】:

【参考方案3】:

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

一些语言提供多字节数据结构,其内容由 解决算术和间接操作。为了支持这个特性,CLI 允许值类型 以指定的大小创建,但没有关于其数据成员的信息。的实例 这些“不透明类”的处理方式与任何其他类的实例完全相同,但 ldfld、stfld、ldflda、ldsfld 和 stsfld 指令不得用于访问其内容。

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

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

【讨论】:

IntPtr is out”什么时候是真的了? struct S IntPtr p1, p2, p3; 完全不受管理,您可以完全接受sizeof(S)。与struct P void* p1, p2, p3; sizeof(P) 等类似......没有理由禁止这些(而且它们不是)。你的回答对我来说毫无意义。 您似乎将大小问题与 can't-use-ldfld/stfld 问题混淆了。他们没有编写代码来从使用 sizeof 的表达式生成 ldind/stind 地址。埃里克·利珀特 (Eric Lippert) 对为什么不这样做的回答总是一样的。 虽然我不是 C# 专家,但通常像 VM 级别的规范是因为与线程安全相关的原子性。 VM 可以对原语进行原子操作,但不能对任何类型的结构进行操作。这是保持线程安全保证的唯一方法。

以上是关于为啥固定大小的缓冲区只能是原始类型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥没有定义 Java 的布尔原始大小?

为啥固定大小的缓冲区(数组)一定是不安全的?

缓冲区

JS对象类型及区别

为啥结构对齐取决于字段类型是原始类型还是用户定义?

为啥一个地方的原始类型会导致其他地方的通用调用点被视为原始类型?