Why is TypedReference.MakeTypedReference so constrained?

1.4k views Asked by At

I've finally understood the usage of the TypedReference.MakeTypedReference method, but why are the arguments so limited? The underlying private InternalMakeTypedReference(void* result, object target, IntPtr[] flds, RuntimeType lastFieldType) can do a lot more things than the MakeTypedReference that limits the field array to have elements and the field types to be non-primitive.

I've made a sample usage code that shows the full possibility of it:

private static readonly MethodInfo InternalMakeTypedReferenceMethod = typeof(TypedReference).GetMethod("InternalMakeTypedReference", flags);
private static readonly Type InternalMakeTypedReferenceDelegateType = ReflectionTools.NewCustomDelegateType(InternalMakeTypedReferenceMethod.ReturnType, InternalMakeTypedReferenceMethod.GetParameters().Select(p => p.ParameterType).ToArray());
private static readonly Delegate InternalMakeTypedReference = Delegate.CreateDelegate(InternalMakeTypedReferenceDelegateType, InternalMakeTypedReferenceMethod);

public static void MakeTypedReference([Out]TypedReference* result, object target, params FieldInfo[] fields)
{
    IntPtr ptr = (IntPtr)result;
    IntPtr[] flds = new IntPtr[fields.Length];
    Type lastType = target.GetType();
    for(int i = 0; i < fields.Length; i++)
    {
        var field = fields[i];
        if(field.IsStatic)
        {
            throw new ArgumentException("Field cannot be static.", "fields");
        }
        flds[i] = field.FieldHandle.Value;
        lastType = field.FieldType;
    }
    InternalMakeTypedReference.DynamicInvoke(ptr, target, flds, lastType);
}

Unfortunately, actually calling it needs more hacks, as it can't be invoked from MethodInfo and one parameter is RuntimeType, so the delegate type has to be generated dynamically (DynamicMethod can be also used).

Now what can this do? It can access any field (class or struct type, even primitive) of any value of any object without limitations. Moreover, it can create a reference to a boxed value type.

object a = 98;
TypedReference tr;
InteropTools.MakeTypedReference(&tr, a);
Console.WriteLine(__refvalue(tr, int)); //98
__refvalue(tr, int) = 1;
Console.WriteLine(a); //1

So why have the developers seemingly senselessly decided to disallow this kind of usage, while this is obviously useful?

2

There are 2 answers

0
Glenn Slayden On

Blame Plato and his darn "theory of types"...

It is the inherent in the nature of any ref (managed pointer) reference--including the new C# 7 ref local and ref return features--and as you observe, TypedReference, that you can use such for both reading and writing to the target. Because isn't that the whole point?

Now because the CTS can't rule-out either of those possibilities, strong-typing requires that the Type of every ref be constrained from both above and below in the type hierarchy.

More formally, the Type is constrained to be the intersection of the polymorphic covariance and contravariance for which it would otherwise be eligible. Obviously, the result of this intersection collapses to a single Type, itself, which is henceforth invariant.

0
György Kőszeg On

So why have the developers seemingly senselessly decided to disallow this kind of usage, while this is obviously useful?

Because we don't need it if we have fields.

What you are doing here in a very complicated way is basically the following:

((Int32)a).m_value = 1;

Of course, in pure C# we cannot do this because a ((Point)p).X = 1 like assignment fails with CS0445: Cannot modify the result of an unboxing conversion.

Not to mention that Int32.m_value is int, which is the Int32 struct again. You cannot create such a value type in C#: CS0523: Struct member causes a cycle in the struct layout.

The MakeTypedReference actually returns a TypedReference for a FieldInfo. A little bit cleaner version of the unrestricted variant could be:

// If target contains desiredField, then returns it as a TypedReference,
// otherwise, returns the reference to the last field
private static unsafe void MakeTypedReference(TypedReference* result, object target, FieldInfo desiredField = null)
{
    var flds = new List<IntPtr>();
    Type lastType = target.GetType();
    foreach (FieldInfo f in target.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    {
        flds.Add(f.FieldHandle.Value);
        lastType = f.FieldType;
        if (f == desiredField)
            break;
    }

    InternalMakeTypedReference.DynamicInvoke((IntPtr)result, target, flds.ToArray(), lastType);
}

So if target is an int, it returns the reference to the m_value field, which is the int value itself.

But if you deal with a FieldInfo anyway, it is much more simple to use its SetValue for the same effect:

object a = 98;
FieldInfo int32mValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");
int32mValue.SetValue(a, 1);
Console.WriteLine(a); // 1

If you really want to use a TypedReference (without the reflection API), then you can use it directly on the object and then access the boxed value via this reference. All you need to know is the memory layout of a managed object reference:

Managed objects memory layout

object a = 98;

// pinning is required to prevent GC reallocating the object during the pointer operations
var objectPinned = GCHandle.Alloc(a, GCHandleType.Pinned);
try
{
    TypedReference objRef = __makeref(a);

    // objRef.Value->object->boxed content
    int* rawContent = (int*)*(IntPtr*)*(IntPtr*)&objRef;

    // A managed object reference points to the type handle
    // (that is another pointer to the method table), which is
    // followed by the first field.
    if (IntPtr.Size == 4)
        rawContent[1] = 1;
    else
        rawContent[2] = 1;
}
finally
{
    objectPinned.Free();
}

Console.WriteLine(a); // 1

But actually this is just a bit faster than the FieldInfo.SetValue version, mainly due to the object pinning.