Pure silver

May 3, 2010

Generic weak event handlers

Filed under: Silverlight and .NET — Stefan Dragnev @ 1:29 am

If you’ve grown to need really generic weak event handlers and weak delegates, then I assume you have at least tried using existing non-entirely-generic ones.
Dustin Campbell has a great article detailing the implementation of a weak event handler that can be used for any EventHandler<> parameterization, which is great for most practical purposes.
Unfortunately, there seems to be no way to create a single generic class that can act as a weak proxy for all possible delegate types. It appears to be an inherent .NET limitation –generic type parameters that are in fact delegates cannot be used for invocation or anything delegate-related. It would have been possible if you could spell it as a type constraint, e.g. class Foo<T> where T : delegate {};. Oh well, no biggie – let’s just write a separate class for each possible delegate type. However, let’s be smart about it!
By now you should’ve guessed that we’ll just use code generation – we’ll just generate an exact replica of the code in Campbell’s article, with the only difference being the delegate’s type. I’m giving you the entire code below.

    /// <summary>
    /// Weak delegates are useful where you don't want the lifetime of a delegate target
    /// to be bound to the delegate's lifetime.
    ///
    /// The WeakDelegateFactory is used for the the same purpose as WeakEventHandler, but
    /// can work for any delegate type. Is uses Reflection.Emit to create an exact replica
    /// of the WeakEventHandler class, but for the specific delegate type.
    /// </summary>
    public static class WeakDelegateFactory
    {
        private static AssemblyBuilder theDelegatesAsm;
        private static ModuleBuilder theModuleBuilder;

        public static TDelegate Create<TDelegate>(TDelegate targetDelegate, Action<TDelegate> unregisterDelegate) where TDelegate : class
        {
            EnsureBuildersCreated();

            var type = targetDelegate.GetType();

            if (type.GetMethod("Invoke").ReturnType != typeof(void))
                throw new ArgumentException("Weak delegates can only be created for delegates with void return type.", "targetDelegate");

            // create the class builder that will give birth to this weak delegate class
            var className = "WeakDelg->" + GetPrettyName(type);
            var classType = theModuleBuilder.GetType(className)
                            ?? CreateWeakDelegateClass(targetDelegate, type, className);

            var ctor = classType.GetConstructors()[0];
            var weakDelg = ctor.Invoke(new object[] { targetDelegate, unregisterDelegate });
            return (TDelegate) (object) Delegate.CreateDelegate(type, weakDelg, classType.GetMethod("Invoke"));
        }

        private static Type CreateWeakDelegateClass<TDelegate>(TDelegate targetDelegate, Type type, string className) where TDelegate : class
        {
            var classBuilder = theModuleBuilder.DefineType(className, TypeAttributes.Class | TypeAttributes.Public);

            // create the class fields for the unbound delegate, the weak reference to the delegate's target and an unregister callback
            var weakRefFld = classBuilder.DefineField("myWeakRef", typeof(WeakReference), FieldAttributes.Private);

            MethodBuilder unboundDelgInvoke;
            var unboundDelgType = CreateUnboundDelegateType(classBuilder, targetDelegate, out unboundDelgInvoke);
            var unboundDelgFld = classBuilder.DefineField("myUnboundDelg", unboundDelgType, FieldAttributes.Private);

            var unregisterDelgFld = classBuilder.DefineField("myUnregisterDelg", typeof(Action<TDelegate>), FieldAttributes.Private);

            // create constructor
            DefineConstructor<TDelegate>(type, classBuilder, unboundDelgType, weakRefFld, unboundDelgFld, unregisterDelgFld);

            // create the method that implements the weak delegate calling
            DefineInvokeMethod(targetDelegate, classBuilder, weakRefFld, unboundDelgFld, unregisterDelgFld, unboundDelgInvoke);

            unboundDelgType.CreateType();
            return classBuilder.CreateType();
        }

        private static void DefineConstructor<TDelegate>(Type delegateType, TypeBuilder classBuilder, TypeBuilder unboundDelgType, FieldBuilder weakRefFld, FieldBuilder unboundDelgFld, FieldBuilder unregisterDelgFld)
        {
            var target = delegateType.GetProperty("Target");
            // create the constructor; it initializes the unbound delegate and the weak reference
            var constructor = classBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis,
                                                             new[] { delegateType, typeof(Action<TDelegate>) });
            var ctorIl = constructor.GetILGenerator();

            // call object.ctor()
            ctorIl.Emit(OpCodes.Ldarg_0);
            ctorIl.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[] { }));

            // store a WeakReference to the delegate's target into a field
            ctorIl.Emit(OpCodes.Ldarg_0);
            ctorIl.Emit(OpCodes.Ldarg_1);
            ctorIl.Emit(OpCodes.Callvirt, target.GetGetMethod());
            ctorIl.Emit(OpCodes.Newobj, typeof(WeakReference).GetConstructor(new[] { typeof(object) }));
            ctorIl.Emit(OpCodes.Stfld, weakRefFld);

            // create an unbound delegate type from the given delegate's method
            ctorIl.Emit(OpCodes.Ldarg_0); // for stfld
            ctorIl.Emit(OpCodes.Ldtoken, unboundDelgType);
            ctorIl.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) })); // first parameter; load the unbound delegate's type
            ctorIl.Emit(OpCodes.Ldnull); // second parameter; no instance given, i.e. treat it as a static method
            ctorIl.Emit(OpCodes.Ldarg_1); // third parameter to CreateDelegate
            ctorIl.Emit(OpCodes.Callvirt, typeof(Delegate).GetProperty("Method").GetGetMethod());
            ctorIl.Emit(OpCodes.Call, typeof(Delegate).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object), typeof(MethodInfo) }));
            ctorIl.Emit(OpCodes.Castclass, unboundDelgType);
            ctorIl.Emit(OpCodes.Stfld, unboundDelgFld);

            // store the unregister callback
            ctorIl.Emit(OpCodes.Ldarg_0);
            ctorIl.Emit(OpCodes.Ldarg_2);
            ctorIl.Emit(OpCodes.Stfld, unregisterDelgFld);

            ctorIl.Emit(OpCodes.Ret);
        }

        private static void DefineInvokeMethod<TDelegate>(TDelegate targetDelegate, TypeBuilder classBuilder, FieldBuilder weakRefFld, FieldBuilder unboundDelgFld, FieldBuilder unregisterDelgFld, MethodBuilder unboundDelgInvoke) where TDelegate : class
        {
            var targetType = ((Delegate) (object) targetDelegate).Target.GetType();
            var unregisterDelgType = typeof(Action<TDelegate>);
            var delegateType = targetDelegate.GetType();
            var delegateSignature = delegateType.GetMethod("Invoke");
            var delgParamTypes = GetParameterTypes(delegateSignature);
            var invoker = classBuilder.DefineMethod("Invoke", MethodAttributes.Public, delegateSignature.ReturnType, delgParamTypes.ToArray());
            var invokerIl = invoker.GetILGenerator();

            var targetLocal = invokerIl.DeclareLocal(targetType);

            var endLabel = invokerIl.DefineLabel();
            var targetIsNullLabel = invokerIl.DefineLabel();

            // get Target from weak reference
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldfld, weakRefFld);
            invokerIl.Emit(OpCodes.Callvirt, typeof(WeakReference).GetProperty("Target").GetGetMethod());
            invokerIl.Emit(OpCodes.Isinst, targetType);
            invokerIl.Emit(OpCodes.Castclass, targetType);
            invokerIl.Emit(OpCodes.Stloc, targetLocal);
            invokerIl.Emit(OpCodes.Ldloc, targetLocal);
            invokerIl.Emit(OpCodes.Brfalse_S, targetIsNullLabel);

            // the target is not null - call the unbound delegate
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldfld, unboundDelgFld);
            invokerIl.Emit(OpCodes.Ldloc, targetLocal);
            invokerIl.Emit(OpCodes.Ldarg_1);
            invokerIl.Emit(OpCodes.Ldarg_2);
            invokerIl.Emit(OpCodes.Callvirt, unboundDelgInvoke);
            invokerIl.Emit(OpCodes.Ret);

            invokerIl.MarkLabel(targetIsNullLabel);
            // target was null, call unregister
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldfld, unregisterDelgFld);
            invokerIl.Emit(OpCodes.Brfalse_S, endLabel); // check if unregister is null
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldfld, unregisterDelgFld);
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldftn, invoker);
            invokerIl.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); // create delegate from invoke
            invokerIl.Emit(OpCodes.Callvirt, unregisterDelgType.GetMethod("Invoke")); // call unregister
            invokerIl.Emit(OpCodes.Ldarg_0);
            invokerIl.Emit(OpCodes.Ldnull);
            invokerIl.Emit(OpCodes.Stfld, unregisterDelgFld); // nullify unregister
            invokerIl.MarkLabel(endLabel);

            invokerIl.Emit(OpCodes.Ret);
        }

        private static void EnsureBuildersCreated()
        {
            if (theDelegatesAsm == null)
            {
                theDelegatesAsm = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("WeakDelegates"), AssemblyBuilderAccess.Run);
                theModuleBuilder = theDelegatesAsm.DefineDynamicModule("WeakDelegates");
            }
        }

        /// <summary>
        /// Creates an unbound delegate type from the type of a normal delegate
        /// </summary>
        private static TypeBuilder CreateUnboundDelegateType(TypeBuilder parentClass, object boundDelegate, out MethodBuilder outInvokeMethod)
        {
            var invokeMethod = boundDelegate.GetType().GetMethod("Invoke");
            var targetType = ((Delegate) boundDelegate).Target.GetType();
            return CreateDelegateType(parentClass, invokeMethod.ReturnType, new[] { targetType }.Concat(GetParameterTypes(invokeMethod)).ToArray(), out outInvokeMethod);
        }

        private static IEnumerable<Type> GetParameterTypes(MethodInfo mi)
        {
            return from p in mi.GetParameters() select p.ParameterType;
        }

        /// <summary>
        /// Basically does what the compiler does when it has to compile a delegate statement.
        /// Creates the delegate as a nested type.
        /// </summary>
        /// <returns>A class derived from System.MulticastDelegate that can be called with the given parameters</returns>
        private static TypeBuilder CreateDelegateType(TypeBuilder parentClass, Type returnType, Type[] parameters, out MethodBuilder outInvokeMethod)
        {
            // from Joel Pobar's CLR weblog
            // Creating delegate types via Reflection.Emit
            // http://blogs.msdn.com/joelpob/archive/2004/02/15/73239.aspx

            var delgTypeBuilder = parentClass.DefineNestedType("UnboundDelegate", TypeAttributes.Class | TypeAttributes.NestedPublic | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, typeof(MulticastDelegate));
            var constructorBuilder = delgTypeBuilder.DefineConstructor(MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(object), typeof(IntPtr) });
            constructorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);

            outInvokeMethod = delgTypeBuilder.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, returnType, parameters);
            outInvokeMethod.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);

            return delgTypeBuilder;
        }

        private static string GetPrettyName(Type t)
        {
            if (t.IsGenericType)
            {
                return t.Namespace + '.' + t.Name + '(' + String.Join(";", (from p in t.GetGenericArguments() select p.Name).ToArray()) + ')';
            }
            else
            {
                return t.FullName;
            }
        }
    }

