Search code examples
c#delegatesclrcilreflection.emit

How come delegate objects are allowed to call internal methods?


I can easily wrap a call to an internal method inside a delegate. The delegate can then call this method when I call Invoke(). However, the delegate is in the mscorlib assembly. Why can it call an internal method from my assembly?

Obviously, a delegate must be able to do this for C# to work properly. The question is not why it's allowed. It's how.

I assumed checking for visibility was a C# feature and that calling the method from CIL directly should just work. So I tried to just call that method from a dynamically defined type, thus using CIL directly and skipping C#. It failed miserably. Here's the code:

public static class InternalCall
{
    internal static void InternalMethod()
    {
        Debug.WriteLine("Successfully called an internal method from: " + typeof(InternalCall).Assembly.FullName);
    }

    public interface IMyAction
    {
        void MyInvoke();
    }

    private static IMyAction MakeMyAction()
    {
        var assembly = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("Outside"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("Outside", false);
        var customType = module.DefineType("MyAction",
            TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit,
            typeof(object),
            new[] { typeof(IMyAction) });
        var method = customType.DefineMethod("MyInvoke",
            MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final);
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Call, typeof(InternalCall).GetMethod("InternalMethod", BindingFlags.Static | BindingFlags.NonPublic));
        il.Emit(OpCodes.Ret);

        return (IMyAction)customType.CreateType().GetConstructor(Type.EmptyTypes).Invoke(null);
    }

    public static void RunTest()
    {
        var action = new Action(InternalMethod);
        Debug.WriteLine("Calling via action from assembly: " + action.GetType().Assembly.FullName);
        action.Invoke();

        var myAction = MakeMyAction();
        Debug.WriteLine("Calling via my type from assembly: " + myAction.GetType().Assembly.FullName);
        myAction.MyInvoke(); // MethodAccessException
    }
}

So, assuming delegates are still abiding by CIL rules (since that is what C# compiles to), what CIL mechanism are they using to call any method regardless of visibility?


Solution

  • It's insightful to see how a custom delegate looks in CIL:

    .class auto ansi sealed MyDeleg extends [mscorlib]System.MulticastDelegate
    {
    
    .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed
    {
    }
    
    .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed
    {
    }
    
    .method public hidebysig newslot virtual instance int32  EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
    {
    }
    
    .method public hidebysig newslot virtual instance int32  Invoke() runtime managed
    {
    }
    
    }
    

    That's it. The runtime attribute tells us that it's the runtime that provides the implementation for this method, not the code (cil). It's like p/invoke to the runtime. At this stage, there is no code-check, no verifier, no JIT. The execution is handled to the CLR to decide how to invoke the delegate, which then simply invokes it.

    The execution is akin to the calli instruction, but that's not how these methods are implemented, although you can use it to simulate delegates fairly well. Why are there no visibility checks? Because it's the raw method pointer (from GetFunctionPointer) that is passed to the instruction, and obtaining its metadata may cause negative performace impact (or there may be no metadata at all). Of course doing any visibility checks is simply unwanted.