Conversion between primitive types is trivial in high-level languages like C# as you can use the cast operator, optionally surrounded with checked or unchecked to force the integer overflow semantics. At CIL level the conversion is less trivial, involving the choice of no fewer than 27 different op-codes, which are required to be used in combination with each other under some circumstances.
This is yet another place where extension methods are incredibly useful, because rather than having to remember all this it is possible to capture the information about how to convert between two types in a method and expose it alongside the low-level ones on the ILGenerator class. The extension method to do this is below (note that IsUnsignedInteger is an extension method for the Type class which returns whether the type is one of byte, ushort, uint or ulong).
public static void EmitConv(this ILGenerator il, Type fromType, Type toType, bool ovf)
{
if (fromType == toType)
{
// no conversion required
}
else if (toType == typeof(float) || toType == typeof(double))
{
// if converting to a floating point number then we must use a conv.* instruction as there are no ovf
// checked operations that work with them. if the starting point is an unsigned integer then we must
// convert it to a real first as conv.r4 and conv.r8 don't work with unsigned integers (at IL level
// the char data type is considered to be a 2-byte unsigned integer)
if (fromType.IsUnsignedInteger() || fromType == typeof(char))
{
il.Emit(OpCodes.Conv_R_Un);
}
EmitConv(il, toType);
}
else if (ovf)
{
// overflow checking required so use conv.ovf.* or conv.ovf.*.un depending on the source type
if (fromType.IsUnsignedInteger() || fromType == typeof(char))
{
EmitConv_Ovf_Un(il, toType);
}
else
{
EmitConv_Ovf(il, toType);
}
}
else
{
// no overflow checking so use the conv.* instruction
EmitConv(il, toType);
}
}
Not many of the 27 instructions present there; just one to handle the special case of converting unsigned integers to reals. The real heavy lifting is done by the three other Emit* methods, which all use dictionary lookups in the same way as my post about emitting arbitrary constants. I doubt there's any point in putting more text at the bottom of the post, so I'll say now that this code has been tested by over 350 unit tests which cover every possible type conversion, using both in-range and out-of-range values for both checked and unchecked variants.
Here are the lookup dictionaries:
private static readonly IDictionary<Type, Action<ILGenerator>> emitConvMethods =
new Dictionary<Type, Action<ILGenerator>>
{
{ typeof(sbyte), il => il.Emit(OpCodes.Conv_I1) },
{ typeof(byte), il => il.Emit(OpCodes.Conv_U1) },
{ typeof(short), il => il.Emit(OpCodes.Conv_I2) },
{ typeof(ushort), il => il.Emit(OpCodes.Conv_U2) },
{ typeof(int), il => il.Emit(OpCodes.Conv_I4) },
{ typeof(uint), il => il.Emit(OpCodes.Conv_U4) },
{ typeof(long), il => il.Emit(OpCodes.Conv_I8) },
{ typeof(ulong), il => il.Emit(OpCodes.Conv_U8) },
{ typeof(float), il => il.Emit(OpCodes.Conv_R4) },
{ typeof(double), il => il.Emit(OpCodes.Conv_R8) },
{ typeof(char), il => il.Emit(OpCodes.Conv_U2) },
};
private static readonly IDictionary<Type, Action<ILGenerator>> emitConvOvfMethods =
new Dictionary<Type, Action<ILGenerator>>
{
{ typeof(sbyte), il => il.Emit(OpCodes.Conv_Ovf_I1) },
{ typeof(byte), il => il.Emit(OpCodes.Conv_Ovf_U1) },
{ typeof(short), il => il.Emit(OpCodes.Conv_Ovf_I2) },
{ typeof(ushort), il => il.Emit(OpCodes.Conv_Ovf_U2) },
{ typeof(int), il => il.Emit(OpCodes.Conv_Ovf_I4) },
{ typeof(uint), il => il.Emit(OpCodes.Conv_Ovf_U4) },
{ typeof(long), il => il.Emit(OpCodes.Conv_Ovf_I8) },
{ typeof(ulong), il => il.Emit(OpCodes.Conv_Ovf_U8) },
{ typeof(char), il => il.Emit(OpCodes.Conv_Ovf_U2) },
};
private static readonly IDictionary<Type, Action<ILGenerator>> emitConvOvfUnMethods =
new Dictionary<Type, Action<ILGenerator>>
{
{ typeof(sbyte), il => il.Emit(OpCodes.Conv_Ovf_I1_Un) },
{ typeof(byte), il => il.Emit(OpCodes.Conv_Ovf_U1_Un) },
{ typeof(short), il => il.Emit(OpCodes.Conv_Ovf_I2_Un) },
{ typeof(ushort), il => il.Emit(OpCodes.Conv_Ovf_U2_Un) },
{ typeof(int), il => il.Emit(OpCodes.Conv_Ovf_I4_Un) },
{ typeof(uint), il => il.Emit(OpCodes.Conv_Ovf_U4_Un) },
{ typeof(long), il => il.Emit(OpCodes.Conv_Ovf_I8_Un) },
{ typeof(ulong), il => il.Emit(OpCodes.Conv_Ovf_U8_Un) },
{ typeof(char), il => il.Emit(OpCodes.Conv_Ovf_U2_Un) },
};
And here are the methods that make use of them:
private static void EmitConv(ILGenerator il, Type toType)
{
if (emitConvMethods.ContainsKey(toType))
{
var emitConv = emitConvMethods[toType];
emitConv(il);
}
else
{
throw new NotSupportedException("A conv.* op-code cannot be emitted for " + toType + ".");
}
}
private static void EmitConv_Ovf(ILGenerator il, Type toType)
{
if (emitConvOvfMethods.ContainsKey(toType))
{
var emitConvOvf = emitConvOvfMethods[toType];
emitConvOvf(il);
}
else
{
throw new NotSupportedException("A conv.ovf.* op-code cannot be emitted for " + toType + ".");
}
}
private static void EmitConv_Ovf_Un(ILGenerator il, Type toType)
{
if (emitConvOvfUnMethods.ContainsKey(toType))
{
var emitConvOvfUn = emitConvOvfUnMethods[toType];
emitConvOvfUn(il);
}
else
{
throw new NotSupportedException("A conv.ovf.*.un op-code cannot be emitted for " + toType + ".");
}
}
Posted
Aug 20 2008, 10:34 PM
by
Greg Beech