C# 互操作:fixed 和 MarshalAs 之间的不良交互

C# interop: bad interaction between fixed and MarshalAs

我需要将 C# 4.0 中的一些嵌套结构编组为二进制 blob,以传递给 C++ 框架。

到目前为止,我使用 unsafe/fixed 处理基本类型的固定长度数组取得了很大的成功。现在我需要处理一个包含其他结构的嵌套固定长度数组的结构。

我正在使用复杂的解决方法来展平结构,但后来我遇到了一个 MarshalAs 属性的示例,它看起来可以为我省去很多问题。

不幸的是,虽然它给了我正确的 数量 数据,但它似乎也阻止了 fixed 数组被正确编组,如该程序的输出所示。您可以通过在最后一行放置断点并检查每个指针处的内存来确认失败。

using System;
using System.Threading;
using System.Runtime.InteropServices;

namespace MarshalNested
{
  public unsafe struct a_struct_test1
  {
    public fixed sbyte a_string[3];
    public fixed sbyte some_data[12];
  }

  public struct a_struct_test2
  {
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public sbyte[] a_string;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
  }

  public unsafe struct a_struct_test3
  {
    public fixed sbyte a_string[3];
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
  }


  public unsafe struct a_nested
  {
    public fixed sbyte a_notherstring[3];
  }

  class Program
  {
    static unsafe void Main(string[] args)
    {
      a_struct_test1 lStruct1 = new a_struct_test1();
      lStruct1.a_string[0] = (sbyte)'a';
      lStruct1.a_string[1] = (sbyte)'b';
      lStruct1.a_string[2] = (sbyte)'c';

      a_struct_test2 lStruct2 = new a_struct_test2();
      lStruct2.a_string = new sbyte[3];
      lStruct2.a_string[0] = (sbyte)'a';
      lStruct2.a_string[1] = (sbyte)'b';
      lStruct2.a_string[2] = (sbyte)'c';

      a_struct_test3 lStruct3 = new a_struct_test3();
      lStruct3.a_string[0] = (sbyte)'a';
      lStruct3.a_string[1] = (sbyte)'b';
      lStruct3.a_string[2] = (sbyte)'c';

      IntPtr lPtr1 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct1, lPtr1, false);

      IntPtr lPtr2 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct2, lPtr2, false);

      IntPtr lPtr3 = Marshal.AllocHGlobal(15);
      Marshal.StructureToPtr(lStruct3, lPtr3, false);

      string s1 = "";
      string s2 = "";
      string s3 = "";
      for (int x = 0; x < 3; x++)
      {
        s1 += (char) Marshal.ReadByte(lPtr1+x);
        s2 += (char) Marshal.ReadByte(lPtr2+x);
        s3 += (char) Marshal.ReadByte(lPtr3+x);
      }

      Console.WriteLine("Ptr1 (size " + Marshal.SizeOf(lStruct1) + ") says " + s1);
      Console.WriteLine("Ptr2 (size " + Marshal.SizeOf(lStruct2) + ") says " + s2);
      Console.WriteLine("Ptr3 (size " + Marshal.SizeOf(lStruct3) + ") says " + s3);

      Thread.Sleep(10000);
    }
  }
}

输出:

Ptr1 (size 15) says abc
Ptr2 (size 15) says abc
Ptr3 (size 15) says a

所以出于某种原因,它只是编组我的 fixed ANSI 字符串的第一个字符。有什么办法解决这个问题,还是我做了一些与编组无关的愚蠢事情?

这是一个缺少诊断的案例。 应该有人 站出来告诉您您的声明不受支持。有人是 C# 编译器,产生编译错误,或者是 CLR 字段编组器,产生运行时异常。

并不是说你不能得到诊断。当你真正开始按预期使用结构时,你肯定会得到一个:

    a_struct_test3 lStruct3 = new a_struct_test3();
    lStruct3.some_data = new a_nested[4];
    lStruct3.some_data[0] = new a_nested();
    lStruct3.some_data[0].a_notherstring[0] = (sbyte)'a';  // Eek!

