C#interop:固定和MarshalAs之间的不良交互

Der*_*ren 14 c# interop unsafe marshalling

我需要将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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Ptr1 (size 15) says abc
Ptr2 (size 15) says abc
Ptr3 (size 15) says a
Run Code Online (Sandbox Code Playgroud)

因此,由于某种原因,它只是编组我的fixedANSI字符串的第一个字符.有什么方法可以解决这个问题,还是我做了一些与编组无关的愚蠢行为?

Han*_*ant 14

这是缺少诊断的情况. 有人应该说出来并告诉你,你的声明不受支持.那个人要么是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!
Run Code Online (Sandbox Code Playgroud)

这引出了CS1666,"你不能使用未固定表达式中包含的固定大小的缓冲区.尝试使用fixed语句".不是"尝试这个"建议是有帮助的:

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

完全相同的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!
Run Code Online (Sandbox Code Playgroud)

让C#编译器保持高兴但现在CLR说出来并且你在运行时得到一个TypeLoadException:"附加信息:无法编组'MarshalNested.a_struct_test3'类型的字段'a_string':无效的托管/非托管类型组合(此值类型必须配对与结构)."

因此,简而言之,您应该在原始尝试中获得CS1666或TypeLoadException.这没有发生,因为C#编译器没有被强制查看坏部分,它只在访问数组的语句上生成CS1666.它并没有在运行时发生,因为CLR中的字段编组器没有尝试编组数组,因为它是null.您可以在connect.microsoft.com上提交错误反馈报告,但如果他们不以"按设计"关闭它,我会非常惊讶.


通常,一个模糊的细节对CLR中的字段编组很重要,CLR是将结构值和类对象从其托管布局转换为非托管布局的代码块.它的文档很少,微软不想确定具体的实现细节.主要是因为他们过分依赖目标架构.

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

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

如上所述,数组是获得blittable值或对象的障碍.该fixed关键字旨在解决此问题.CLR将其视为不透明的值类型,没有成员,只是一个字节blob.没有对象头和没有Length成员,尽可能接近C数组.并且在C#代码中使用,就像你在C程序中使用数组一样,你必须使用指针来寻址数组元素,并检查三次你没有在行外面着色.有时您必须使用固定数组,当您声明一个联合(重叠字段)并且您将数组与值重叠时会发生.对垃圾收集器的毒害,它不能再确定该字段是否存储对象根.C#编译器未检测到但在运行时可靠地跳过TypeLoadException.


长话短说,fixed 用于blittable类型.将固定大小缓冲区类型的字段与必须编组的字段混合不起作用.并且无用,无论如何都要复制对象或值,因此您也可以使用友好的数组类型.