Note that we use Reflection.Emit to do the onerous code generation – we could’ve used CSharpCodeProvider to the same effect, so why didn’t we? Well, CSharpCodeProvider isn’t available in Silverlight, so there. This code works in Silverlight as it does in .NET. The only inherent limitation in Silverlight (as with the code in Campbell’s article) is that the bound method must be public enough – otherwise trying to create an unbound delegate from the delegate target throws a SecurityException.

The code probably works – it’s more of a proof of concept than anything else. I’ve used it in production code, so there aren’t any train-wrecking problems in it. There is probably more stuff needed to make it work in all cases, however.

This is how you use it:

        public event Action<int> SomeEvent;

        private void Register()
        {
            SomeEvent += WeakDelegateFactory.Create(new Action<int>(Sink), e => SomeEvent -= e);
        }

        private void Sink(int foo)
        {}

It would have been great if methods were implicitly usable as delegates. You could then write (with an appropriate extension method) SomeEvent += Sink.ToWeak(e=>SomeEvent-=e);

About these ads

3 Comments »

  1. I must admit I’d never have thought of going the reflection route. Very impressive.

    I haven’t really dared to put my solution into production properly. I think I’ve just used it in one place to test the water…

    Comment by Ben — May 7, 2010 @ 6:51 am

  2. Awesome solution, it takes me a while to get it but it’s impressive.
    Great job.

    Comment by raffaeu — June 11, 2010 @ 8:55 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The Silver is the New Black Theme. Get a free blog at WordPress.com

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: