Want to show your appreciation? Please to my charity.

Sunday, July 12, 2009

Intercept Non-Virtual Interface Implementation, Implicit or Explicit

Back in April, I blogged about Rhino Mocks' (Castle DP intrinsic) inability to mock non-virtual interface implementation and posted the question in the Rhino Mocks' support group. I didn't receive much feedback until recently, Krzysztof Koźmic killed a good amount of bugs in the DP and so I tried again. What a coincident that Krzysztof also discussed the same issue in his blog 2 days before I sent him the request. :)

Today, the problem is fixed. But Krzysztof also mentioned two limitations. While the 2nd limitation is well understood, I believe there should be a way to overcome the 1st limitation: calling the explicit implementation method in base class.

The problem reminds me of another challenge that I talked about in my post named, C#.Net Calling Grandparent's Virtual Method (base.base in C#). The lesson learned there was that as long as you can get hold of a MethodInfo object, you should be able to emit the call.

I was able to do similar to successfully call the base class explicit implementation method. Code below is to illustration the theory.

    public interface IFoo { void FooMethod(); }

    public class ThirdParty : IFoo
    {
        void IFoo.FooMethod()
        {
            Console.WriteLine("ThirdParty.FooMethod");
        }
    }

    public class Proxy : ThirdParty, IFoo
    {
        private static readonly Action<ThirdParty> baseFooMethod;

        static Proxy()
        {
            var mi = typeof(ThirdParty).GetMethod(
                typeof(IFoo).FullName.Replace('+', '.') + ".FooMethod", 
                BindingFlags.NonPublic | BindingFlags.Instance);
            baseFooMethod = (Action<ThirdParty>) 
                Delegate.CreateDelegate(typeof(Action<ThirdParty>), mi);
        }

        void IFoo.FooMethod()
        {
            Console.WriteLine("Proxy.FooMethod");
            baseFooMethod(this);
        }

        public static void Main(string[] args)
        {
            IFoo foo = new Proxy();
            foo.FooMethod();
        }
    }

I'm not familiar with Emit API enough to write it out so I'll have to leave the rest to DP expert, Krzysztof Koźmic :)

Update 7/17/2009: I was able to implement this, using delegate, with Reflection.Emit. The result was quite good. It is relatively simple, and has minimal runtime CPU performance overhead. See: Intercept Explicit Interface Implementation with Inheritance Based Proxy

4 comments:

Krzysztof Koźmic (2) said...

This is possible, because delegates are bound to methods differently. You can call private methods using delegates, whereas you can't call base class' private methods from inherited class.

The issue with interface implementation is that class methods to interface declarations are bound via methodImpl table, and this is overriden in the inheriting type, so there's really nothing we can do.

Hacking it via the delegate could solve it, but it's got it's drawbacks as well.

You need to create delegate each time you create an instance which is an expensive operation. You also need appropriate delegate type to bind to.

What if the method had this signature:

DateTime Foo(out int, ref string, DateTime) ?

There's no delegate type that you could bind that to, so you'd have to generate delegate types as well.

That's just not worth it.

Kenneth Xu said...

Krzysztof, Thanks for the comment and I totally agree that's not worth it if we had to create a delegate type for each call. But The delegate was just to illustrate the ideal. I would expect a il.EmitCall to replace it. Something like below.

var mi = typeof(ThirdParty).GetMethod(
typeof(IFoo).FullName.Replace('+', '.') + ".FooMethod",
BindingFlags.NonPublic | BindingFlags.Instance);

il.EmitCall(OpCodes.Call, mi, null);

But as I said I'm not familiar with Emit API in .Net so correct me if I'm wrong.

Krzysztof Koźmic said...

What you're suggesting is what I have tried and what wouldn't work due to reasons I outlined in my previous comment. You can do it only using delegate and this is not a viable option.

Kenneth Xu said...

Gotcha, thanks for sharing! I appreciate it!

Post a Comment