8

I have code like this to emit IL code that loads integer or string values. But I don't know how to add the decimal type to that. It isn't supported in the Emit method. Any solutions to this?

ILGenerator ilGen = methodBuilder.GetILGenerator();
if (type == typeof(int))
{
    ilGen.Emit(OpCodes.Ldc_I4, Convert.ToInt32(value, CultureInfo.InvariantCulture));
}
else if (type == typeof(double))
{
    ilGen.Emit(OpCodes.Ldc_R8, Convert.ToDouble(value, CultureInfo.InvariantCulture));
}
else if (type == typeof(string))
{
    ilGen.Emit(OpCodes.Ldstr, Convert.ToString(value, CultureInfo.InvariantCulture));
}

Not working:

else if (type == typeof(decimal))
{
    ilGen.Emit(OpCodes.Ld_???, Convert.ToDecimal(value, CultureInfo.InvariantCulture));
}

Edit: Okay, so here's what I did:

else if (type == typeof(decimal))
{
    decimal d = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
    // Source: https://msdn.microsoft.com/en-us/library/bb1c1a6x.aspx
    var bits = decimal.GetBits(d);
    bool sign = (bits[3] & 0x80000000) != 0;
    byte scale = (byte)((bits[3] >> 16) & 0x7f);
    ilGen.Emit(OpCodes.Ldc_I4, bits[0]);
    ilGen.Emit(OpCodes.Ldc_I4, bits[1]);
    ilGen.Emit(OpCodes.Ldc_I4, bits[2]);
    ilGen.Emit(sign ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
    ilGen.Emit(OpCodes.Ldc_I4, scale);
    var ctor = typeof(decimal).GetConstructor(new[] { typeof(int), typeof(int), typeof(int), typeof(bool), typeof(byte) });
    ilGen.Emit(OpCodes.Newobj, ctor);
}

But it doesn't generate a newobj opcode, but instead nop and stloc.0. The constructor is found and passed to the Emit call. What's wrong here? Obviously an InvalidProgramException is thrown when trying to execute the generated code because the stack is completely messed up.

ygoe
  • 18,655
  • 23
  • 113
  • 210
  • 1
    Apparently (but don't take my word for it) for "load decimal" there isn't a direct opcode, you load the arguments and call the decimal constructor: see http://stackoverflow.com/a/485834/266143 – CodeCaster Nov 06 '15 at 15:53
  • 1
    See also http://codeblog.jonskeet.uk/2014/08/22/when-is-a-constant-not-a-constant-when-its-a-decimal/. In short: decimals are not CLR primitive types and there's no IL opcode for loading one directly. – Jeroen Mostert Nov 06 '15 at 16:00
  • See my edit above for a non-working solution. – ygoe Nov 06 '15 at 16:37
  • I think this line is wrong: `ilGen.Emit(OpCodes.Ldc_I4, scale)`. You're saying you're loading an I4 (`int`), but then use the `byte` overload of `Emit()`. One way to fix that would be `ilGen.Emit(OpCodes.Ldc_I4, (int)scale);`. – svick Nov 07 '15 at 15:55

2 Answers2

9

Come on, just decompile some C# code that does the same thing - you'll see that there's no decimal primitive.

42M

compiles to

ldc.i4.s    2A
newobj      System.Decimal..ctor

For a decimal number, this is much more complicated:

42.3M

gives

ldc.i4      A7 01 00 00 
ldc.i4.0    
ldc.i4.0    
ldc.i4.0    
ldc.i4.1    
newobj      System.Decimal..ctor

The easiest way to get this for an arbitrary decimal is to use the int[] overload of the constructor and the GetBits static method. You could also reverse-engineer the SetBits method to allow you to call the simpler constructor with the proper values, or use reflection to read the internal state - there's plenty of options.

EDIT:

You're close, but you broke the ILGen - while the last argument to the constructor is a byte, the constant you're loading must be an int. The following works as expected:

var bits = decimal.GetBits(d);
bool sign = (bits[3] & 0x80000000) != 0;
int scale = (byte)((bits[3] >> 16) & 0x7f);
gen.Emit(OpCodes.Ldc_I4, bits[0]);
gen.Emit(OpCodes.Ldc_I4, bits[1]);
gen.Emit(OpCodes.Ldc_I4, bits[2]);
gen.Emit(sign ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
gen.Emit(OpCodes.Ldc_I4, scale);
var ctor = typeof(decimal).GetConstructor(new[] { typeof(int), typeof(int), 
                                                typeof(int), typeof(bool), typeof(byte) });
gen.Emit(OpCodes.Newobj, ctor);
gen.Emit(OpCodes.Ret);

EDIT 2:

A simple example of how you can use expression trees (in this case the tree is built by the C# compiler, but that's up to you) to define dynamic method bodies:

var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Test"), 
                                                     AssemblyBuilderAccess.Run);
var module = assembly.DefineDynamicModule("Test");
var type = module.DefineType("TestType");

var methodBuilder = type.DefineMethod("MyMethod", MethodAttributes.Public 
                                                  | MethodAttributes.Static);
methodBuilder.SetReturnType(typeof(decimal));

Expression<Func<decimal>> decimalExpression = () => 42M;

decimalExpression.CompileToMethod(methodBuilder);

var t = type.CreateType();

var result = (decimal)t.GetMethod("MyMethod").Invoke(null, new object[] {});

result.Dump(); // 42 :)
Luaan
  • 62,244
  • 7
  • 97
  • 116
  • 3
    This is apparently because _"The Extended Numerics Library"_ is not part of the CIL specification, because _"some commonly available processors do not provide direct support for the data types"_ (source: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf, large PDF). That's why there's no opcode for loading a `decimal` (nor a `single`). – CodeCaster Nov 06 '15 at 16:00
  • Thanks for the clues. Unfortunately it still doesn't work properly. See my edit in the question. – ygoe Nov 06 '15 at 16:31
  • 1
    @LonelyPixel Updated with the correct code - `ldc.i4` *must* be passed an `int`. It's a shame that ILGen will let you do this, but you just have to be careful :) However, you don't really need ILGen all that much nowadays - why not use `Expression.Compile`? – Luaan Nov 07 '15 at 16:53
  • @LonelyPixel Or, to clear up the confusion, of course ILGen doesn't catch that error - it doesn't do any validation at all. It simply emits bytes - that's it. Your decompiler then tells you that the `newobj` wasn't emitted, but that isn't actually true - the problem is that the `ldc.i4` before that `newobj` was three bytes shorter than it should have been. I hope that clears up the confusion somehow :) ILGen will always be extremely fragile - it does very little besides translating opcodes and constants into IL bytes. – Luaan Nov 07 '15 at 17:02
  • Thanks, changing it to `int` made it work. The whole system is using a dynamic assembly and `TypeBuilder` so it needs to be extended that way. But now everything's fine. – ygoe Nov 11 '15 at 14:41
  • @LonelyPixel Actually, you can use an expression tree to build a method body - that's what the `CompileToMethod` method is for :) – Luaan Nov 11 '15 at 14:49
  • @LonelyPixel I've added an example of how to do this. There are some limitations, but if you can work within them, you're going to save yourself a lot of trouble in the long run. – Luaan Nov 11 '15 at 14:56
0

As Luaan mentioned before, you can use the decimal.GetBits method and the int[] constructor. Have a look at this example:

public static decimal RecreateDecimal(decimal input)
{
    var bits = decimal.GetBits(input);

    var d = new DynamicMethod("recreate", typeof(decimal), null);
    var il = d.GetILGenerator();

    il.Emit(OpCodes.Ldc_I4_4);
    il.Emit(OpCodes.Newarr, typeof(int));

    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ldc_I4, bits[0]);
    il.Emit(OpCodes.Stelem_I4);

    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldc_I4_1);
    il.Emit(OpCodes.Ldc_I4, bits[1]);
    il.Emit(OpCodes.Stelem_I4);

    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldc_I4_2);
    il.Emit(OpCodes.Ldc_I4, bits[2]);
    il.Emit(OpCodes.Stelem_I4);

    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldc_I4_3);
    il.Emit(OpCodes.Ldc_I4, bits[3]);
    il.Emit(OpCodes.Stelem_I4);

    il.Emit(OpCodes.Newobj, typeof(decimal).GetConstructor(new[] {typeof(int[])}));

    il.Emit(OpCodes.Ret);
    return (decimal) d.Invoke(null, null);
}
thehennyy
  • 4,020
  • 1
  • 22
  • 31