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

时间:2016-03-17 22:57:16

标签: c# interop marshalling unsafe

我需要将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字符串的第一个字符。有没有办法解决这个问题,还是我做了一些与编组无关的愚蠢行为?

1 个答案:

答案 0 :(得分: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!

引出CS1666,&#34;您不能使用未固定表达式中包含的固定大小缓冲区。尝试使用固定语句&#34;。不是那样的&#34;尝试这个&#34;建议是有用的:

    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:&#34;附加信息:无法编组字段&#39; a_string&#39;类型&#39; MarshalNested.a_struct_test3&#39;:无效的托管/非托管类型组合(此值类型必须与Struct配对)。&#34;

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

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

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

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

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

长话短说,仅使用fixed 作为blittable类型。将固定大小缓冲区类型的字段与必须编组的字段混合不起作用。并且不是很有用,无论如何都要复制对象或值,这样你也可以使用友好的数组类型。