引出 CS1666,"You cannot use fixed size buffers contained in unfixed expressions. Try using the fixed statement"。并不是 "try this" 的建议有用:

    fixed (sbyte* p = &lStruct3.some_data[0].a_notherstring[0])  // Eek!
    {
        *p = (sbyte)'a';
    }

完全相同的 CS1666 错误。接下来您要尝试的是在固定缓冲区上放置一个属性:

public unsafe struct a_struct_test3 {
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public fixed sbyte a_string[3];
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public a_nested[] some_data;
}
//...

    a_struct_test3 lStruct3 = new a_struct_test3();
    lStruct3.some_data = new a_nested[4];
    IntPtr lPtr3 = Marshal.AllocHGlobal(15);
    Marshal.StructureToPtr(lStruct3, lPtr3, false);  // Eek!

让 C# 编译器满意,但现在 CLR 发声了,你在运行时得到一个 TypeLoadException:"Additional information: Cannot marshal field 'a_string' of type 'MarshalNested.a_struct_test3': Invalid managed/unmanaged type combination (this value type must be paired with Struct)."

因此,简而言之,您在最初的尝试中也应该得到 CS1666 或 TypeLoadException。这并没有发生,因为 C# 编译器没有被迫查看错误部分,它只在访问数组的语句上生成 CS1666。它并没有在运行时发生,因为 CLR 中的字段编组器没有尝试编组数组,因为它是空的。您可以在 connect.microsoft.com 提交错误反馈报告,但如果他们不使用 "by design" 关闭它,我会感到非常惊讶。


一般来说,一个模糊的细节对于 CLR 中的字段编组器非常重要,它是将结构值和 class 对象从托管布局转换为非托管布局的代码块。它的文档很少,Microsoft 不想确定确切的实施细节。主要是因为它们太依赖于目标架构。

重要的是值或对象是否 blittable。当托管和非托管布局相同时,它是可复制的。只有当类型的每个成员在两种布局中都具有 exact 相同的大小和对齐方式时才会发生这种情况。这通常只发生在字段是非常简单的值类型(如 byteint)或本身是 blittable 的结构时。众所周知,当它是 bool 时,有太多冲突的非托管 bool 类型。数组类型的字段从不 blittable,托管数组看起来不像 C 数组,因为它们有一个对象头和一个长度成员。

非常需要有一个 blittable 值或对象,它避免了字段编组器必须创建一个副本。本机代码获得一个指向托管内存的简单指针,所需要做的就是固定内存。非常快。这也是非常危险的,如果声明不匹配,那么本机代码很容易在行外着色并破坏 GC 堆或堆栈框架。使用 pinvoke 的程序使用 ExecutionEngineException 随机轰炸的一个非常常见的原因,非常难以诊断。这样的声明确实值得 unsafe 关键字,但 C# 编译器并不坚持。也不能,编译器不允许对托管对象布局做出任何假设。您通过在 Marshal.SizeOf<T> 的 return 值上使用 Debug.Assert() 来保证它的安全,它必须与 C 程序中 sizeof(T) 的值完全匹配。

如前所述,数组是获取可 blittable 值或对象的障碍。 fixed 关键字旨在作为解决此问题的方法。 CLR 将其视为没有成员的不透明值类型,只是一团字节。没有对象头,也没有 Length 成员,尽可能接近 C 数组。在 C# 代码中使用就像在 C 程序中使用数组一样,您必须使用指针来寻址数组元素并检查三遍以确保您没有在行外着色。有时你 必须 使用固定数组,当你声明一个联合(重叠字段)并且你用一个值重叠一个数组时就会发生这种情况。垃圾收集器中毒,它无法再判断该字段是否存储对象根。未被 C# 编译器检测到,但在运行时可靠地触发 TypeLoadException。


长话短说,使用 fixed 作为 blittable 类型。无法将固定大小缓冲区类型的字段与 必须 编组的字段混合使用。并且没有用,无论如何都会复制对象或值,因此您不妨使用友好的数组类型。