-
-
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; | |
} | |
} | |
} |
And here is a version that introduces WeakDelegate<TDelegate>
and does not need ConditionalWeakTable
.
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 of WeakReference
in addition to the CWT.
I want to be sure I understand the way MulticastDelegate.Remove works for multicast delegates.
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 new MulticastDelegate
without the removed delegate. (similarly to String.Replace
)
How do you like the Raise pseudomethod?
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.
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.
That is an excellent point. I was mistaken- delegates are removed by value, not reference. So my code oversimplifies. I updated my code to resolve this issue using a
ConditionalWeakTable
to guarantee that as long as the target is alive, the delegate will stay alive. If the target does not stay alive, the delegate gets collected.I want to be sure I understand the way
MulticastDelegate.Remove
works for multicast delegates.How do you like the Raise pseudomethod?