Skip to content

Add APIs for allocation-free delegate invocation list inspection #97683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 31, 2024
15 changes: 15 additions & 0 deletions src/coreclr/System.Private.CoreLib/src/System/MulticastDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,21 @@ public sealed override Delegate[] GetInvocationList()
return del;
}

internal new bool HasSingleTarget => !(_invocationList is object[]);

// Used by delegate invocation list enumerator
internal object? /* Delegate? */ TryGetAt(int index)
{
if (!(_invocationList is object[] invocationList))
{
return (index == 0) ? this : null;
}
else
{
return ((uint)index < (uint)_invocationCount) ? invocationList[index] : null;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(MulticastDelegate? d1, MulticastDelegate? d2)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ public sealed override Delegate[] GetInvocationList()
return del;
}

internal new bool HasSingleTarget => !(m_helperObject is Delegate[]);

// Used by delegate invocation list enumerator
internal Delegate? TryGetAt(int index)
{
if (!(m_helperObject is Delegate[] invocationList))
{
return (index == 0) ? this : null;
}
else
{
return ((uint)index < (uint)m_extraFunctionPointerOrData) ? invocationList[index] : null;
}
}

protected override MethodInfo GetMethodImpl()
{
return base.GetMethodImpl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,6 @@ public virtual void EndNew(int itemIndex)
return newItem;
}

private bool AddingNewHandled => _onAddingNew != null && _onAddingNew.GetInvocationList().Length > 0;

/// <summary>
/// Creates a new item and adds it to the list.
///
Expand Down Expand Up @@ -336,7 +334,7 @@ public bool AllowNew
}
// Even if the item doesn't have a default constructor, the user can hook AddingNew to provide an item.
// If there's a handler for this, we should allow new.
return AddingNewHandled;
return _onAddingNew != null;
}
set
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ protected void CheckReentrancy()
// only arises if reentrant changes make the original event args
// invalid for later listeners. This keeps existing code working
// (e.g. Selector.SelectedItems).
if (CollectionChanged?.GetInvocationList().Length > 1)
NotifyCollectionChangedEventHandler? handler = CollectionChanged;
if (handler != null && !handler.HasSingleTarget)
throw new InvalidOperationException(SR.ObservableCollectionReentrancyNotAllowed);
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Delegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,69 @@ public abstract partial class Delegate : ICloneable, ISerializable

public virtual Delegate[] GetInvocationList() => new Delegate[] { this };

/// <summary>
/// Gets a value that indicates whether the <see cref="Delegate"/> has a single invocation target.
/// </summary>
/// <value>true if the <see cref="Delegate"/> has a single invocation target.</value>
public bool HasSingleTarget => Unsafe.As<MulticastDelegate>(this).HasSingleTarget;

/// <summary>
/// Gets an enumerator for the invocation targets of this delegate.
/// </summary>
/// <remarks>
/// This returns a <see cref="InvocationListEnumerator{TDelegate}"/>" /> that follows the IEnumerable pattern and
/// thus can be used in a C# 'foreach' statements to retrieve the invocation targets of this delegate without allocations.
/// The order of the delegates returned by the enumerator is the same order in which the current delegate invokes the methods that those delegates represent.
/// The method returns an empty enumerator for null delegate.
/// </remarks>
public static System.Delegate.InvocationListEnumerator<TDelegate> EnumerateInvocationList<TDelegate>(TDelegate? d) where TDelegate : System.Delegate
=> new InvocationListEnumerator<TDelegate>(Unsafe.As<MulticastDelegate>(d));

/// <summary>
/// Provides an enumerator for the invocation list of a delegate.
/// </summary>
/// <typeparam name="TDelegate">Delegate type being enumerated.</typeparam>
public struct InvocationListEnumerator<TDelegate> where TDelegate : System.Delegate
{
private readonly MulticastDelegate? _delegate;
private int _index;
private TDelegate? _current;

internal InvocationListEnumerator(MulticastDelegate? d)
{
_delegate = d;
_index = -1;
}

/// <summary>
/// Implements the IEnumerator pattern.
/// </summary>
public TDelegate Current
{
get => _current!;
}

/// <summary>
/// Implements the IEnumerator pattern.
/// </summary>
public bool MoveNext()
{
int index = _index + 1;
if ((_current = Unsafe.As<TDelegate>(_delegate?.TryGetAt(index))) == null)
{
return false;
}
_index = index;
return true;
}

/// <summary>
/// Implement IEnumerable.GetEnumerator() to return 'this' as the IEnumerator
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] // Only here to make foreach work
public System.Delegate.InvocationListEnumerator<TDelegate> GetEnumerator() => this;
}

public object? DynamicInvoke(params object?[]? args)
{
return DynamicInvokeImpl(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -620,31 +620,24 @@ public void Dispose()
Justification = "The code handles the Assembly.Location equals null")]
private Assembly? GetFirstResolvedAssemblyFromResolvingEvent(AssemblyName assemblyName)
{
Assembly? resolvedAssembly = null;

Func<AssemblyLoadContext, AssemblyName, Assembly>? resolvingHandler = _resolving;

if (resolvingHandler != null)
// Loop through the event subscribers and return the first non-null Assembly instance
foreach (Func<AssemblyLoadContext, AssemblyName, Assembly> handler in Delegate.EnumerateInvocationList(_resolving))
{
// Loop through the event subscribers and return the first non-null Assembly instance
foreach (Func<AssemblyLoadContext, AssemblyName, Assembly> handler in resolvingHandler.GetInvocationList())
{
resolvedAssembly = handler(this, assemblyName);
Assembly? resolvedAssembly = handler(this, assemblyName);
#if CORECLR
if (IsTracingEnabled())
{
TraceResolvingHandlerInvoked(
assemblyName.FullName,
handler.Method.Name,
this != Default ? ToString() : Name,
resolvedAssembly?.FullName,
resolvedAssembly != null && !resolvedAssembly.IsDynamic ? resolvedAssembly.Location : null);
}
if (IsTracingEnabled())
{
TraceResolvingHandlerInvoked(
assemblyName.FullName,
handler.Method.Name,
this != Default ? ToString() : Name,
resolvedAssembly?.FullName,
resolvedAssembly != null && !resolvedAssembly.IsDynamic ? resolvedAssembly.Location : null);
}
#endif // CORECLR
if (resolvedAssembly != null)
{
return resolvedAssembly;
}
if (resolvedAssembly != null)
{
return resolvedAssembly;
}
}

Expand Down Expand Up @@ -741,7 +734,7 @@ internal static void InvokeAssemblyLoadEvent(Assembly assembly)

var args = new ResolveEventArgs(name, assembly);

foreach (ResolveEventHandler handler in eventHandler.GetInvocationList())
foreach (ResolveEventHandler handler in Delegate.EnumerateInvocationList(eventHandler))
{
Assembly? asm = handler(AppDomain.CurrentDomain, args);
#if CORECLR
Expand Down Expand Up @@ -815,20 +808,13 @@ internal static void InvokeAssemblyLoadEvent(Assembly assembly)

internal IntPtr GetResolvedUnmanagedDll(Assembly assembly, string unmanagedDllName)
{
IntPtr resolvedDll = IntPtr.Zero;

Func<Assembly, string, IntPtr>? dllResolveHandler = _resolvingUnmanagedDll;

if (dllResolveHandler != null)
// Loop through the event subscribers and return the first non-null native library handle
foreach (Func<Assembly, string, IntPtr> handler in Delegate.EnumerateInvocationList(_resolvingUnmanagedDll))
{
// Loop through the event subscribers and return the first non-null native library handle
foreach (Func<Assembly, string, IntPtr> handler in dllResolveHandler.GetInvocationList())
IntPtr resolvedDll = handler(assembly, unmanagedDllName);
if (resolvedDll != IntPtr.Zero)
{
resolvedDll = handler(assembly, unmanagedDllName);
if (resolvedDll != IntPtr.Zero)
{
return resolvedDll;
}
return resolvedDll;
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,7 @@ public abstract partial class Delegate : System.ICloneable, System.Runtime.Seria
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The target method might be removed")]
protected Delegate(object target, string method) { }
protected Delegate([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type target, string method) { }
public bool HasSingleTarget { get { throw null; } }
public System.Reflection.MethodInfo Method { get { throw null; } }
public object? Target { get { throw null; } }
public virtual object Clone() { throw null; }
Expand All @@ -2178,6 +2179,7 @@ protected Delegate([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAt
public static System.Delegate? CreateDelegate(System.Type type, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type target, string method, bool ignoreCase, bool throwOnBindFailure) { throw null; }
public object? DynamicInvoke(params object?[]? args) { throw null; }
protected virtual object? DynamicInvokeImpl(object?[]? args) { throw null; }
public static System.Delegate.InvocationListEnumerator<TDelegate> EnumerateInvocationList<TDelegate>(TDelegate? d) where TDelegate : System.Delegate { throw null; }
public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; }
public override int GetHashCode() { throw null; }
public virtual System.Delegate[] GetInvocationList() { throw null; }
Expand All @@ -2190,6 +2192,13 @@ public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo
public static System.Delegate? Remove(System.Delegate? source, System.Delegate? value) { throw null; }
public static System.Delegate? RemoveAll(System.Delegate? source, System.Delegate? value) { throw null; }
protected virtual System.Delegate? RemoveImpl(System.Delegate d) { throw null; }
public partial struct InvocationListEnumerator<TDelegate> where TDelegate : System.Delegate
{
public TDelegate Current { get { throw null; } }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Delegate.InvocationListEnumerator<TDelegate> GetEnumerator() { throw null; }
public bool MoveNext() { throw null; }
}
}
public partial class DivideByZeroException : System.ArithmeticException
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ public static void GetInvocationList()
Assert.NotNull(delegates);
Assert.Equal(1, delegates.Length);
Assert.True(dfoo.Equals(delegates[0]));

Assert.True(dfoo.HasSingleTarget);

int count = 0;
foreach (DFoo d in Delegate.EnumerateInvocationList(dfoo))
{
Assert.Same(d, dfoo);
count++;
}
Assert.Equal(1, count);
}

[Fact]
public static void EnumerateInvocationListNull()
{
foreach (Action d in Delegate.EnumerateInvocationList<Action>(null))
{
Assert.Fail();
}
}

[Fact]
Expand Down Expand Up @@ -182,10 +201,20 @@ private static void CheckInvokeList(D[] expected, D combo, Tracker target)
}
Assert.Same(combo.Target, expected[expected.Length - 1].Target);
Assert.Same(combo.Target, target);
Assert.Equal(combo.HasSingleTarget, invokeList.Length == 1);
int count = 0;
foreach (D d in Delegate.EnumerateInvocationList(combo))
{
Assert.Same(d, invokeList[count]);
count++;
}
Assert.Equal(count, invokeList.Length);
}

private static void CheckIsSingletonDelegate(D expected, D actual, Tracker target)
{
Assert.True(actual.HasSingleTarget);

Assert.True(expected.Equals(actual));
Delegate[] invokeList = actual.GetInvocationList();
Assert.Equal(1, invokeList.Length);
Expand Down
32 changes: 18 additions & 14 deletions src/mono/System.Private.CoreLib/src/System/MulticastDelegate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,6 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
}
}

// Some high-performance applications use this internal property
// to avoid using a slow path to determine if there is more than one handler
// This brings an API that we removed in f410e545e2db0e0dc338673a6b10a5cfd2d3340f
// which some users depeneded on
//
// This is an example of code that used this:
// https://gist.github.com/migueldeicaza/cd99938c2a4372e7e5d5
//
// Do not remove this API
internal bool HasSingleTarget
{
get { return delegates == null; }
}

// <remarks>
// Equals: two multicast delegates are equal if their base is equal
// and their invocations list is equal.
Expand Down Expand Up @@ -127,6 +113,24 @@ public sealed override Delegate[] GetInvocationList()
return new Delegate[1] { this };
}

internal new bool HasSingleTarget
{
get { return delegates == null || delegates.Length == 1; }
}

// Used by delegate invocation list enumerator
internal Delegate? TryGetAt(int index)
{
if (delegates == null)
{
return (index == 0) ? this : null;
}
else
{
return ((uint)index < (uint)delegates.Length) ? delegates[index] : null;
}
}

// <summary>
// Combines this MulticastDelegate with the (Multicast)Delegate `follow'.
// This does _not_ combine with Delegates. ECMA states the whole delegate
Expand Down