Working on a SQLHelper class to automate stored procedures calls in a similar way to what is done in the XmlRpc.Net library, I have hit a very strange problem when running a method generated manually from IL code.
I've narrowed it down to a simple generated method (probably it could be simplified even more). I create a new assembly and type, containing two methods to comply with
public interface iTestDecimal { void TestOk(ref decimal value); void TestWrong(ref decimal value); }
The test methods are just loading the decimal argument into the stack, boxing it, checking if it's NULL, and if it is not, unboxing it.
The generation of TestOk() method is as follows:
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); }
The building for TestWrong() is nearly identical:
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); }
The only difference is I'm using BrFalse instead of BrTrue to check if the value in the stack is null.
Now, running the following code:
iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create(); decimal dectest = 1; testiface.TestOk(ref dectest); Console.WriteLine(" Dectest: " + dectest.ToString());
The SimpleCodeGen.Create() is creating a new assembly and type, and calling the BuildMethodXX above to generate the code for TestOk and TestWrong. This works as expected: does nothing, value of dectest is not changed. However, running:
iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create(); decimal dectest = 1; testiface.TestWrong(ref dectest); Console.WriteLine(" Dectest: " + dectest.ToString());
the value of dectest is corrupted (sometimes it gets a big value, sometimes it says "invalid decimal value", ...) , and the program crashes.
May this be a bug in the JIT, or am I doing something wrong?
Some hints:
I'm omitting the rest of the code, creating the assembly and type. If you want the full code, just ask me.
Thanks very much!
Edit: I'm including the rest of the assembly and type creation code, for completion:
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 */ } }
In computer technology, a bug is a coding error in a computer program. (We consider a program to also include the microcode that is manufactured into a microprocessor.) The process of finding bugs -- before users do -- is called debugging.
When developing programs there are three types of error that can occur: syntax errors. logic errors. runtime errors.
Syntax errors: Errors that occur when you violate the rules of writing C/C++ syntax are known as syntax errors. This compiler error indicates something that must be fixed before the code can be compiled. All these errors are detected by compiler and thus are known as compile-time errors.
Look at this part of your code:
ilgen.Emit(OpCodes.Dup); ilgen.Emit(OpCodes.Brfalse, valIsNull); ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal)); ilgen.MarkLabel(valIsNull);
After the first line, the top of the stack will contain two object references. You then conditionally branch, removing one of the references. The next line unboxes the reference to a decimal
value. So where you mark your label, the top of the stack is either an object reference (if the branch was taken) or a decimal value (if it wasn't). These stack states are not compatible.
EDIT
As you point out in your comment, your IL code following this would work if the stack state has a decimal on top or if it has an object reference on top, since it just pops the value off of the stack either way. However, what you're trying to do still won't work (by design): there needs to be a single stack state at each instruction. See section 1.8.1.3 (Merging stack states) of the ECMA CLI spec for more details.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With