Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active September 20, 2023 11:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/d78cd49335615418411b to your computer and use it in GitHub Desktop.
Save jnm2/d78cd49335615418411b 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. (Same as WeakEventSource.cs, but uses WeakDelegate instead of ConditionalWeakTable.)
// MIT license, copyright 2015 Joseph N. Musser II
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace jnm2
{
public struct WeakDelegate<TDelegate> : IEquatable<WeakDelegate<TDelegate>> where TDelegate : class
{
private readonly WeakReference<object> targetRef;
private readonly MethodInfo method;
public WeakDelegate(TDelegate @delegate)
{
var target = ((Delegate)(object)@delegate).Target;
targetRef = target == null ? null : new WeakReference<object>(target);
method = ((Delegate)(object)@delegate).Method;
}
public TDelegate GetIfAlive()
{
var target = (object)null;
return targetRef == null || targetRef.TryGetTarget(out target) ? (TDelegate)(object)method.CreateDelegate(typeof(TDelegate), target) : null;
}
public override bool Equals(object obj)
{
return obj is WeakDelegate<TDelegate> && Equals((WeakDelegate<TDelegate>)obj);
}
public override int GetHashCode()
{
unchecked
{
return ((targetRef != null ? targetRef.GetHashCode() : 0) * 397) ^ (method != null ? method.GetHashCode() : 0);
}
}
public bool Equals(WeakDelegate<TDelegate> other)
{
if (method != other.method) return false;
if (targetRef == null) return other.targetRef == null;
if (other.targetRef == null) return false;
object thisTarget;
object otherTarget;
return targetRef.TryGetTarget(out thisTarget) == other.targetRef.TryGetTarget(out otherTarget) && thisTarget == otherTarget;
}
}
/// <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;
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<WeakDelegate<TDelegate>> handlers = new List<WeakDelegate<TDelegate>>();
public void Subscribe(TDelegate addend)
{
if (addend == null) return;
lock (handlers)
{
handlers.Add(new WeakDelegate<TDelegate>(addend));
}
}
public void Unsubscribe(TDelegate subtrahend)
{
if (subtrahend == null) return;
lock (handlers)
{
for (var i = handlers.Count - 1; i >= 0; i--)
{
var del = handlers[i].GetIfAlive();
if (del == null)
{
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))
{
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--)
{
var del = handlers[i].GetIfAlive();
if (del != null)
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