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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Anyway, feel free to copy/take anything you like.