Last active
August 27, 2019 07:40
-
-
Save jnm2/e9703624ef5ca7cef74f to your computer and use it in GitHub Desktop.
Replaces a multicast delegate as an event's backing store. Duplicate behavior, except it is thread safe and holds weak references.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} |
Anyway, feel free to copy/take anything you like.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
orUnsubscribe
(not uncommon), complicated bad stuff happens. I kept all list operations atomic by making a temporary list and then invoking from that.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.