Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where does C# store a structure's vtable when unmarshalling using [StructLayout(LayoutKind.Sequential)]

I have a device that transmits binary data. To interpret the data I have defined a struct that matches the data format. The struct has a StuctLayoutAttribute with LayoutKind.Sequential. This works as expected, e.g:

[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

Now I wish to treat one struct similar to an other struct, so I experimented with the structure implementing an interface:

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

To my surprise the offsets and size of DemoWithInterface remain the same as DemoPlain and converting the same binary data from the device to either an an array of DemoPlain or an array of DemoWithInterface both work. How is this possible?

C++ implementations often use a vtable (see Where in memory is vtable stored?) to sore virtual methods. I believe that in C# methods published in an interface, and methods that are declared virtual, are similar to virtual methods in C++ and that it requires something similar to a vtable to find the correct method. Is this correct or does C# do it completely different? If correct, where is the vtable like structure stored? If different, how is C# implemented with respect to interface inheritance and virtual methods?

like image 253
Kasper van den Berg Avatar asked Apr 06 '18 07:04

Kasper van den Berg


1 Answers

Basically, "does not apply". Structs in C# - as has been discussed - do not support inheritance, and so no v-table is required.

The field layout is the field layout. It is simply: where the actual fields are. Implementing interfaces doesn't change the fields at all, and doesn't require any change to the layout. So that's why the size and layout isn't impacted.

There are some virtual methods that structs can (and usually should) override - ToString() etc. So you can legitimately ask "so how does that work?" - and the answer is: smoke and mirrors. Also known as constrained call. This defers the question of "virtual call vs static call" to the JIT. The JIT has full knowledge as to whether the method is overridden or not, and can emit appropriate opcodes - either a box and virtual call (a box is an object, so has a v-table), or a direct static call.

It might be tempting to think that the compiler should do this, not the JIT - but often the struct is in an external assembly, and it would be catastrophic if the compiler emitted a static call because it could see the overridden ToString() etc, and then someone updates the library without rebuilding the app, and it gets a version that doesn't override (MissingMethodException) - so constrained call is more reliable. And doing the same thing even for in-assembly types is just simpler and easier to support.

This constrained call also happens for generic (<T>) methods - since the T could be a struct. Recall that the JIT executes per T for value-typed T on a generic method, so it can apply this logic per-type, and bake in the actual known static call locations. And if you're using something like .ToString() and your T is a struct that doesn't override that: it will box and virtual-call instead.

Note that once you assign a struct to an interface variable - for example:

DemoWithInterface foo = default;
IDemo bar = foo;
var i = bar.Product();

you have "boxed" it, and everything is now virtual-call on the box. A box has a full v-table. That is why generic methods with generic type constraints are often preferable:

DemoWithInterface foo = default;
DoSomething(foo);

void DoSomething<T>(T obj) where T : IDemo
{
    //...
    int i = obj.Product();
    //...
}

will use constrained call throughout and will not require a box, despite accessing interface members. The JIT resolves the static call options for the specific T at execution.

like image 72
Marc Gravell Avatar answered Nov 15 '22 00:11

Marc Gravell