Работа над классом SQLHelper для автоматизации вызовов хранимых процедур аналогично тому, как это делается в библиотеке XmlRpc.Net, я ударил очень странная проблема при запуске метода, созданного вручную из IL-кода.
Я сузил его до простого сгенерированного метода (возможно, его можно было упростить еще больше). Я создаю новую сборку и тип, содержащий два метода для соответствия
public interface iTestDecimal
{
void TestOk(ref decimal value);
void TestWrong(ref decimal value);
}
Методы испытаний просто загружают десятичный аргумент в стек, боксируют его, проверяют, является ли он NULL, а если нет, то распаковываем его.
Генерация метода TestOk() заключается в следующем:
static void BuildMethodOk(TypeBuilder tb)
{
/* Create a method builder */
MethodBuilder mthdBldr = tb.DefineMethod( "TestOk", MethodAttributes.Public | MethodAttributes.Virtual,
typeof(void), new Type[] {typeof(decimal).MakeByRefType() });
ParameterBuilder paramBldr = mthdBldr.DefineParameter(1, ParameterAttributes.In | ParameterAttributes.Out, "value");
// generate IL
ILGenerator ilgen = mthdBldr.GetILGenerator();
/* Load argument to stack, and box the decimal value */
ilgen.Emit(OpCodes.Ldarg, 1);
ilgen.Emit(OpCodes.Dup);
ilgen.Emit(OpCodes.Ldobj, typeof(decimal));
ilgen.Emit(OpCodes.Box, typeof(decimal));
/* Some things were done in here, invoking other method, etc */
/* At the top of the stack we should have a boxed T or null */
/* Copy reference values out */
/* Skip unboxing if value in the stack is null */
Label valIsNotNull = ilgen.DefineLabel();
ilgen.Emit(OpCodes.Dup);
/* This block works */
ilgen.Emit(OpCodes.Brtrue, valIsNotNull);
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Ret);
/* End block */
ilgen.MarkLabel(valIsNotNull);
ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));
/* Just clean the stack */
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Ret);
}
Здание для TestWrong() почти идентично:
static void BuildMethodWrong(TypeBuilder tb)
{
/* Create a method builder */
MethodBuilder mthdBldr = tb.DefineMethod("TestWrong", MethodAttributes.Public | MethodAttributes.Virtual,
typeof(void), new Type[] { typeof(decimal).MakeByRefType() });
ParameterBuilder paramBldr = mthdBldr.DefineParameter(1, ParameterAttributes.In | ParameterAttributes.Out, "value");
// generate IL
ILGenerator ilgen = mthdBldr.GetILGenerator();
/* Load argument to stack, and box the decimal value */
ilgen.Emit(OpCodes.Ldarg, 1);
ilgen.Emit(OpCodes.Dup);
ilgen.Emit(OpCodes.Ldobj, typeof(decimal));
ilgen.Emit(OpCodes.Box, typeof(decimal));
/* Some things were done in here, invoking other method, etc */
/* At the top of the stack we should have a boxed decimal or null */
/* Copy reference values out */
/* Skip unboxing if value in the stack is null */
Label valIsNull = ilgen.DefineLabel();
ilgen.Emit(OpCodes.Dup);
/* This block fails */
ilgen.Emit(OpCodes.Brfalse, valIsNull);
/* End block */
ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));
ilgen.MarkLabel(valIsNull);
/* Just clean the stack */
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Pop);
ilgen.Emit(OpCodes.Ret);
}
Единственное отличие заключается в том, что я использую BrFalse вместо BrTrue, чтобы проверить, является ли значение в стеке равным нулю.
Теперь, запустив следующий код:
iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();
decimal dectest = 1;
testiface.TestOk(ref dectest);
Console.WriteLine(" Dectest: " + dectest.ToString());
SimpleCodeGen.Create() создает новую сборку и тип и вызывает BuildMethodXX выше для генерации кода для TestOk и TestWrong. Это работает так, как ожидалось: ничего не делает, значение dectest не изменяется. Однако запуск:
iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();
decimal dectest = 1;
testiface.TestWrong(ref dectest);
Console.WriteLine(" Dectest: " + dectest.ToString());
значение dectest повреждено (иногда оно получает большое значение, иногда оно говорит "недопустимое десятичное значение",...), и программа вылетает.
Может быть, это ошибка в JIT, или я делаю что-то неправильно?
Некоторые подсказки:
- В отладчике это происходит только тогда, когда отключена функция "Запретить оптимизацию JIT". Если включена опция "Подавить оптимизацию JIT", она работает. Это заставляет меня думать, что проблема должна быть в оптимизированном коде JIT.
- Выполнение того же теста на Mono 2.4.6 работает так, как ожидалось, поэтому это что-то особенное для Microsoft.NET.
- Проблема возникает при использовании типов datetime или decimal. По-видимому, он работает для int или для ссылочных типов (для ссылочных типов сгенерированный код не идентичен, но я опускаю этот случай, когда он работает).
- Я думаю, эта ссылка, сообщенная давным-давно, может быть связана.
- Я пробовал .NET framework v2.0, v3.0, v3.5 и v4, и поведение точно такое же.
Я опускаю остальную часть кода, создавая сборку и тип. Если вы хотите получить полный код, просто спросите меня.
Большое спасибо!
Изменить: я включаю остальную часть кода сборки и типа для завершения:
class SimpleCodeGen
{
public static object Create()
{
Type proxyType;
Guid guid = Guid.NewGuid();
string assemblyName = "TestType" + guid.ToString();
string moduleName = "TestType" + guid.ToString() + ".dll";
string typeName = "TestType" + guid.ToString();
/* Build the new type */
AssemblyBuilder assBldr = BuildAssembly(typeof(iTestDecimal), assemblyName, moduleName, typeName);
proxyType = assBldr.GetType(typeName);
/* Create an instance */
return Activator.CreateInstance(proxyType);
}
static AssemblyBuilder BuildAssembly(Type itf, string assemblyName, string moduleName, string typeName)
{
/* Create a new type */
AssemblyName assName = new AssemblyName();
assName.Name = assemblyName;
assName.Version = itf.Assembly.GetName().Version;
AssemblyBuilder assBldr = AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBldr = assBldr.DefineDynamicModule(assName.Name, moduleName);
TypeBuilder typeBldr = modBldr.DefineType(typeName,
TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public,
typeof(object), new Type[] { itf });
BuildConstructor(typeBldr, typeof(object));
BuildMethodOk(typeBldr);
BuildMethodWrong(typeBldr);
typeBldr.CreateType();
return assBldr;
}
private static void BuildConstructor(TypeBuilder typeBldr, Type baseType)
{
ConstructorBuilder ctorBldr = typeBldr.DefineConstructor(
MethodAttributes.Public | MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,
CallingConventions.Standard,
Type.EmptyTypes);
ILGenerator ilgen = ctorBldr.GetILGenerator();
// Call the base constructor.
ilgen.Emit(OpCodes.Ldarg_0);
ConstructorInfo ctorInfo = baseType.GetConstructor(System.Type.EmptyTypes);
ilgen.Emit(OpCodes.Call, ctorInfo);
ilgen.Emit(OpCodes.Ret);
}
static void BuildMethodOk(TypeBuilder tb)
{
/* Code included in examples above */
}
static void BuildMethodWrong(TypeBuilder tb)
{
/* Code included in examples above */
}
}