This is rather a complicated question, so bear with me.
I have defined a struct that serves as an unmanaged array. Because I am using (and are stuck with) C# 7.3, generic structs with an unmanaged constraint are seen as a managed type. This means I cannot take, or use pointers to this struct. For this reason, the unmanaged array is not declared generic. To have type-safety, when calling the (pseudo-)constructor, the TypeHandle of the generic type is stored along with the object. For every subsequent call the TypeHandle of the generic is compared with the stored TypeHandle to assert it is the same type.
Next, I've wrapped this unmanaged array in another object that IS generic. This severely simplifies the API calls to the unmanaged array. Since you cannot see what's stored in a pointer, I've made a DebuggerTypeProxy that converts the unmanaged array to a managed array. So during debugging, you can view the contents of the unmanaged array.
Now here's where (finally) the problem comes in. When defining this DebuggerTypeProxy as a Class, there is a mismatch between TypeHandles for the unmanaged array call. However, when defining DebuggerRypeProxy as a Struct, there is no mismatch. What exactly is happening here?
Below is some of the code used for the implementation to clarify what I've done. It is heavily trimmed down to only show the relevant parts.
public unsafe struct UnsafeArray
{
void* _buffer;
int _length;
IntPtr _typeHandle;
//Pseudo-constructor
public static UnsafeArray* Allocate<T>(int size) where T : unmanaged
{
//Do a lot of allocation stuff...
UnsafeArray* array;
//Storing the typehandle of the type that is used to construct the unmanaged array
array->_typeHandle = typeof(T).TypeHandle.Value;
return array;
}
//One of the methods that operates on the UnsafeArray
public static T* GetPtr<T>(UnsafeArray* array, long index) where T : unmanaged
{
UDebug.Assert(array != null);
//This assertion fails!!
UDebug.Assert(typeof(T).TypeHandle.Value == array->_typeHandle);
// cast to uint trick, which eliminates < 0 check
if ((uint)index >= (uint)array->_length)
{
throw new IndexOutOfRangeException(index.ToString());
}
return (T*)array->_buffer + index;
}
}
Following is the wrapper that uses the NativeArray internally.
[DebuggerDisplay("Length = {Length}")]
[DebuggerTypeProxy(typeof(Debug.TypeProxies.NativeArrayDebugView<>))]
public unsafe struct NativeArray<T> : IDisposable, IEnumerable<T>, IEnumerable where T : unmanaged
{
private UnsafeArray* m_inner;
public T[] ToArray()
{
var arr = new T[Length];
Copy(this, 0, arr, 0, arr.Length);
return arr;
}
//A lot of other stuff....
}
Lastly, the type used for the DebuggerTypeProxy
internal struct NativeArrayDebugView<T> where T : unmanaged
{
private readonly NativeArray<T> m_array;
public NativeArrayDebugView(NativeArray<T> array)
{
m_array = array;
}
public T[] Items
{
get
{
if (!m_array.IsCreated)
throw new System.NullReferenceException();
//This fails as it makes an internal call to the UnsafeArray with the wrong type!
return m_array.ToArray();
}
}
}
Lastly, it is worth mentioning the NativeArrayDebugView works perfectly fine when manually creating it and viewing its contents. It is only broken under the following conditions:
- The NativeArrayDebugView is a class
- The NativeArrayDebugView is accessed via the debug view when viewing a NativeArray
I understand it is a very long and probably complicated question, so if anyone needs clarification I'd happily try and provide it.