Inconsitent behavior of `Marshal.SizeOf<T>()` and `Marshal.SizeOf<T>(T structure)`

138 Views Asked by At

I see inconsistent behavior of the Marshal.SizeOf<> method overloads. For brevity, I will consider here only the generic overloads.

This was tried with the .NET 8 runtime.

ISSUE 1: Marshal.SizeOf<T>() allows too little

Consider this code:

using System.Collections.Generic;
using System.Runtime.InteropServices;

...

    int size1a = Marshal.SizeOf<KeyValuePair<int, int>>(KeyValuePair.Create(123, 456));  // good, gives 8
    int size1b = Marshal.SizeOf<KeyValuePair<int, int>>(default);                        // good, gives 8
    int size2 = Marshal.SizeOf<KeyValuePair<int, int>>();                                // throws?!?

Explanation: KeyValuePair<TKey, TValue> is a struct which holds two nonstatic fields of type TKey and TValue, respectively. When I construct it with int (struct without managed fields) on both positions TKey and TValue, I have a type KeyValuePair<int, int> which holds no reference type members and is therefore "unmanaged".

Expected: size2 can be determined (as 8) just as size1a and size1b

Actual: blows up with an ArgumentException saying that T cannot be a generic type

Question: Is it not entirely inconsistent that the size cannot be found, when it is found with no issue by the overload taking an arg structure?

Note that the closed type KeyValuePair<int, int> will happily satisfy the generic constraint where TSomething : unmanaged which exists since C# 7.3 (from 2018). This also makes it natural to expect that its size can be determined.

ISSUE 2: Marshal.SizeOf<T>(T structure) allows too much!

Let us put in manged types in KeyValuePair<,>:

using System.Collections.Generic;
using System.Runtime.InteropServices;

...

    int size3 = Marshal.SizeOf<KeyValuePair<string, string>>(KeyValuePair.Create("arbitrarily long text", "can go in here"));

Expected: Call should lead to an exception since the type/instance holds references to managed objects (in this example of type System.String (class)).

Actual: Call succeeds, and returns a value that depends on build platform. For example, size3 becomes 8 (== 4+4) when built for x86 and becomes 16 (== 8+8) when built for x64, and so on.

Question: Why? I am seeing twice the width of the runtime's references.

This works correctly on .NET Framework 4.8. Here, attempting to find size3 (must use new KeyValuePair<string, string>(...) explicitly since static Create method is absent) leads to:

ArgumentException: Type 'System.Collections.Generic.KeyValuePair`2[System.String,System.String]' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.

So ISSUE 2 must be new somehow on .NET Core. As I said, I am using .NET 8.


Should I report this as bugs in .NET, or am I overlooking something?

1

There are 1 best solutions below

1
Afaq Ali Shah On

Alternatives:

For Issue 1: When dealing with unmanaged types, you can explicitly specify the type when calling Marshal.SizeOf. For example, instead of Marshal.SizeOf<KeyValuePair<int, int>>(), you can do the following:

int size2 = Marshal.SizeOf(typeof(KeyValuePair<int, int>));

This way, you explicitly provide the type information, and it should work without throwing an exception.

For Issue 2: You can avoid the issue by not using KeyValuePair with reference types when you need to calculate the size. Instead, you can create a custom struct to represent the data you want to work with, ensuring it contains only value types (not reference types). For example:

public struct MyKeyValuePair
{
    public string Key;
    public string Value;
}

Then, you can calculate the size of this custom struct without encountering issues related to reference types:

int size3 = Marshal.SizeOf(typeof(MyKeyValuePair));
By using a custom struct that only contains value types, you avoid the problem of Marshal.SizeOf incorrectly calculating the size based on reference types.

Keep in mind that these workarounds may require refactoring your code, especially in the case of Issue 2, where you'd need to replace usages of KeyValuePair with your custom struct. These workarounds address the limitations of the Marshal.SizeOf method in .NET Core/.NET 5+ for the specific scenarios you've described.