使用[StructLayout(LayoutKind.Sequential)]

时间:2018-04-06 07:08:30

标签: c# inheritance unmarshalling vtable

我有一个传输二进制数据的设备。为了解释数据,我定义了一个与数据格式匹配的structstructStuctLayoutAttribute LayoutKind.Sequential。这可以按预期工作,例如:

[StructLayout(LayoutKind.Sequential)]
struct DemoPlain
{
     public int x;
     public int y;
}

Marshal.OffsetOf<DemoPlain>("x");    // yields 0, as expected
Marshal.OffsetOf<DemoPlain>("y");    // yields 4, as expected
Marshal.SizeOf<DemoPlain>();         // yields 8, as expected

现在我想处理一个类似于其他结构的结构,所以我尝试了实现接口的结构:

interface IDemo
{
    int Product();
}


[StructLayout(LayoutKind.Sequential)]
struct DemoWithInterface: IDemo
{
     public int x;
     public int y;
     public int Product() => x * y;
}

Marshal.OffsetOf<DemoWithInterface>("x").Dump();    // yields 0
Marshal.OffsetOf<DemoWithInterface>("y").Dump();    // yields 4
Marshal.SizeOf<DemoWithInterface>().Dump();         // yields 8

令我惊讶的是,DemoWithInterface的偏移量和大小与DemoPlain保持一致,并将相同的二进制数据从设备转换为DemoPlain数组或DemoWithInterface数组{1}}两者都有效。这怎么可能?

C ++实现通常使用vtable(请参阅Where in memory is vtable stored?)来解决虚拟方法。我相信在接口中发布的C#方法和声明为virtual的方法类似于C ++中的虚方法,并且它需要类似于vtable的东西才能找到正确的方法。这是正确的还是C#完全不同?如果正确,vtable结构存储在哪里?如果不同,关于接口继承和虚方法如何实现C#?

2 个答案:

答案 0 :(得分:5)

基本上,&#34;不适用&#34;。 C#中的结构 - 如前所述 - 不支持继承,因此不需要v-table。

字段布局是字段布局。它很简单:实际字段在哪里。实现接口根本不会改变字段,也不需要对布局进行任何更改。这就是为什么尺寸和布局没有受到影响的原因。

一些结构可以(通常应该)覆盖的虚拟方法 - ToString()等等。所以你可以合法地询问&#34;那么它是如何工作的?&#34 ; - 答案是:烟雾和镜子。也称为constrained call。这推迟了&#34;虚拟呼叫与静态呼叫的问题&#34;到JIT。 JIT完全知道该方法是否被覆盖,并且可以发出适当的操作码 - 盒子和虚拟调用(盒子是对象,所以有v-table),或者直接静态调用。“ p>

可能很容易认为编译器应该这样做,而不是JIT - 但结构通常是在外部程序集中,如果编译器发出静态调用,它会是灾难性的,因为它可以看到被覆盖的{{ 1}}等等,然后有人在不重建应用程序的情况下更新库,并且它获得覆盖(ToString())的版本 - 因此受限制的调用更可靠。即使对于嵌入式类型,做同样的事情也更简单,更容易支持。

此约束调用也适用于通用(MissingMethodException)方法 - 因为<T>可能是T。回想一下,对于泛型方法,JIT对值类型struct执行T,因此它可以应用每个类型的逻辑,并在实际已知的静态调用位置进行烘焙。如果您正在使用T这样的内容,而您的.ToString()是一个不会覆盖它的结构:它将改为包装和虚拟调用。

注意,一旦将结构分配给接口变量 - 例如:

T

你有&#34;盒装&#34;它,现在一切都是虚拟呼叫。 具有完整的v表。这就是为什么具有泛型类型约束的泛型方法通常更可取的原因:

DemoWithInterface foo = default;
IDemo bar = foo;
var i = bar.Product();
尽管访问接口成员,

将始终使用受约束的呼叫,并且不需要盒子。 JIT在执行时解析特定DemoWithInterface foo = default; DoSomething(foo); void DoSomething<T>(T obj) where T : IDemo { //... int i = obj.Product(); //... } 的静态调用选项。

答案 1 :(得分:0)

Default Marshalling Behavior | Microsoft Docs,尤其是Value Types Used in Platform Invoke部分提供了答案:

  

当封送到非托管代码时,这些格式化的类型被封送为C样式结构。

  

当格​​式化的类型作为结构封送时,只能访问该类型中的字段。如果类型具有方法,属性或事件,则无法从非托管代码中访问它们。

因此,C#struct的(虚拟)方法被剥离,并且只传输了一个普通的C-struct。在OP的情况下,设备发送包含普通C结构的字节,Marshal.PtrToStructure<T>(IntPtr)将字节转换为C#-struct,并且在DemoWithInterface的情况下,附加Product - 方法和vtable(或其他元数据)使结构实现IDemo