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:
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:
Some consider this a violation of privacy through inheritence. Lots of code is written under the assumption that overriding a virtual method is sufficient to guarantee custom logic within gets called. Intuitively, this makes sense, and C# lulls you into this sense of security because it always emits calls to virtual methods as callvirts.
And then:
Late in Whidbey, some folks decided this is subtly strange enough that we at least don’t want partially trusted code to be doing it. That it’s even possible is often surprising to people. We resolved the mismatch between expectations and reality through the introduction of a new verification rule.
The rule restricts the manner in which callers can make non-virtual calls to virtual methods, specifically by only permitting it if the target method is being called on the caller’s ‘this’ pointer. This effectively allows an object to call up (or down, although that would be odd) its own type hierarchy.
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
method sealed
in MyDerivedClass
... at that point it's not virtual any more in the sense that a call to SomeCall
on a reference of type MyDerivedClass
will always call the same method... whether that's sufficiently non-virtual for peverify is a different matter though :)
See more on this question at Stackoverflow