During some experiments with IL, I attempted to change callvirt
calls in an assembly to call
methods. Basically what happens is that I have an inheritance chain with member functions that I am calling.
Basically the call is similar to this:
((MyDerivedClass)myBaseObject).SomeCall();
or in IL
castclass MyDerivedClass // ** 1
call SomeCall() // ** 2
The base class defines SomeCall
as abstract method, the derived class implements it. The derived class is sealed.
I know that callvirt
is basically the equivalent of check if the object is null, if it's not call the method using the vtable and if it is, throw an exception. In my case I know that it's never null
and I know that's the implementation I want to call. I understand why you would normally need a callvirt
in such a case.
That said, because I know that the object is never null, and always is an instance of the derived type, I would think it's not a problem:
- When you consider that data and types are separated, I'd actually figure that (**1) could be removed (the data of the object will be the same) and
- That (**2) can be a simple
call
, since we know exactly what member to call. No vtable lookup is necessary.
It also seemed to me like a quite reasonable thing the compiler can also deduce in some cases. For those interested, yes, there is a speed penalty for callvirt
, although it's pretty small.
However. PEVerify tells me it's wrong. And as a good boy, I always take note of what PEVerify is telling me. So what am I missing here? Why does changing this call lead to an incorrect assembly?
Apparently creating a minimum test case isn't so simple... so far I don't have a lot of luck with it.
As for the issue itself, I can simply reproduce it in a larger program:
[IL]: Error: [C:\tmp\emit\test.dll : NubiloSoft.Test::Value][offset 0x00000007] The 'this' parameter to the call must be the calling method's 'this' parameter.
IL code of Value:
L_0000: ldarg.0
L_0001: ldfld class NubiloSoft.Test SomeField
L_0006: ldarg.1
L_0007: call instance bool NubiloSoft.Test::Contains(int32)
The type of the field is NubiloSoft.Test
.
As for Contains
, it's abstract in a base class, and in the derived class it's overridden. Just as you would expect. When I remove the 'abstract base method' + 'override', PEVerify likes it all again.
In an attempt to reproduce the issue I did this, so far without luck to reproduce it in a minimal test case:
public abstract class FooBase
{
public abstract void MyMethod();
}
// sealed doesn't seem to do anything...
public class FooDerived : FooBase
{
public override void MyMethod()
{
Console.WriteLine("Hello world!");
}
}
public class FooGenerator
{
static void Main(string[] args)
{
Type t = CreateClass();
object o = Activator.CreateInstance(t, new[] { new FooDerived() });
var meth = t.GetMethod("Caller");
meth.Invoke(o, new object[0]);
Console.ReadLine();
}
public static Type CreateClass()
{
// Create assembly
var assemblyName = new AssemblyName("testemit");
var assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.RunAndSave, @"c:\tmp");
// Create module
var moduleBuilder = assemblyBuilder.DefineDynamicModule("testemit", "test_emit.dll", false);
// Create type : IFoo
var typeBuilder = moduleBuilder.DefineType("TestClass", TypeAttributes.Public, typeof(object));
// Apparently we need a field to trigger the issue???
var field = typeBuilder.DefineField("MyObject", typeof(FooDerived), FieldAttributes.Public);
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.HideBySig |
MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
CallingConventions.HasThis, new Type[] { typeof(FooDerived) });
// Generate the constructor IL.
ILGenerator gen = constructorBuilder.GetILGenerator();
// The constructor calls the constructor of Object
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
// Store the field
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Stfld, field);
// Return
gen.Emit(OpCodes.Ret);
// Add the 'Second' method
var mb = typeBuilder.DefineMethod("Caller",
MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual |
MethodAttributes.Final | MethodAttributes.Public, CallingConventions.HasThis,
typeof(void), Type.EmptyTypes);
// Implement
gen = mb.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, field);
gen.Emit(OpCodes.Call, typeof(FooDerived).GetMethod("MyMethod"));
gen.Emit(OpCodes.Ret);
Type result = typeBuilder.CreateType();
assemblyBuilder.Save("testemit.dll");
return result;
}
}
When you run it and call peverify, it'll tell you the code doesn't have bugs... :-S
I'm not sure what's going on here... seems to me like it's pretty similar.
I suspect this blog post is relevant. In particular:
And then:
In other words, assuming this change is what you're talking about (it sounds like it) the rule is there to prevent IL from violating normal expectations of how virtual methods are called.
You might want to try making the
SomeCall
methodsealed
inMyDerivedClass
... at that point it's not virtual any more in the sense that a call toSomeCall
on a reference of typeMyDerivedClass
will always call the same method... whether that's sufficiently non-virtual for peverify is a different matter though :)