Emit IL code to load a decimal value
Asked Answered
A

2

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.

Antifreeze answered 6/11, 2015 at 15:49 Comment(4)
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 https://mcmap.net/q/274741/-why-can-39-t-you-assign-a-number-with-a-decimal-point-to-decimal-type-directly-without-using-type-suffixNoneffective
See also codeblog.jonskeet.uk/2014/08/22/…. In short: decimals are not CLR primitive types and there's no IL opcode for loading one directly.Poindexter
See my edit above for a non-working solution.Antifreeze
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);.Mcdaniels
F
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 :)
Friedly answered 6/11, 2015 at 15:59 Comment(7)
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: 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).Noneffective
Thanks for the clues. Unfortunately it still doesn't work properly. See my edit in the question.Antifreeze
@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?Friedly
@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.Friedly
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.Antifreeze
@LonelyPixel Actually, you can use an expression tree to build a method body - that's what the CompileToMethod method is for :)Friedly
@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.Friedly
R
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);
}
Ramey answered 6/11, 2015 at 22:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.