Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active August 27, 2019 07:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/e9703624ef5ca7cef74f to your computer and use it in GitHub Desktop.
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.
// 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;
}
}
}
@jnm2
Copy link
Author

jnm2 commented Sep 2, 2015

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