为什么固定大小的缓冲区只能是基本类型?

23

我们需要经常与本地代码进行互操作,在这种情况下,使用不需要编组的非安全结构体会快得多。但是,当结构包含非原始类型的固定大小缓冲区时,我们无法这样做。 为什么C#编译器要求固定大小的缓冲区只能是原始类型?为什么不能将固定大小的缓冲区设置为结构体,例如:

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

2
但是编译器已经完成了这个。如果您尝试创建一个指向包含任何这些内容的结构体的指针,您会收到编译器错误:http://msdn.microsoft.com/en-us/library/x2estayf(v=vs.90).aspx - jakobbotsch
您可以查看 http://tutorials.csharp-online.net/Use_Interop%E2%80%94Fixed_Size_Buffers。 - user1088520
“但这已经被编译器完成了。”只是部分地完成了。编译器可以检查类型是否受管理,但这并不能处理生成代码以读取/写入结构到固定缓冲区的问题。它可以完成(在CIL级别上没有任何阻止),只是在C#中没有实现。 - Ibasa
3
@Mehrdad那有点阴谋论,你觉得呢? - Blorgbeard
@Blorgbeard:当然,但这是我能想到的唯一合乎逻辑的解释。否则你怎么解释在C#中与本地代码交互如此困难的事实呢?我再给你举个例子:如果泛型类型不包含任何托管成员,它们为什么不能是非托管的呢?但他们积极地阻止制作非托管本机类型,这并没有技术上的理由。他们为什么要这样做呢?对我来说,最合乎逻辑的解释可能是他们想推广托管代码。 - user541686
显示剩余3条评论
3个回答

22

在C#中,固定大小的缓冲区是通过一个名为“opaque classes”的CLI特性实现的。Ecma-335的第I.12.1.6.3节对它们进行了描述:

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

"无法获取数据成员的信息"和"不得使用ldfld/stfld"是关键。第二条规则禁止结构体,因为需要使用ldfld和stfld来访问它们的成员。C#编译器无法提供替代方案,结构体的布局是运行时实现细节。Decimal和Nullable<>也不行,因为它们也是结构体。IntPtr也不行,因为其大小取决于进程的位数,这使得C#编译器很难生成用于访问缓冲区的ldind/stind操作码的地址。引用类型的引用也不行,因为GC需要能够找到它们,但根据第一条规则,它们无法被找回。枚举类型的大小取决于其基础类型,听起来像是可以解决的问题,但不确定为什么要跳过它。

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


1
自从“IntPtr已经过时”是真的吗? struct S { IntPtr p1, p2, p3; } 是完全不受管理的,您可以很好地使用 sizeof(S)。 同样,对于 struct P { void* p1, p2, p3; }sizeof(P) 等等... 没有理由禁止这些(它们也没有被禁止)。 您的回答对我来说毫无意义。 - user541686
1
你似乎把大小问题和不能使用ldfld/stfld的问题混淆了。他们没有编写代码从使用sizeof的表达式生成ldind/stind地址。Eric Lippert对为什么不这样做的回答总是相同的。 - Hans Passant
虽然我不是C#大师,但通常在VM级别上这样的规格是与线程安全相关的原子性有关。 VM可以原子地操作基元,但不能对任何类型的结构进行操作。这是保持线程安全保证的唯一方法。 - WeaponsGrade

5

什么是固定缓冲区?

来自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特定功能应用于适用于枚举项的结构体。例如,泛型、接口实现、非基元数组的更深层次属性等。毫无疑问,运行时也会有一些与此类事情的交互问题。”
Ibasa说:
“但这只是部分实现了。编译器可以检查类型是否受管控,但这并不能处理生成代码来读取/写入结构体到固定缓冲区的问题。它可以做到(在CIL级别没有任何障碍),只是在C#中没有实现。”
最后,Mehrdad说:
“我认为这实际上是因为他们不希望您使用固定大小的缓冲区(因为他们希望您使用托管代码)。使与本地代码的互操作变得太容易会使您不太可能将.NET用于所有内容,他们希望尽可能推广托管代码。”
答案似乎是“它只是没有实现”。
为什么没有实现呢?
我的猜测是,对开发人员来说,成本和实现时间不值得。开发人员更愿意在C#中推广托管代码而不是非托管代码。未来版本的C#可能会实现它,但当前的CLR缺少许多所需的复杂性。
另一种选择可能是安全问题。考虑到如果在您的代码中实现不良,则固定缓冲区极易受到各种问题和安全风险的攻击,我可以理解为什么在C#中鼓励使用托管代码而不是它们。为什么要花费大量精力去做一些您想要阻止使用的事情呢?

0

我理解你的观点...另一方面,我认为这可能是Microsoft保留的某种前向兼容性。你的代码被编译成MSIL,并且它的布局在内存中是特定.NET框架和操作系统的业务。

我可以想象到会有新的Intel CPU问世,它要求将变量按照每8字节布局以获得最佳性能。在那种情况下,未来可能需要在某个未来的.NET框架6和Windows 9中以不同方式布局这些结构体。在这种情况下,你的示例代码将对Microsoft构成压力,不要改变将来的内存布局,也不要将.NET框架加速到现代硬件。

这只是猜测...

你尝试过设置FieldOffset吗?参考C++中的C#联合


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接