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)
...
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With