7

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?

Kasper van den Berg
  • 8,951
  • 4
  • 48
  • 70
  • 1
    "are similar to virtual methods in C++" - no, they can't be. Structures do not support inheritance and therefore no virtual/override. And even on a class, an interface is something else and does not imply virtual. – H H Apr 06 '18 at 07:12
  • 3
    Since structs cannot be inherited, there is no need for vtable pointer in struct itself, even if it implements interface. Say you have `var s = new DemoWithInterface(); s.Product()`. There is no virtual dispatch needed here, only one specific method can be called. However, if struct is boxed - then it's another story and there is such entry, but boxed struct is not the same as unboxed one. So here: `IDemo s = new DemoWithInterface(); s.Product()` virtual dispatch is needed, but struct is boxed and represented differently (not just plain fields). – Evk Apr 06 '18 at 08:01

2 Answers2

10

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.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
0

Default Marshalling Behavior | Microsoft Docs and especially the section Value Types Used in Platform Invoke provides the answer:

When marshaled to unmanaged code, these formatted types are marshaled as C-style structures.

and

When a formatted type is marshaled as a structure, only the fields within the type are accessible. If the type has methods, properties, or events, they are inaccessible from unmanaged code.

So the C# struct's (virtual) methods are stripped and only a plain C-struct in transmitted. In the OP's case the device sends the bytes which comprise a plain C-struct, Marshal.PtrToStructure<T>(IntPtr) converts the bytes to a C#-struct and, in case of DemoWithInterface, attaches the Product-method and the vtable (or other meta-data) to make the struct implement IDemo.

Kasper van den Berg
  • 8,951
  • 4
  • 48
  • 70
  • 2
    As pointed out in the comments, C# `structs` cannot *have* virtual methods so wondering where they are is a bit of a miscue. – Damien_The_Unbeliever Apr 06 '18 at 08:04
  • 2
    That is not very accurate, struct types do have virtual methods (ToString, Equals, GetHashCode) and overriding them is fine. Does not change the outcome of this code at all. This is not good Q+A, modeling the world flat by trying to map the .NET type system onto C++ is unhelpful and does nothing but ingrain a bad mental model. If you want somebody to write a book then consider putting a bounty on the question, otherwise everybody is better off without this Q+A. – Hans Passant Apr 07 '18 at 16:05
  • @HansPassant bounty set, I hope to learn more; I should probably improve the question as well. Currently I have no idea how to improve it though, I'll think about how to reword the question. – Kasper van den Berg Apr 10 '18 at 07:45