-
-
Save jnm2/e9703624ef5ca7cef74f to your computer and use it in GitHub Desktop.
// MIT license, copyright 2015 Joseph N. Musser II | |
using System; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using System.Reflection.Emit; | |
using System.Runtime.CompilerServices; | |
namespace jnm2 | |
{ | |
/// <summary> | |
/// Replaces a multicast delegate as an event's backing store. Duplicate behavior, except it is thread safe and holds weak references. | |
/// </summary> | |
public sealed class WeakEventSource<TDelegate> where TDelegate : class | |
{ | |
// ReSharper disable once StaticMemberInGenericType | |
private static readonly DynamicMethod raiseMethod; | |
private readonly ConditionalWeakTable<object, List<object>> keepAliveDelegatesWithTarget = new ConditionalWeakTable<object, List<object>>(); | |
static WeakEventSource() | |
{ | |
if (!typeof(TDelegate).IsSubclassOf(typeof(Delegate))) throw new InvalidOperationException("TDelegate must derive from System.Delegate."); | |
// Cache raise method per delegate type | |
var raiseSignature = typeof(TDelegate).GetMethod("Invoke"); | |
var parameters = raiseSignature.GetParameters(); | |
var raiseParameterTypes = new Type[parameters.Length + 1]; | |
raiseParameterTypes[0] = typeof(WeakEventSource<TDelegate>); // First parameter is raise delegate target (this) | |
for (var i = 0; i < parameters.Length; i++) | |
raiseParameterTypes[i + 1] = parameters[i].ParameterType; | |
raiseMethod = new DynamicMethod("Raise", raiseSignature.ReturnType, raiseParameterTypes, typeof(WeakEventSource<TDelegate>)); | |
var il = raiseMethod.GetILGenerator(); | |
il.DeclareLocal(typeof(List<TDelegate>)); // loc_0 | |
il.DeclareLocal(typeof(int)); // loc_1 | |
if (raiseSignature.ReturnType != typeof(void)) | |
{ | |
il.DeclareLocal(raiseSignature.ReturnType); // loc_2 | |
// var r = default(TReturn) | |
if (raiseSignature.ReturnType.IsClass) | |
{ | |
il.Emit(OpCodes.Ldnull); | |
il.Emit(OpCodes.Stloc_2); | |
} | |
else | |
{ | |
il.Emit(OpCodes.Ldloca_S, 2); | |
il.Emit(OpCodes.Initobj, raiseSignature.ReturnType); | |
} | |
} | |
// var handlerList = this.GetLiveHandlersReverseOrder(); | |
il.Emit(OpCodes.Ldarg_0); | |
il.Emit(OpCodes.Call, typeof(WeakEventSource<TDelegate>).GetMethod("GetLiveHandlersReverseOrder", BindingFlags.Instance | BindingFlags.NonPublic)); | |
il.Emit(OpCodes.Stloc_0); | |
// var i = handlerList.Count - 1; | |
il.Emit(OpCodes.Ldloc_0); | |
il.Emit(OpCodes.Callvirt, typeof(List<TDelegate>).GetMethod("get_Count")); | |
il.Emit(OpCodes.Ldc_I4_1); | |
il.Emit(OpCodes.Sub); | |
il.Emit(OpCodes.Stloc_1); | |
var loopConditionLabel = il.DefineLabel(); | |
var loopBodyLabel = il.DefineLabel(); | |
il.Emit(OpCodes.Br_S, loopConditionLabel); | |
il.MarkLabel(loopBodyLabel); | |
// handlerList[i] | |
il.Emit(OpCodes.Ldloc_0); | |
il.Emit(OpCodes.Ldloc_1); | |
il.Emit(OpCodes.Callvirt, typeof(List<TDelegate>).GetMethod("get_Item")); | |
// .Invoke(p1, p2, p3, ...) | |
for (var parameterIndex = 1; parameterIndex < raiseParameterTypes.Length; parameterIndex++) | |
il.Emit(raiseParameterTypes[parameterIndex].IsByRef ? OpCodes.Ldarga_S : OpCodes.Ldarg_S, parameterIndex); | |
il.Emit(OpCodes.Callvirt, typeof(TDelegate).GetMethod("Invoke")); | |
if (raiseSignature.ReturnType != typeof(void)) il.Emit(OpCodes.Stloc_2); | |
// i--; | |
il.Emit(OpCodes.Ldloc_1); | |
il.Emit(OpCodes.Ldc_I4_1); | |
il.Emit(OpCodes.Sub); | |
il.Emit(OpCodes.Stloc_1); | |
il.MarkLabel(loopConditionLabel); | |
// i >= 0; | |
il.Emit(OpCodes.Ldloc_1); | |
il.Emit(OpCodes.Ldc_I4_0); | |
il.Emit(OpCodes.Bge_S, loopBodyLabel); | |
if (raiseSignature.ReturnType != typeof(void)) il.Emit(OpCodes.Ldloc_2); | |
il.Emit(OpCodes.Ret); | |
} | |
public WeakEventSource() | |
{ | |
this.Raise = (TDelegate)(object)raiseMethod.CreateDelegate(typeof(TDelegate), this); | |
} | |
public TDelegate Raise { get; private set; } | |
private readonly List<WeakReference<TDelegate>> handlers = new List<WeakReference<TDelegate>>(); | |
public void Subscribe(TDelegate addend) | |
{ | |
if (addend == null) return; | |
lock (handlers) | |
{ | |
handlers.Add(new WeakReference<TDelegate>(addend)); | |
// Ensure that the addend stays alive as long as the target | |
keepAliveDelegatesWithTarget.GetOrCreateValue(((Delegate)(object)addend).Target).Add(addend); | |
} | |
} | |
public void Unsubscribe(TDelegate subtrahend) | |
{ | |
if (subtrahend == null) return; | |
lock (handlers) | |
{ | |
for (var i = handlers.Count - 1; i >= 0; i--) | |
{ | |
TDelegate del; | |
if (!handlers[i].TryGetTarget(out del)) | |
{ | |
handlers.RemoveAt(i); // May as well clean up the list while we search, since we have to call TryGetTarget anyway | |
continue; | |
} | |
// Once we've removed the subtrahend exactly once, return. | |
if (del.Equals(subtrahend)) | |
{ | |
List<object> delegates; | |
if (keepAliveDelegatesWithTarget.TryGetValue(((Delegate)(object)subtrahend).Target, out delegates)) delegates.Remove(subtrahend); | |
handlers.RemoveAt(i); | |
return; | |
} | |
} | |
} | |
} | |
// Retrieve the current list of live handlers to be invoked. | |
// (NB! Never invoke a delegate while holding a lock; it raises the chance of contention and often causes reentrancy errors in hard-to-predict places of the host program.) | |
// ReSharper disable once UnusedMember.Local | |
private List<TDelegate> GetLiveHandlersReverseOrder() | |
{ | |
var r = new List<TDelegate>(handlers.Count); | |
lock (handlers) | |
{ | |
for (var i = handlers.Count - 1; i >= 0; i--) | |
{ | |
TDelegate del; | |
if (handlers[i].TryGetTarget(out del)) | |
r.Add(del); | |
else | |
handlers.RemoveAt(i); | |
} | |
} | |
return r; | |
} | |
} | |
} |
You can't enumerate a ConditionalWeakTable
, so I couldn't think of anything more efficient than a separate list of weak references. What do you think of the WeakDelegate version?
Also did I explain well enough what can happen if you invoke a delegate within a lock? Since locks are reentrant by the same thread, you lose thread safety features. If one of the handlers happens to cause a call to Subscribe
or Unsubscribe
(not uncommon), complicated bad stuff happens. I kept all list operations atomic by making a temporary list and then invoking from that.
Not quite; a delegate is immutable...
Understood. It still would be good to mimic the add/remove behavior. It looks like multicast delegate math is not commutative; am I right that A + B - (A + B) = (A + B)
? If I'm right, this means we don't have to do anything special since our subscribe/unsubscribe works that way as well.
Anyway, feel free to copy/take anything you like.
Hi Joseph,
Using a
ConditionalWeakTable
is a great idea! I think I would use it a bit differently, though; I don't think it's really necessary to have a list ofWeakReference
in addition to the CWT.Not quite; a delegate is immutable, and the
Remove
method doesn't actually affect the delegate instance on which it's called. It creates a newMulticastDelegate
without the removed delegate. (similarly toString.Replace
)It's a good approach; actually I think it's the only way to expose a
Raise
"method" with the correct signature, since the signature isn't known in advance.