Выпустить код IL для загрузки десятичного значения

У меня есть подобный код для генерации кода IL, который загружает целые или строковые значения. Но я не знаю, как добавить к этому тип decimal. Он не поддерживается в методе Emit. Какие-нибудь решения для этого?

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));
}

Не работает:

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

Изменить: Хорошо, вот что я сделал:

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);
}

Но он не генерирует код операции newobj, а вместо этого создает nop и stloc.0. Конструктор найден и передан вызову Emit. Что здесь не так? Очевидно, что при попытке выполнить сгенерированный код выдается InvalidProgramException, потому что стек полностью испорчен.


person ygoe    schedule 06.11.2015    source источник
comment
По-видимому (но не верьте мне на слово) для десятичной загрузки нет прямого кода операции, вы загружаете аргументы и вызываете десятичный конструктор: см. stackoverflow.com/a/485834/266143   -  person CodeCaster    schedule 06.11.2015
comment
См. Также codeblog.jonskeet.uk/2014/08/22/. Вкратце: десятичные дроби не являются примитивными типами CLR, и нет кода операции IL для прямой загрузки.   -  person Jeroen Mostert    schedule 06.11.2015
comment
Смотрите мою правку выше для неработающего решения.   -  person ygoe    schedule 06.11.2015
comment
Я считаю, что это неправильная строка: ilGen.Emit(OpCodes.Ldc_I4, scale). Вы говорите, что загружаете I4 (int), но затем используете byte перегрузку Emit(). Один из способов исправить это - ilGen.Emit(OpCodes.Ldc_I4, (int)scale);.   -  person svick    schedule 07.11.2015


Ответы (2)


Давай, просто декомпилируйте какой-нибудь код C #, который делает то же самое - вы увидите, что десятичного примитива нет.

42M

компилируется в

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

Для десятичного числа это намного сложнее:

42.3M

дает

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

Самый простой способ получить это для произвольного десятичного числа - использовать перегрузку int[] конструктора и статический метод GetBits. Вы также можете перепроектировать метод SetBits, чтобы позволить вам вызывать более простой конструктор с правильными значениями, или использовать отражение для чтения внутреннего состояния - есть множество вариантов.

РЕДАКТИРОВАТЬ:

Вы близки, но вы нарушили ILGen - в то время как последний аргумент конструктора - это byte, константа, которую вы загружаете, должна быть int. Следующее работает, как ожидалось:

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);

ИЗМЕНИТЬ 2:

Простой пример того, как вы можете использовать деревья выражений (в данном случае дерево строится компилятором C #, но это зависит от вас) для определения тел динамических методов:

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 :)
person Luaan    schedule 06.11.2015
comment
Очевидно, это связано с тем, что Библиотека расширенных числовых значений не является частью спецификации CIL, поскольку некоторые общедоступные процессоры не обеспечивают прямой поддержки типов данных (источник: ecma-international.org/publications/files/ECMA-ST/ECMA -335.pdf, большой PDF). Вот почему нет кода операции для загрузки decimal (или single). - person CodeCaster; 06.11.2015
comment
Спасибо за подсказки. К сожалению, это все еще не работает должным образом. См. Мою правку в вопросе. - person ygoe; 06.11.2015
comment
@LonelyPixel Обновлен с использованием правильного кода - ldc.i4 должен передаваться int. Жаль, что ILGen позволит вам это сделать, но вы просто должны быть осторожны :) Однако в наши дни ILGen не так уж и нужен - почему бы не использовать Expression.Compile? - person Luaan; 07.11.2015
comment
@LonelyPixel Или, чтобы прояснить путаницу, конечно, ILGen не улавливает эту ошибку - он вообще не выполняет никакой проверки. Он просто выдает байты - вот и все. Затем ваш декомпилятор сообщает вам, что newobj не был сгенерирован, но на самом деле это не так - проблема в том, что ldc.i4 перед этим newobj был на три байта короче, чем должен был быть. Надеюсь, это как-то проясняет путаницу :) ILGen всегда будет чрезвычайно хрупким - он мало что делает, кроме преобразования кодов операций и констант в байты IL. - person Luaan; 07.11.2015
comment
Спасибо, изменив его на int, все заработало. Вся система использует динамическую сборку и TypeBuilder, поэтому ее нужно расширить таким образом. Но теперь все в порядке. - person ygoe; 11.11.2015
comment
@LonelyPixel На самом деле, вы можете использовать дерево выражений для построения тела метода - для этого и нужен метод CompileToMethod :) - person Luaan; 11.11.2015
comment
@LonelyPixel Я добавил пример того, как это сделать. Есть некоторые ограничения, но если вы можете работать с ними, вы избавите себя от многих проблем в долгосрочной перспективе. - person Luaan; 11.11.2015

Как уже упоминал Луаан, вы можете использовать метод decimal.GetBits и конструктор int[]. Взгляните на этот пример:

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);
}
person thehennyy    schedule 06.11.2015