Search code examples
c#structoverridingcilboxing

Why does calling an unoverridden struct method require boxing?


According to boxing on structs when calling ToString(), calling an unoverridden method on a C# struct causes it to be boxed. This is ultimately because of how the constrained callvirt IL instruction works, which, according to the documentation, says that:

If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.

However, it doesn't explain why that is the way it is, especially if the struct does implement the method, it doesn't box the struct:

If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.

Why can't the runtime just do the same even if the method wasn't implemented or overridden by the struct? This probably has something to do with how the runtime calls methods, but I can't figure it out.


Solution

  • Simply, the method object.ToString() is written under the assumption that this will be an instance of object. (That is, after all, how instance methods work). You can't pass it a pointer to a struct and expect it to work.

    All reference types have an object header and method table. However, structs do not (there's no need). Therefore if you just pass a (pointer to a) struct to a method which is expecting a pointer to an object, that method will probably crash in exciting and fatal ways.

    When you box a struct, it gets copied to the heap, and it's given an object header and method table. This is why a boxed struct is a valid object. Therefore, the way to call a method which expects an object, but pass a struct, is to box the struct.

    When you define an override of ToString on the struct itself, that method expects a pointer to that struct (and doesn't assume that the pointer points to a method table), so you can directly pass a struct without boxing.

    (Actually, when you override ToString in a struct, the runtime emits two methods. One is the method you defined which expects this to be a pointer to a struct, and the other is a "thunk" which expects this to be an object (a boxed struct), which tweaks the this pointer to point to the struct itself rather than the method table, then calls the first method. This lets you call ToString on a boxed struct. I won't repeat the gory details, but see this excellent article by Matt Warren).