Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to emit OpCodes.Constrained with OpCodes.Callvirt given I have the needed MethodInfo and instance Type at hand

I have a recursive function emit : Map<string,LocalBuilder> -> exp -> unit where il : ILGenerator is global to the function and exp is a discriminant union representing a type-checked parsed language with case InstanceCall of exp * MethodInfo * exp list * Type and Type is a property on exp representing the Type of the expression.

In the following fragment, I am trying to emit IL opcodes for an instance call where instance.Type may or may not be a ValueType. So I understand I can use OpCodes.Constrained to flexibly and efficiently make virtual calls on reference, value, and enum types. I'm new to Reflection.Emit and machine languages in general so understanding the linked documentation for OpCodes.Constrained is not strong for me.

Here is my attempt, but it results in a VerificationException, "Operation could destabilize the runtime.":

let rec emit lenv ast =
    match ast with
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        instance::args |> List.iter (emit lenv)
        il.Emit(OpCodes.Constrained, instance.Type)
        il.Emit(OpCodes.Callvirt, methodInfo)
    ...

Looking at the docs, I think the key may be "A managed pointer, ptr, is pushed onto the stack. The type of ptr must be a managed pointer (&) to thisType. Note that this is different from the case of an unprefixed callvirt instruction, which expects a reference of thisType."

Update

Thank you @Tomas and @desco, I now understand when to use OpCodes.Constrained (instance.Type is a ValueType, but methodInfo.DeclaringType is a reference type).

But it turns out I don't need to consider that case just yet, and my real problem was the instance argument on the stack: it only took me 6 hours to learn it needs an address instead of the value (looking at the DLR source code gave me clues, and then using ilasm.exe on a simple C# program made it clear).

Here is my final working version:

let rec emit lenv ast =
    match ast with
    | Int32(x,_) -> 
        il.Emit(OpCodes.Ldc_I4, x)
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        emit lenv instance
        //if value type, pop, put in field, then load the field address
        if instance.Type.IsValueType then
            let loc = il.DeclareLocal(instance.Type)
            il.Emit(OpCodes.Stloc, loc)
            il.Emit(OpCodes.Ldloca, loc)

        for arg in args do emit lenv arg

        if instance.Type.IsValueType then
            il.Emit(OpCodes.Call, methodInfo)
        else
            il.Emit(OpCodes.Callvirt, methodInfo)
        ...
like image 210
Stephen Swensen Avatar asked Oct 11 '22 09:10

Stephen Swensen


2 Answers

Basically I agree with Tomas: if you know the exact type at compile time, then you can emit correct call instruction yourself. Constrained prefix is usually used for generic code

But documentation also says:

The constrained opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr is a value type or a reference type. Although it is intended for the case where thisType is a generic type variable, the constrained prefix also works for nongeneric types and can reduce the complexity of generating virtual calls in languages that hide the distinction between value types and reference types. ...

Using the constrained prefix also avoids potential versioning problems with value types. If the constrained prefix is not used, different IL must be emitted depending on whether or not a value type overrides a method of System.Object. For example, if a value type V overrides the Object.ToString() method, a call V.ToString() instruction is emitted; if it does not, a box instruction and a callvirt Object.ToString() instruction are emitted. A versioning problem can arise in the former case if the override is later removed, and in the latter case if an override is later added.

Small demonstration (shame on me, I don't have F# on my netbook):

using System;
using System.Reflection;
using System.Reflection.Emit;

public struct EvilMutableStruct
{
    int i;
    public override string ToString()
    {
            i++;
            return i.ToString();
    }
}

class Program
{
    public static void Main()
    {
            var intToString = Make<int>();
            var stringToString = Make<string>();
            var structToString = Make<EvilMutableStruct>();
            Console.WriteLine(intToString(5));
            Console.WriteLine(stringToString("!!!"));   
            Console.WriteLine(structToString (new EvilMutableStruct())); 
    }

    static MethodInfo ToStringMethod = new Func<string>(new object().ToString).Method;
    static MethodInfo ConcatMethod = new Func<string, string, string>(String.Concat).Method;

    // x => x.ToString() + x.ToString()
    private static Func<T, string> Make<T>()
    {
            var dynamicMethod = new DynamicMethod("ToString", typeof(string), new[] {typeof(T)});
            var il = dynamicMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Call, ConcatMethod);

            il.Emit(OpCodes.Ret);
            return (Func<T, string>)dynamicMethod.CreateDelegate(typeof(Func<T, string>));
     }
}

Output:

55
!!!!!!
12
like image 180
desco Avatar answered Oct 12 '22 23:10

desco


I think that the bit of documentation that you quoted at the end of the question is the source of the problem. I'm not quite sure what the OpCodes.Constrained prefix is for (I don't understand the documentation better than you), but I tried looking how it is used by Microsoft :-).

Here is a snippet from source code of Dynamic Language Runtime that emits a method call:

// Emit arguments
List<WriteBack> wb = EmitArguments(mi, args);

// Emit the actual call
OpCode callOp = UseVirtual(mi) ? OpCodes.Callvirt : OpCodes.Call;
if (callOp == OpCodes.Callvirt && objectType.IsValueType) {
    // This automatically boxes value types if necessary.
    _ilg.Emit(OpCodes.Constrained, objectType);
}
// The method call can be a tail call if [...]
if ((flags & CompilationFlags.EmitAsTailCallMask) == CompilationFlags.EmitAsTail && 
    !MethodHasByRefParameter(mi)) {
    _ilg.Emit(OpCodes.Tailcall);
}
if (mi.CallingConvention == CallingConventions.VarArgs) {
    _ilg.EmitCall(callOp, mi, args.Map(a => a.Type));
} else {
    _ilg.Emit(callOp, mi);
}

// Emit writebacks for properties passed as "ref" arguments
EmitWriteBack(wb);

I think you'll probably want to follow their behavior - it seems that the constrained prefix is only used for virtual calls on value types. My interpretation is that for value types, you know what is the actual type, so you don't need actual (unconstrained) virtual call.

like image 24
Tomas Petricek Avatar answered Oct 12 '22 21:10

Tomas Petricek