From 03ee90908ba2c1277206d85bb8408933397ac552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= Date: Mon, 20 Jul 2026 18:26:19 +0200 Subject: [PATCH 1/9] Share common delegate code, cleanup NativeAOT delegate impl --- .../src/System/Delegate.CoreCLR.cs | 238 +------- .../src/System/Delegate.cs | 534 +++++------------- .../src/System/Delegate.cs | 339 +++++++++-- .../CompilerServices/RuntimeHelpers.cs | 35 +- .../src/System/Delegate.Mono.cs | 24 +- .../src/System/MulticastDelegate.Mono.cs | 5 +- 6 files changed, 472 insertions(+), 703 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs index ea1d1ed65a2dac..dc907fa7747a0f 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs @@ -3,14 +3,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; -using System.Threading; namespace System { @@ -46,8 +44,6 @@ public abstract partial class Delegate : ICloneable, ISerializable private bool IsClosed => _methodPtrAux == 0; - public partial bool HasSingleTarget => _helperObject is null || _helperObject.GetType() != typeof(Wrapper[]); - public object? Target => TryGetInvocations(out ReadOnlySpan invocations) ? invocations[^1].Value!.Target @@ -110,22 +106,6 @@ protected Delegate([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Al DelegateBindingFlags.CaselessMatching); } - // This method returns the Invocation list of this multicast delegate. - public Delegate[] GetInvocationList() - { - if (!TryGetInvocations(out ReadOnlySpan invocations)) - { - return [this]; - } - - Delegate[] invocationList = new Delegate[invocations.Length]; - for (int i = 0; i < invocations.Length; i++) - { - invocationList[i] = invocations[i].Value!; - } - return invocationList; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool TryGetInvocations(out ReadOnlySpan invocations) { @@ -146,22 +126,6 @@ private bool TryGetInvocations(out ReadOnlySpan invocations) return true; } - // Used by delegate invocation list enumerator - private Delegate? TryGetAt(int index) - { - if (TryGetInvocations(out ReadOnlySpan invocations)) - { - if ((uint)index < (uint)invocations.Length) - return invocations[index].Value; - } - else if (index == 0) - { - return this; - } - - return null; - } - protected virtual object? DynamicInvokeImpl(object?[]? args) { RuntimeMethodHandleInternal method = new RuntimeMethodHandleInternal(GetInvokeMethod()); @@ -170,20 +134,9 @@ private bool TryGetInvocations(out ReadOnlySpan invocations) return invoke.Invoke(this, BindingFlags.Default, null, args, null); } - // Equals returns true IIF the delegate is not null and has the - // same target, method and invocation list as this object. - public sealed override unsafe bool Equals([NotNullWhen(true)] object? obj) + private unsafe bool EqualsCore(Delegate other) { - if (obj == null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (!InternalEqualTypes(this, obj)) - return false; - - // Since this is a Delegate, and we know the types are the same, obj should also be a Delegate - Debug.Assert(obj is Delegate, "Shouldn't have failed here since we already checked the types are the same!"); - Delegate other = Unsafe.As(obj); + Debug.Assert(RuntimeHelpers.TypeEquivalent(this, other)); // Check closed delegates first if (IsClosed) @@ -315,7 +268,7 @@ private unsafe MethodInfo GetMethodImplUncached() else { // it's an open one, need to fetch the first arg of the instantiation - MethodInfo invoke = GetType().GetMethod("Invoke")!; + MethodInfo invoke = GetInvokeMethod(GetType()); declaringType = (RuntimeType)invoke.GetParametersAsSpan()[0].ParameterType; } } @@ -528,29 +481,6 @@ private static unsafe Delegate InternalAlloc(MethodTable* type) return Unsafe.As(RuntimeTypeHandle.InternalAllocNoChecks(type)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe bool InternalEqualTypes(object a, object b) - { - if (a.GetType() == b.GetType()) - return true; - -#if FEATURE_TYPEEQUIVALENCE - MethodTable* pMTa = RuntimeHelpers.GetMethodTable(a); - MethodTable* pMTb = RuntimeHelpers.GetMethodTable(b); - - bool ret = pMTa->HasTypeEquivalence && pMTb->HasTypeEquivalence && - // only use QCall to check the type equivalence scenario - RuntimeHelpers.AreTypesEquivalent(pMTa, pMTb); - - GC.KeepAlive(a); - GC.KeepAlive(b); - - return ret; -#else - return false; -#endif // FEATURE_TYPEEQUIVALENCE - } - // Used by the ctor. Do not call directly. // The name of this function will appear in managed stacktraces as delegate constructor. private void DelegateConstruct(object target, IntPtr method) @@ -620,33 +550,6 @@ private static unsafe IRuntimeMethodInfo CreateMethodInfo(MethodDesc* methodDesc [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "Delegate_GetMethodDesc")] private static unsafe partial MethodDesc* GetMethodDesc(ObjectHandleOnStack instance); - internal struct Wrapper(Delegate? value) : IEquatable - { - internal Delegate? Value = value; - - public readonly bool Equals(Wrapper other) - { - // we should never get null here - Debug.Assert(Value is not null); - Debug.Assert(other.Value is not null); - return Value.Equals(other.Value); - } - - public override readonly bool Equals(object? obj) - { - // we should never get another type here - Debug.Assert(obj is Wrapper); - return Equals((Wrapper)obj); - } - - public override readonly int GetHashCode() - { - // we should never get null here - Debug.Assert(Value is not null); - return Value.GetHashCode(); - } - } - private unsafe Delegate NewMulticastDelegate(Wrapper[] invocationList, int invocationCount, bool thisIsMultiCastAlready = false) { // First, allocate a new multicast delegate just like this one, i.e. same type as the this object @@ -671,137 +574,10 @@ private unsafe Delegate NewMulticastDelegate(Wrapper[] invocationList, int invoc return result; } - private static bool TrySetSlot(ref Delegate? d, Delegate o) - { - Delegate? previous = d; - if (previous is null) - { - previous = Interlocked.CompareExchange(ref d, o, null); - if (previous == null) - return true; - } - - // The slot may be already set because we have added and removed the same method before. - // Optimize this case, because it's cheaper than copying the array. - return previous._methodPtr == o._methodPtr && - previous._methodPtrAux == o._methodPtrAux && - previous._target == o._target; - } - - // This method will combine this delegate with the passed delegate - // to form a new delegate. - protected Delegate CombineImpl(Delegate? d) - { - if (d is null) - return this; - - // Verify that the types are the same... - if (!InternalEqualTypes(this, d)) - throw new ArgumentException(SR.Arg_DlgtTypeMis); - - Wrapper wrapper = new Wrapper(d); - ReadOnlySpan followList = d.TryGetInvocations(out ReadOnlySpan span) ? span : new ReadOnlySpan(ref wrapper); - - if (!TryGetInvocations(out ReadOnlySpan invocationList)) - { - int newResultCount = 1 + followList.Length; - Wrapper[] newResultList = new Wrapper[newResultCount]; - newResultList[0] = new Wrapper(this); - followList.CopyTo(new Span(newResultList, 1, followList.Length)); - return NewMulticastDelegate(newResultList, newResultCount); - } - - int resultCount = invocationList.Length + followList.Length; - Wrapper[]? resultList = (Wrapper[])_helperObject!; - if (resultList.Length < resultCount) - { - resultList = null; - } - else - { - Span newInvocations = resultList.AsSpan(invocationList.Length, followList.Length); - for (int i = 0; i < followList.Length; i++) - { - if (TrySetSlot(ref newInvocations[i].Value, followList[i].Value!)) - continue; - - resultList = null; - break; - } - } - - if (resultList == null) - { - resultList = new Wrapper[BitOperations.RoundUpToPowerOf2((uint)resultCount)]; - invocationList.CopyTo(resultList); - followList.CopyTo(resultList.AsSpan(invocationList.Length)); - } - return NewMulticastDelegate(resultList, resultCount, true); - } - - private static Wrapper[] DeleteFromInvocationList(ReadOnlySpan invocationList, int deleteIndex, int deleteCount) - { - Wrapper[] newInvocationList = new Wrapper[BitOperations.RoundUpToPowerOf2((uint)(invocationList.Length - deleteCount))]; - - invocationList.Slice(0, deleteIndex).CopyTo(newInvocationList); - invocationList.Slice(deleteIndex + deleteCount).CopyTo(newInvocationList.AsSpan(deleteIndex)); - - return newInvocationList; - } - - // This method currently looks backward on the invocation list - // for an element that has Delegate based equality with value. (Doesn't - // look at the invocation list.) If this is found we remove it from - // this list and return a new delegate. If its not found a copy of the - // current list is returned. - protected Delegate? RemoveImpl(Delegate? d) - { - // There is a special case were we are removing using a delegate as - // the value we need to check for this case - if (d is null) - return this; - - bool isMulticast = TryGetInvocations(out ReadOnlySpan invocationList); - - if (!d.TryGetInvocations(out ReadOnlySpan otherInvocations)) - { - // they are both not real Multicast - if (!isMulticast) - return Equals(d) ? null : this; - - int index = invocationList.LastIndexOf(new Wrapper(d)); - if (index < 0) - return this; - - // Special case - only one value left, either at the beginning or the end - if (invocationList.Length == 2) - return invocationList[1 - index].Value; - - Wrapper[] list = DeleteFromInvocationList(invocationList, index, 1); - return NewMulticastDelegate(list, invocationList.Length - 1, true); - } - - if (!isMulticast) - return this; - - int i = invocationList.LastIndexOf(otherInvocations); - if (i < 0) - return this; - - int newCount = invocationList.Length - otherInvocations.Length; - switch (newCount) - { - case 0: - // Special case - no values left - return null; - case 1: - // Special case - only one value left, either at the beginning or the end - return invocationList[i == 0 ? ^1 : 0].Value; - default: - Wrapper[] list = DeleteFromInvocationList(invocationList, i, otherInvocations.Length); - return NewMulticastDelegate(list, newCount, true); - } - } + private static bool SlotEquals(Delegate previous, Delegate o) => + previous._methodPtr == o._methodPtr && + previous._methodPtrAux == o._methodPtrAux && + previous._target == o._target; internal static IntPtr AdjustTarget(object target, IntPtr methodPtr) { diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index 28d03e3ae491de..2b259e6aaf0f67 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; -using System.Threading; using Internal.Reflection.Augments; using Internal.Runtime; @@ -19,6 +18,55 @@ namespace System { public abstract partial class Delegate : ICloneable, ISerializable { + // WARNING: These constants are also declared in System.Private.TypeLoader\Internal\Runtime\TypeLoader\CallConverterThunk.cs + // Do not change their values without updating the values in the calling convention converter component + private const int MulticastThunk = 0; + private const int ClosedStaticThunk = 1; + private const int OpenStaticThunk = 2; + private const int ClosedInstanceThunkOverGenericMethod = 3; // This may not exist + private const int OpenInstanceThunk = 4; // This may not exist + private const int ObjectArrayThunk = 5; // This may not exist + + private object _helperObject; + private object _target; // Keep _target and _methodPtr next to each other for optimal delegate invoke performance + private IntPtr _methodPtr; + private nint _extraFunctionPointerOrData; + + private bool IsDynamicDelegate => GetThunk(MulticastThunk) == IntPtr.Zero; + + public object? Target + { + get + { + // Multi-cast delegates return the Target of the last delegate in the list + if (TryGetInvocations(out ReadOnlySpan invocations)) + { + return invocations[^1].Value!.Target; + } + + // Closed static delegates place a value in _helperObject that they pass to the target method. + if (_methodPtr == GetThunk(ClosedStaticThunk) || + _methodPtr == GetThunk(ClosedInstanceThunkOverGenericMethod) || + _methodPtr == GetThunk(ObjectArrayThunk)) + return _helperObject; + + // Other non-closed thunks can be identified as the _target field points at this. + if (ReferenceEquals(_target, this)) + { + return null; + } + + // NativeFunctionPointerWrapper used by marshalled function pointers is not returned as a public target + if (_target is NativeFunctionPointerWrapper) + { + return null; + } + + // Closed instance delegates place a value in _target, and we've ruled out all other types of delegates + return _target; + } + } + // V1 API: Create closed instance delegates. Method name matching is case sensitive. [RequiresUnreferencedCode("The target method might be removed")] protected Delegate(object target, string method) @@ -40,28 +88,25 @@ protected Delegate([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Al throw new PlatformNotSupportedException(); } - private object _helperObject; - private object _target; // Keep _target and _methodPtr next to each other for optimal delegate invoke performance - private IntPtr _methodPtr; - private nint _extraFunctionPointerOrData; - - // _helperObject may point to an array of delegates if this is a multicast delegate. We use this wrapper to distinguish between - // our own array of delegates and user provided Wrapper[]. As a added benefit, this wrapper also eliminates array co-variance - // overhead for our own array of delegates. - private struct Wrapper + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetInvocations(out ReadOnlySpan invocations) { - public Wrapper(Delegate value) => Value = value; - public Delegate Value; - } + if (HasSingleTarget) + { + invocations = default; + return false; + } - // WARNING: These constants are also declared in System.Private.TypeLoader\Internal\Runtime\TypeLoader\CallConverterThunk.cs - // Do not change their values without updating the values in the calling convention converter component - private protected const int MulticastThunk = 0; - private protected const int ClosedStaticThunk = 1; - private protected const int OpenStaticThunk = 2; - private protected const int ClosedInstanceThunkOverGenericMethod = 3; // This may not exist - private protected const int OpenInstanceThunk = 4; // This may not exist - private protected const int ObjectArrayThunk = 5; // This may not exist + Debug.Assert(_helperObject is Wrapper[]); + Wrapper[] invocationList = (Wrapper[])_helperObject; + + Debug.Assert(invocationList.Length > 1); + Debug.Assert((uint)invocationList.Length >= (nuint)_extraFunctionPointerOrData); + Debug.Assert(invocationList[0].Value is not null); + + invocations = new ReadOnlySpan(invocationList, 0, (int)_extraFunctionPointerOrData); + return true; + } // // If the thunk does not exist, the function will return IntPtr.Zero. @@ -84,25 +129,25 @@ private protected virtual IntPtr GetThunk(int whichThunk) /// internal unsafe IntPtr GetDelegateLdFtnResult(out RuntimeTypeHandle typeOfFirstParameterIfInstanceDelegate, out bool isOpenResolver) { - typeOfFirstParameterIfInstanceDelegate = default(RuntimeTypeHandle); + typeOfFirstParameterIfInstanceDelegate = default; isOpenResolver = false; if (_extraFunctionPointerOrData != 0) { - if (GetThunk(OpenInstanceThunk) == _methodPtr) + if (GetThunk(OpenInstanceThunk) != _methodPtr) { - typeOfFirstParameterIfInstanceDelegate = ((OpenMethodResolver*)_extraFunctionPointerOrData)->DeclaringType; - isOpenResolver = true; + return _extraFunctionPointerOrData; } + + typeOfFirstParameterIfInstanceDelegate = ((OpenMethodResolver*)_extraFunctionPointerOrData)->DeclaringType; + isOpenResolver = true; return _extraFunctionPointerOrData; } - else - { - if (_target != null) - typeOfFirstParameterIfInstanceDelegate = new RuntimeTypeHandle(_target.GetMethodTable()); - return _methodPtr; - } + if (_target != null) + typeOfFirstParameterIfInstanceDelegate = new RuntimeTypeHandle(_target.GetMethodTable()); + + return _methodPtr; } // This function is known to the compiler. @@ -162,8 +207,6 @@ private void InitializeClosedInstanceWithGVMResolution(object firstParameter, In _extraFunctionPointerOrData = functionResolution; _helperObject = firstParameter; } - - return; } // This function is known to the compiler. @@ -228,12 +271,10 @@ private IntPtr GetActualTargetFunctionPointer(object thisObject) return OpenMethodResolver.ResolveMethod(_extraFunctionPointerOrData, thisObject); } - internal bool IsDynamicDelegate() => GetThunk(MulticastThunk) == IntPtr.Zero; - - [DebuggerGuidedStepThroughAttribute] + [DebuggerGuidedStepThrough] protected virtual object? DynamicInvokeImpl(object?[]? args) { - if (IsDynamicDelegate()) + if (IsDynamicDelegate) { // DynamicDelegate case object? result = ((Func)_helperObject)(args); @@ -256,16 +297,15 @@ protected virtual MethodInfo GetMethodImpl() // NOTE: this implementation is mirrored in GetDiagnosticMethodInfo below // Multi-cast delegates return the Method of the last delegate in the list - if (_helperObject is Wrapper[] invocationList) + if (TryGetInvocations(out ReadOnlySpan invocations)) { - int invocationCount = (int)_extraFunctionPointerOrData; - return invocationList[invocationCount - 1].Value.GetMethodImpl(); + return invocations[^1].Value!.GetMethodImpl(); } // Return the delegate Invoke method for marshalled function pointers and LINQ expressions if ((_target is NativeFunctionPointerWrapper) || (_methodPtr == GetThunk(ObjectArrayThunk))) { - return GetType().GetMethod("Invoke"); + return GetInvokeMethod(GetType()); } return ReflectionAugments.GetDelegateMethod(this); @@ -276,10 +316,9 @@ internal DiagnosticMethodInfo GetDiagnosticMethodInfo() // NOTE: this implementation is mirrored in GetMethodImpl above // Multi-cast delegates return the diagnostic method info of the last delegate in the list - if (_helperObject is Wrapper[] invocationList) + if (TryGetInvocations(out ReadOnlySpan invocations)) { - int invocationCount = (int)_extraFunctionPointerOrData; - return invocationList[invocationCount - 1].Value.GetDiagnosticMethodInfo(); + return invocations[^1].Value!.GetDiagnosticMethodInfo(); } // Return the delegate Invoke method for marshalled function pointers and LINQ expressions @@ -298,85 +337,48 @@ internal DiagnosticMethodInfo GetDiagnosticMethodInfo() declaringType = declaringType.GetGenericTypeDefinition(); return new DiagnosticMethodInfo(mi.Name, declaringType.FullName, mi.Module.Assembly.FullName); } - else + + IntPtr functionPointer; + if (FunctionPointerOps.IsGenericMethodPointer(ldftnResult)) { - IntPtr functionPointer; - if (FunctionPointerOps.IsGenericMethodPointer(ldftnResult)) - { - unsafe - { - GenericMethodDescriptor* realTargetData = FunctionPointerOps.ConvertToGenericDescriptor(ldftnResult); - functionPointer = RuntimeAugments.GetCodeTarget(realTargetData->MethodFunctionPointer); - } - } - else + unsafe { - nint unboxedPointer = RuntimeAugments.GetCodeTarget(ldftnResult); - if (unboxedPointer == ldftnResult) - unboxedPointer = RuntimeAugments.GetTargetOfUnboxingAndInstantiatingStub(ldftnResult); - - functionPointer = unboxedPointer != 0 ? unboxedPointer : ldftnResult; + GenericMethodDescriptor* realTargetData = FunctionPointerOps.ConvertToGenericDescriptor(ldftnResult); + functionPointer = RuntimeAugments.GetCodeTarget(realTargetData->MethodFunctionPointer); } - return RuntimeAugments.StackTraceCallbacksIfAvailable?.TryGetDiagnosticMethodInfoFromStartAddress(functionPointer); } - } - - public object? Target - { - get + else { - // Multi-cast delegates return the Target of the last delegate in the list - if (_helperObject is Wrapper[] invocationList) - { - int invocationCount = (int)_extraFunctionPointerOrData; - return invocationList[invocationCount - 1].Value.Target; - } - - // Closed static delegates place a value in _helperObject that they pass to the target method. - if (_methodPtr == GetThunk(ClosedStaticThunk) || - _methodPtr == GetThunk(ClosedInstanceThunkOverGenericMethod) || - _methodPtr == GetThunk(ObjectArrayThunk)) - return _helperObject; + nint unboxedPointer = RuntimeAugments.GetCodeTarget(ldftnResult); + if (unboxedPointer == ldftnResult) + unboxedPointer = RuntimeAugments.GetTargetOfUnboxingAndInstantiatingStub(ldftnResult); - // Other non-closed thunks can be identified as the _target field points at this. - if (object.ReferenceEquals(_target, this)) - { - return null; - } - - // NativeFunctionPointerWrapper used by marshalled function pointers is not returned as a public target - if (_target is NativeFunctionPointerWrapper) - { - return null; - } - - // Closed instance delegates place a value in _target, and we've ruled out all other types of delegates - return _target; + functionPointer = unboxedPointer != 0 ? unboxedPointer : ldftnResult; } + return RuntimeAugments.StackTraceCallbacksIfAvailable?.TryGetDiagnosticMethodInfoFromStartAddress(functionPointer); } // V2 api: Creates open or closed delegates to static or instance methods - relaxed signature checking allowed. - public static Delegate CreateDelegate(Type type, object? firstArgument, MethodInfo method, bool throwOnBindFailure) => ReflectionAugments.CreateDelegate(type, firstArgument, method, throwOnBindFailure); + public static Delegate CreateDelegate(Type type, object? firstArgument, MethodInfo method, bool throwOnBindFailure) => + ReflectionAugments.CreateDelegate(type, firstArgument, method, throwOnBindFailure); // V1 api: Creates open delegates to static or instance methods - relaxed signature checking allowed. - public static Delegate CreateDelegate(Type type, MethodInfo method, bool throwOnBindFailure) => ReflectionAugments.CreateDelegate(type, method, throwOnBindFailure); + public static Delegate CreateDelegate(Type type, MethodInfo method, bool throwOnBindFailure) => + ReflectionAugments.CreateDelegate(type, method, throwOnBindFailure); // V1 api: Creates closed delegates to instance methods only, relaxed signature checking disallowed. [RequiresUnreferencedCode("The target method might be removed")] - public static Delegate CreateDelegate(Type type, object target, string method, bool ignoreCase, bool throwOnBindFailure) => ReflectionAugments.CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure); + public static Delegate CreateDelegate(Type type, object target, string method, bool ignoreCase, bool throwOnBindFailure) => + ReflectionAugments.CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure); // V1 api: Creates open delegates to static methods only, relaxed signature checking disallowed. - public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, bool ignoreCase, bool throwOnBindFailure) => ReflectionAugments.CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure); + public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, bool ignoreCase, bool throwOnBindFailure) => + ReflectionAugments.CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure); internal IntPtr TryGetOpenStaticFunctionPointer() => (GetThunk(OpenStaticThunk) == _methodPtr) ? _extraFunctionPointerOrData : 0; internal NativeFunctionPointerWrapper? TryGetNativeFunctionPointerWrapper() => _target as NativeFunctionPointerWrapper; - internal static unsafe bool InternalEqualTypes(object a, object b) - { - return a.GetMethodTable() == b.GetMethodTable(); - } - // Returns a new delegate of the specified type whose implementation is provided by the // provided delegate. internal static unsafe Delegate CreateObjectArrayDelegate(Type t, Func handler) @@ -391,7 +393,7 @@ internal static unsafe Delegate CreateObjectArrayDelegate(Type t, Func + ReferenceEquals(previous._target, o._target) && + ReferenceEquals(previous._helperObject, o._helperObject) && + previous._extraFunctionPointerOrData == o._extraFunctionPointerOrData && + previous._methodPtr == o._methodPtr; - // This method will combine this delegate with the passed delegate - // to form a new delegate. - protected Delegate CombineImpl(Delegate? d) + private bool EqualsCore(Delegate other) { - if (d is null) - return this; - - // Verify that the types are the same... - if (!InternalEqualTypes(this, d)) - throw new ArgumentException(SR.Arg_DlgtTypeMis); + Debug.Assert(RuntimeHelpers.TypeEquivalent(this, other)); - if (IsDynamicDelegate()) - throw new InvalidOperationException(); - - int followCount = 1; - Wrapper[]? followList = d._helperObject as Wrapper[]; - if (followList != null) - followCount = (int)d._extraFunctionPointerOrData; - - int resultCount; - Wrapper[]? resultList; - if (_helperObject is not Wrapper[] invocationList) - { - resultCount = 1 + followCount; - resultList = new Wrapper[resultCount]; - resultList[0] = new Wrapper(this); - if (followList == null) - { - resultList[1] = new Wrapper(d); - } - else - { - for (int i = 0; i < followCount; i++) - resultList[1 + i] = followList[i]; - } - return NewMulticastDelegate(resultList, resultCount); - } - else - { - int invocationCount = (int)_extraFunctionPointerOrData; - resultCount = invocationCount + followCount; - resultList = null; - if (resultCount <= invocationList.Length) - { - resultList = invocationList; - if (followList == null) - { - if (!TrySetSlot(resultList, invocationCount, d)) - resultList = null; - } - else - { - for (int i = 0; i < followCount; i++) - { - if (!TrySetSlot(resultList, invocationCount + i, followList[i].Value)) - { - resultList = null; - break; - } - } - } - } - - if (resultList == null) - { - int allocCount = invocationList.Length; - while (allocCount < resultCount) - allocCount *= 2; - - resultList = new Wrapper[allocCount]; - - for (int i = 0; i < invocationCount; i++) - resultList[i] = invocationList[i]; - - if (followList == null) - { - resultList[invocationCount] = new Wrapper(d); - } - else - { - for (int i = 0; i < followCount; i++) - resultList[invocationCount + i] = followList[i]; - } - } - return NewMulticastDelegate(resultList, resultCount, true); - } - } - - private static Wrapper[] DeleteFromInvocationList(Wrapper[] invocationList, int invocationCount, int deleteIndex, int deleteCount) - { - int allocCount = invocationList.Length; - while (allocCount / 2 >= invocationCount - deleteCount) - allocCount /= 2; - - Wrapper[] newInvocationList = new Wrapper[allocCount]; - - for (int i = 0; i < deleteIndex; i++) - newInvocationList[i] = invocationList[i]; - - for (int i = deleteIndex + deleteCount; i < invocationCount; i++) - newInvocationList[i - deleteCount] = invocationList[i]; - - return newInvocationList; - } - - private static bool EqualInvocationLists(Wrapper[] a, Wrapper[] b, int start, int count) - { - for (int i = 0; i < count; i++) - { - if (!(a[start + i].Value.Equals(b[i].Value))) - return false; - } - return true; - } - - // This method currently looks backward on the invocation list - // for an element that has Delegate based equality with value. (Doesn't - // look at the invocation list.) If this is found we remove it from - // this list and return a new delegate. If its not found a copy of the - // current list is returned. - protected Delegate? RemoveImpl(Delegate? d) - { - // There is a special case were we are removing using a delegate as - // the value we need to check for this case - // - if (d is null) - return this; - if (d._helperObject is not Wrapper[] dInvocationList) - { - if (_helperObject is not Wrapper[] invocationList) - { - // they are both not real Multicast - if (this.Equals(d)) - return null; - } - else - { - int invocationCount = (int)_extraFunctionPointerOrData; - for (int i = invocationCount; --i >= 0;) - { - if (d.Equals(invocationList[i].Value)) - { - if (invocationCount == 2) - { - // Special case - only one value left, either at the beginning or the end - return invocationList[1 - i].Value; - } - else - { - Wrapper[] list = DeleteFromInvocationList(invocationList, invocationCount, i, 1); - return NewMulticastDelegate(list, invocationCount - 1, true); - } - } - } - } - } - else - { - if (_helperObject is Wrapper[] invocationList) - { - int invocationCount = (int)_extraFunctionPointerOrData; - int dInvocationCount = (int)d._extraFunctionPointerOrData; - for (int i = invocationCount - dInvocationCount; i >= 0; i--) - { - if (EqualInvocationLists(invocationList, dInvocationList, i, dInvocationCount)) - { - if (invocationCount - dInvocationCount == 0) - { - // Special case - no values left - return null; - } - else if (invocationCount - dInvocationCount == 1) - { - // Special case - only one value left, either at the beginning or the end - return invocationList[i != 0 ? 0 : invocationCount - 1].Value; - } - else - { - Wrapper[] list = DeleteFromInvocationList(invocationList, invocationCount, i, dInvocationCount); - return NewMulticastDelegate(list, invocationCount - dInvocationCount, true); - } - } - } - } - } - - return this; - } - - public Delegate[] GetInvocationList() - { - if (_helperObject is Wrapper[] invocationList) - { - // Create an array of delegate copies and each - // element into the array - int invocationCount = (int)_extraFunctionPointerOrData; - - var del = new Delegate[invocationCount]; - for (int i = 0; i < del.Length; i++) - del[i] = invocationList[i].Value; - return del; - } - - return new Delegate[] { this }; - } - - public sealed override bool Equals([NotNullWhen(true)] object? obj) - { - if (obj == null) - return false; - if (object.ReferenceEquals(this, obj)) - return true; - if (!InternalEqualTypes(this, obj)) - return false; - - // Since this is a Delegate and we know the types are the same, obj should also be a Delegate - Debug.Assert(obj is Delegate, "Shouldn't have failed here since we already checked the types are the same!"); - var d = Unsafe.As(obj); - - if (_helperObject is Wrapper[] invocationList) - { - if (d._extraFunctionPointerOrData != _extraFunctionPointerOrData) - return false; - - if (d._helperObject is not Wrapper[] dInvocationList) - return false; - - int invocationCount = (int)_extraFunctionPointerOrData; - for (int i = 0; i < invocationCount; i++) - { - if (!invocationList[i].Value.Equals(dInvocationList[i].Value)) - return false; - } - return true; - } + if (TryGetInvocations(out ReadOnlySpan invocations)) + return other.TryGetInvocations(out ReadOnlySpan otherInvocations) && invocations.SequenceEqual(otherInvocations); if (_target is NativeFunctionPointerWrapper nativeFunctionPointerWrapper) { - if (d._target is not NativeFunctionPointerWrapper dnativeFunctionPointerWrapper) + if (other._target is not NativeFunctionPointerWrapper dnativeFunctionPointerWrapper) return false; return nativeFunctionPointerWrapper.NativeFunctionPointer == dnativeFunctionPointerWrapper.NativeFunctionPointer; } - if (!object.ReferenceEquals(_helperObject, d._helperObject) || - (!FunctionPointerOps.Compare(_extraFunctionPointerOrData, d._extraFunctionPointerOrData)) || - (!FunctionPointerOps.Compare(_methodPtr, d._methodPtr))) + if (!ReferenceEquals(_helperObject, other._helperObject) || + (!FunctionPointerOps.Compare(_extraFunctionPointerOrData, other._extraFunctionPointerOrData)) || + (!FunctionPointerOps.Compare(_extraFunctionPointerOrData, other._extraFunctionPointerOrData)) || + (!FunctionPointerOps.Compare(_methodPtr, other._methodPtr))) { return false; } // Those delegate kinds with thunks put themselves into the _target, so we can't // blindly compare the _target fields for equality. - if (object.ReferenceEquals(_target, this)) - { - return object.ReferenceEquals(d._target, d); - } - - return object.ReferenceEquals(_target, d._target); + return ReferenceEquals(ReferenceEquals(_target, this) ? other : _target, other._target); } - public sealed override int GetHashCode() + public sealed override unsafe int GetHashCode() { - if (_helperObject is Wrapper[] invocationList) + if (TryGetInvocations(out ReadOnlySpan invocations)) { - int multiCastHash = 0; - for (int i = 0; i < (int)_extraFunctionPointerOrData; i++) + int hash = 0; + foreach (ref readonly Wrapper wrapper in invocations) { - multiCastHash = multiCastHash * 33 + invocationList[i].Value.GetHashCode(); + hash = hash * 33 + wrapper.GetHashCode(); } - return multiCastHash; + return hash; } + MethodTable* methodTable = this.GetMethodTable(); if (_target is NativeFunctionPointerWrapper nativeFunctionPointerWrapper) { - return nativeFunctionPointerWrapper.NativeFunctionPointer.GetHashCode(); + return HashCode.Combine((nuint)methodTable, nativeFunctionPointerWrapper.NativeFunctionPointer); } - int hash = RuntimeHelpers.GetHashCode(_helperObject) + - 7 * FunctionPointerOps.GetHashCode(_extraFunctionPointerOrData) + - 13 * FunctionPointerOps.GetHashCode(_methodPtr); - - if (!object.ReferenceEquals(_target, this)) + int hashCode = HashCode.Combine((nuint)methodTable, + RuntimeHelpers.GetHashCode(_helperObject), + FunctionPointerOps.GetHashCode(_extraFunctionPointerOrData), + FunctionPointerOps.GetHashCode(_methodPtr)); + if (!ReferenceEquals(_target, this)) { - hash += 17 * RuntimeHelpers.GetHashCode(_target); + hashCode += RuntimeHelpers.GetHashCode(_target) * 33; } - - return hash; - } - - public partial bool HasSingleTarget => _helperObject is not Wrapper[]; - - // Used by delegate invocation list enumerator - internal Delegate? TryGetAt(int index) - { - if (_helperObject is Wrapper[] invocationList) - { - return ((uint)index < (uint)_extraFunctionPointerOrData) ? invocationList[index].Value : null; - } - - return (index == 0) ? this : null; + return hashCode; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs index a25fab5c4cdf65..4aaba9d29ae7ed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs @@ -4,10 +4,12 @@ using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; +using System.Threading; namespace System { @@ -15,67 +17,33 @@ namespace System [ComVisible(true)] public abstract partial class Delegate : ICloneable, ISerializable { - public virtual object Clone() => MemberwiseClone(); - - [return: NotNullIfNotNull(nameof(a))] - [return: NotNullIfNotNull(nameof(b))] - public static Delegate? Combine(Delegate? a, Delegate? b) - { - if (a is null) - return b; - - return a.CombineImpl(b); - } - - public static Delegate? Combine(params Delegate?[]? delegates) => - Combine((ReadOnlySpan)delegates); - /// - /// Concatenates the invocation lists of an span of delegates. + /// Gets a value that indicates whether the has a single invocation target. /// - /// The span of delegates to combine. - /// - /// A new delegate with an invocation list that concatenates the invocation lists of the delegates in the span. - /// Returns if is , - /// if contains zero elements, or if every entry in is . - /// - public static Delegate? Combine(params ReadOnlySpan delegates) - { - Delegate? d = null; - - if (!delegates.IsEmpty) - { - d = delegates[0]; - for (int i = 1; i < delegates.Length; i++) - { - d = Combine(d, delegates[i]); - } - } - - return d; - } + /// true if the has a single invocation target. + public partial bool HasSingleTarget { get; } // V2 api: Creates open or closed delegates to static or instance methods - relaxed signature checking allowed. - public static Delegate CreateDelegate(Type type, object? firstArgument, MethodInfo method) => CreateDelegate(type, firstArgument, method, throwOnBindFailure: true)!; + public static Delegate CreateDelegate(Type type, object? firstArgument, MethodInfo method) => + CreateDelegate(type, firstArgument, method, throwOnBindFailure: true)!; // V1 api: Creates open delegates to static or instance methods - relaxed signature checking allowed. - public static Delegate CreateDelegate(Type type, MethodInfo method) => CreateDelegate(type, method, throwOnBindFailure: true)!; + public static Delegate CreateDelegate(Type type, MethodInfo method) => + CreateDelegate(type, method, throwOnBindFailure: true)!; // V1 api: Creates closed delegates to instance methods only, relaxed signature checking disallowed. [RequiresUnreferencedCode("The target method might be removed")] - public static Delegate CreateDelegate(Type type, object target, string method) => CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!; + public static Delegate CreateDelegate(Type type, object target, string method) => + CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!; [RequiresUnreferencedCode("The target method might be removed")] - public static Delegate CreateDelegate(Type type, object target, string method, bool ignoreCase) => CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!; + public static Delegate CreateDelegate(Type type, object target, string method, bool ignoreCase) => + CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!; // V1 api: Creates open delegates to static methods only, relaxed signature checking disallowed. - public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method) => CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!; - public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, bool ignoreCase) => CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!; - - /// - /// Gets a value that indicates whether the has a single invocation target. - /// - /// true if the has a single invocation target. - public partial bool HasSingleTarget { get; } + public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method) => + CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!; + public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, bool ignoreCase) => + CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!; internal object GetTargetForSingleCastInstanceDelegate() { @@ -95,14 +63,14 @@ internal object GetTargetForSingleCastInstanceDelegate() /// 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. /// - public static System.Delegate.InvocationListEnumerator EnumerateInvocationList(TDelegate? d) where TDelegate : System.Delegate + public static InvocationListEnumerator EnumerateInvocationList(TDelegate? d) where TDelegate : Delegate => new InvocationListEnumerator(Unsafe.As(d)); /// /// Provides an enumerator for the invocation list of a delegate. /// /// Delegate type being enumerated. - public struct InvocationListEnumerator where TDelegate : System.Delegate + public struct InvocationListEnumerator where TDelegate : Delegate { private readonly MulticastDelegate? _delegate; private int _index; @@ -143,7 +111,7 @@ public bool MoveNext() /// /// An IEnumerator instance that can be used to iterate through the invocation targets of the delegate. [EditorBrowsable(EditorBrowsableState.Never)] // Only here to make foreach work - public System.Delegate.InvocationListEnumerator GetEnumerator() => this; + public InvocationListEnumerator GetEnumerator() => this; } public object? DynamicInvoke(params object?[]? args) @@ -157,6 +125,43 @@ public bool MoveNext() public MethodInfo Method => GetMethodImpl(); + public virtual object Clone() => MemberwiseClone(); + + [return: NotNullIfNotNull(nameof(a))] + [return: NotNullIfNotNull(nameof(b))] + public static Delegate? Combine(Delegate? a, Delegate? b) + { + return a is null ? b : a.CombineImpl(b); + } + + public static Delegate? Combine(params Delegate?[]? delegates) => + Combine((ReadOnlySpan)delegates); + + /// + /// Concatenates the invocation lists of an span of delegates. + /// + /// The span of delegates to combine. + /// + /// A new delegate with an invocation list that concatenates the invocation lists of the delegates in the span. + /// Returns if is , + /// if contains zero elements, or if every entry in is . + /// + public static Delegate? Combine(params ReadOnlySpan delegates) + { + if (delegates.IsEmpty) + { + return null; + } + + Delegate? combined = delegates[0]; + foreach (Delegate? del in delegates[1..]) + { + combined = Combine(combined, del); + } + + return combined; + } + public static Delegate? Remove(Delegate? source, Delegate? value) { if (source == null) @@ -165,7 +170,7 @@ public bool MoveNext() if (value == null) return source; - if (!InternalEqualTypes(source, value)) + if (!RuntimeHelpers.TypeEquivalent(source, value)) throw new ArgumentException(SR.Arg_DlgtTypeMis); return source.RemoveImpl(value); @@ -210,5 +215,233 @@ public bool MoveNext() return !ReferenceEquals(d2, d1) && !d2.Equals(d1); } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "The trimmer will never remove the Invoke method from delegates.")] + internal static MethodInfo GetInvokeMethod(Type delegateType) + { + Debug.Assert(delegateType.IsAssignableTo(typeof(Delegate))); + + MethodInfo? invoke = delegateType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); + Debug.Assert(invoke is not null); + + return invoke; + } + +#if !MONO + internal struct Wrapper(Delegate? value) : IEquatable + { + internal Delegate? Value = value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Wrapper other) + { + // we should never get null here + Debug.Assert(Value is not null); + Debug.Assert(other.Value is not null); + + // use EqualsCore since the type is always the same here + return ReferenceEquals(Value, other.Value) || Value.EqualsCore(other.Value); + } + + public override readonly bool Equals(object? obj) + { + // we should never get another type here + Debug.Assert(obj is Wrapper); + return Equals((Wrapper)obj); + } + + public override readonly int GetHashCode() + { + // we should never get null here + Debug.Assert(Value is not null); + return Value.GetHashCode(); + } + } + + public partial bool HasSingleTarget => _helperObject is null || _helperObject.GetType() != typeof(Wrapper[]); + + // This method returns the Invocation list of this multicast delegate. + public Delegate[] GetInvocationList() + { + if (!TryGetInvocations(out ReadOnlySpan invocations)) + { + return [this]; + } + + Delegate[] invocationList = new Delegate[invocations.Length]; + for (int i = 0; i < invocations.Length; i++) + { + invocationList[i] = invocations[i].Value!; + } + return invocationList; + } + + // Used by delegate invocation list enumerator + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Delegate? TryGetAt(int index) + { + if (TryGetInvocations(out ReadOnlySpan invocations)) + { + if ((uint)index < (uint)invocations.Length) + return invocations[index].Value; + } + else if (index == 0) + { + return this; + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TrySetSlot(ref Delegate? d, Delegate o) + { + Delegate? previous = d; + if (previous is null) + { + previous = Interlocked.CompareExchange(ref d, o, null); + if (previous == null) + return true; + } + + // The slot may be already set because we have added and removed the same method before. + // Optimize this case, because it's cheaper than copying the array. + return SlotEquals(previous, o); + } + + // This method will combine this delegate with the passed delegate + // to form a new delegate. + protected Delegate CombineImpl(Delegate? d) + { + if (d is null) + return this; + + // Verify that the types are the same... + if (!RuntimeHelpers.TypeEquivalent(this, d)) + throw new ArgumentException(SR.Arg_DlgtTypeMis); + + Wrapper wrapper = new Wrapper(d); + ReadOnlySpan followList = d.TryGetInvocations(out ReadOnlySpan span) ? span : new ReadOnlySpan(ref wrapper); + + if (!TryGetInvocations(out ReadOnlySpan invocationList)) + { + int newResultCount = 1 + followList.Length; + Wrapper[] newResultList = new Wrapper[newResultCount]; + newResultList[0] = new Wrapper(this); + followList.CopyTo(newResultList.AsSpan(1, followList.Length)); + + return NewMulticastDelegate(newResultList, newResultCount); + } + + int resultCount = invocationList.Length + followList.Length; + Wrapper[]? resultList = (Wrapper[])_helperObject!; + if (resultList.Length < resultCount) + { + resultList = null; + } + else + { + Span newInvocations = resultList.AsSpan(invocationList.Length, followList.Length); + for (int i = 0; i < followList.Length; i++) + { + if (TrySetSlot(ref newInvocations[i].Value, followList[i].Value!)) + continue; + + resultList = null; + break; + } + } + + if (resultList == null) + { + resultList = new Wrapper[BitOperations.RoundUpToPowerOf2((uint)resultCount)]; + invocationList.CopyTo(resultList); + followList.CopyTo(resultList.AsSpan(invocationList.Length)); + } + return NewMulticastDelegate(resultList, resultCount, true); + } + + private static Wrapper[] DeleteFromInvocationList(ReadOnlySpan invocationList, int deleteIndex, int deleteCount) + { + Wrapper[] newInvocationList = new Wrapper[BitOperations.RoundUpToPowerOf2((uint)(invocationList.Length - deleteCount))]; + + invocationList.Slice(0, deleteIndex).CopyTo(newInvocationList); + invocationList.Slice(deleteIndex + deleteCount).CopyTo(newInvocationList.AsSpan(deleteIndex)); + + return newInvocationList; + } + + // This method currently looks backward on the invocation list + // for an element that has Delegate based equality with value. (Doesn't + // look at the invocation list.) If this is found we remove it from + // this list and return a new delegate. If its not found a copy of the + // current list is returned. + protected Delegate? RemoveImpl(Delegate? d) + { + // There is a special case were we are removing using a delegate as + // the value we need to check for this case + if (d is null) + return this; + + bool isMulticast = TryGetInvocations(out ReadOnlySpan invocationList); + + if (!d.TryGetInvocations(out ReadOnlySpan otherInvocations)) + { + // they are both not real Multicast + if (!isMulticast) + return Equals(d) ? null : this; + + int index = invocationList.LastIndexOf(new Wrapper(d)); + if (index < 0) + return this; + + // Special case - only one value left, either at the beginning or the end + if (invocationList.Length == 2) + return invocationList[1 - index].Value; + + Wrapper[] list = DeleteFromInvocationList(invocationList, index, 1); + return NewMulticastDelegate(list, invocationList.Length - 1, true); + } + + if (!isMulticast) + return this; + + int i = invocationList.LastIndexOf(otherInvocations); + if (i < 0) + return this; + + int newCount = invocationList.Length - otherInvocations.Length; + switch (newCount) + { + case 0: + // Special case - no values left + return null; + case 1: + // Special case - only one value left, either at the beginning or the end + return invocationList[i == 0 ? ^1 : 0].Value; + default: + Wrapper[] list = DeleteFromInvocationList(invocationList, i, otherInvocations.Length); + return NewMulticastDelegate(list, newCount, true); + } + } + + // Equals returns true IIF the delegate is not null and has the + // same target, method and invocation list as this object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sealed override bool Equals([NotNullWhen(true)] object? obj) + { + if (obj == null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (!RuntimeHelpers.TypeEquivalent(this, obj)) + return false; + + // Since this is a Delegate, and we know the types are the same, obj should also be a Delegate + Debug.Assert(obj is Delegate, "Shouldn't have failed here since we already checked the types are the same!"); + return EqualsCore(Unsafe.As(obj)); + } +#endif } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs index 038452a1c8faf3..9c397c543b8277 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs @@ -2,10 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; +#if NATIVEAOT +using Internal.Runtime; +#endif + namespace System.Runtime.CompilerServices { public static partial class RuntimeHelpers @@ -196,5 +199,35 @@ public static ReadOnlySpan CreateSpan(RuntimeFieldHandle fldHandle) [Intrinsic] internal static void SetNextCallAsyncContinuation(object value) => throw new UnreachableException(); // Unconditionally expanded intrinsic + + internal static unsafe bool TypeEquivalent(object a, object b) + { + Debug.Assert(a is not null); + Debug.Assert(b is not null); + +#if MONO + // Mono doesn't have MethodTables nor Equivalence + return a.GetType() == b.GetType(); +#else + MethodTable* pMTa = GetMethodTable(a); + MethodTable* pMTb = GetMethodTable(b); + + if (pMTa == pMTb) + return true; + +#if FEATURE_TYPEEQUIVALENCE + bool ret = pMTa->HasTypeEquivalence && pMTb->HasTypeEquivalence && + // only use QCall to check the type equivalence scenario + AreTypesEquivalent(pMTa, pMTb); + + GC.KeepAlive(a); + GC.KeepAlive(b); + + return ret; +#else + return false; +#endif // FEATURE_TYPEEQUIVALENCE +#endif + } } } diff --git a/src/mono/System.Private.CoreLib/src/System/Delegate.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Delegate.Mono.cs index 0cfc966bf1d8b7..27510650e40280 100644 --- a/src/mono/System.Private.CoreLib/src/System/Delegate.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Delegate.Mono.cs @@ -212,7 +212,7 @@ public static Delegate CreateDelegate(Type type, object? firstArgument, MethodIn private static MethodInfo? GetCandidateMethod(RuntimeType type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, BindingFlags bflags, bool ignoreCase) { - MethodInfo? invoke = GetDelegateInvokeMethod(type); + MethodInfo? invoke = GetInvokeMethod(type); if (invoke is null) return null; @@ -249,7 +249,7 @@ public static Delegate CreateDelegate(Type type, object? firstArgument, MethodIn private static bool IsMatchingCandidate(RuntimeType type, object? target, MethodInfo method, bool allowClosed, out DelegateData? delegateData) { - MethodInfo? invoke = GetDelegateInvokeMethod(type); + MethodInfo? invoke = GetInvokeMethod(type); if (invoke == null || !IsReturnTypeMatch(invoke.ReturnType!, method.ReturnType!)) { delegateData = null; @@ -363,15 +363,6 @@ private static bool IsMatchingCandidate(RuntimeType type, object? target, Method return argsMatch; } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", - Justification = "ILLinker will never remove the Invoke method from delegates.")] - private static MethodInfo? GetDelegateInvokeMethod(RuntimeType type) - { - Debug.Assert(type.IsDelegate()); - - return type.GetMethod("Invoke"); - } - private static bool IsReturnTypeMatch(Type delReturnType, Type returnType) { bool returnMatch = returnType == delReturnType; @@ -440,7 +431,7 @@ private static bool IsArgumentTypeMatchWithThis(Type delArgType, Type argType, b data ??= CreateDelegateData(); // replace all Type.Missing with default values defined on parameters of the delegate if any - MethodInfo? invoke = GetType().GetMethod("Invoke"); + MethodInfo? invoke = GetInvokeMethod(GetType()); if (invoke != null && args != null) { ReadOnlySpan delegateParameters = invoke.GetParametersAsSpan(); @@ -493,7 +484,7 @@ private static bool IsArgumentTypeMatchWithThis(Type delArgType, Type argType, b public override bool Equals([NotNullWhen(true)] object? obj) { - if (!(obj is Delegate d) || !InternalEqualTypes(this, obj)) + if (!(obj is Delegate d) || !RuntimeHelpers.TypeEquivalent(this, obj)) return false; // Do not compare method_ptr, since it can point to a trampoline @@ -553,7 +544,7 @@ private DelegateData CreateDelegateData() } else { - MethodInfo? invoke = GetType().GetMethod("Invoke"); + MethodInfo? invoke = GetInvokeMethod(GetType()); if (invoke != null && invoke.GetParametersCount() + 1 == method_info.GetParametersCount()) delegate_data.curried_first_arg = true; } @@ -562,11 +553,6 @@ private DelegateData CreateDelegateData() return delegate_data; } - internal static bool InternalEqualTypes(object source, object value) - { - return source.GetType() == value.GetType(); - } - [MethodImplAttribute(MethodImplOptions.InternalCall)] private protected static extern MulticastDelegate AllocDelegateLike_internal(Delegate d); diff --git a/src/mono/System.Private.CoreLib/src/System/MulticastDelegate.Mono.cs b/src/mono/System.Private.CoreLib/src/System/MulticastDelegate.Mono.cs index e45b9d844582b0..81d4de2f7ed539 100644 --- a/src/mono/System.Private.CoreLib/src/System/MulticastDelegate.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/MulticastDelegate.Mono.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Serialization; namespace System { @@ -125,7 +124,7 @@ internal Delegate CombineImplImpl(Delegate? follow) return this; // Verify that the types are the same... - if (!InternalEqualTypes(this, follow)) + if (!RuntimeHelpers.TypeEquivalent(this, follow)) throw new ArgumentException(SR.Arg_DlgtTypeMis); MulticastDelegate other = (MulticastDelegate)follow; From 61c485d5357f8bbc9d0ff750cdc7d173da93017e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:35:42 +0200 Subject: [PATCH 2/9] Fix duplicate comparison in Delegate equality check Removed duplicate comparison of '_extraFunctionPointerOrData' in Delegate.cs. --- .../nativeaot/System.Private.CoreLib/src/System/Delegate.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index 2b259e6aaf0f67..c7b6e16246a55e 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -485,7 +485,6 @@ private bool EqualsCore(Delegate other) } if (!ReferenceEquals(_helperObject, other._helperObject) || - (!FunctionPointerOps.Compare(_extraFunctionPointerOrData, other._extraFunctionPointerOrData)) || (!FunctionPointerOps.Compare(_extraFunctionPointerOrData, other._extraFunctionPointerOrData)) || (!FunctionPointerOps.Compare(_methodPtr, other._methodPtr))) { From eab25cd9ba41f32642710eb741a82782379dc989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:39:52 +0200 Subject: [PATCH 3/9] Refactor Delegate.cs for improved readability --- .../System.Private.CoreLib/src/System/Delegate.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index c7b6e16246a55e..58fb5d22024985 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -303,7 +303,7 @@ protected virtual MethodInfo GetMethodImpl() } // Return the delegate Invoke method for marshalled function pointers and LINQ expressions - if ((_target is NativeFunctionPointerWrapper) || (_methodPtr == GetThunk(ObjectArrayThunk))) + if (_target is NativeFunctionPointerWrapper || _methodPtr == GetThunk(ObjectArrayThunk)) { return GetInvokeMethod(GetType()); } @@ -322,7 +322,7 @@ internal DiagnosticMethodInfo GetDiagnosticMethodInfo() } // Return the delegate Invoke method for marshalled function pointers and LINQ expressions - if ((_target is NativeFunctionPointerWrapper) || (_methodPtr == GetThunk(ObjectArrayThunk))) + if (_target is NativeFunctionPointerWrapper || _methodPtr == GetThunk(ObjectArrayThunk)) { Type t = GetType(); return new DiagnosticMethodInfo("Invoke", t.FullName, t.Module.Assembly.FullName); @@ -375,7 +375,7 @@ public static Delegate CreateDelegate(Type type, object target, string method, b public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllMethods)] Type target, string method, bool ignoreCase, bool throwOnBindFailure) => ReflectionAugments.CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure); - internal IntPtr TryGetOpenStaticFunctionPointer() => (GetThunk(OpenStaticThunk) == _methodPtr) ? _extraFunctionPointerOrData : 0; + internal IntPtr TryGetOpenStaticFunctionPointer() => GetThunk(OpenStaticThunk) == _methodPtr ? _extraFunctionPointerOrData : 0; internal NativeFunctionPointerWrapper? TryGetNativeFunctionPointerWrapper() => _target as NativeFunctionPointerWrapper; @@ -391,7 +391,7 @@ internal static unsafe Delegate CreateObjectArrayDelegate(Type t, Func Date: Wed, 22 Jul 2026 03:27:22 +0200 Subject: [PATCH 4/9] Change GetInvokeMethod to include non-public methods --- src/libraries/System.Private.CoreLib/src/System/Delegate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs index 4aaba9d29ae7ed..90df19f867cb42 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs @@ -222,7 +222,7 @@ internal static MethodInfo GetInvokeMethod(Type delegateType) { Debug.Assert(delegateType.IsAssignableTo(typeof(Delegate))); - MethodInfo? invoke = delegateType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); + MethodInfo? invoke = delegateType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); Debug.Assert(invoke is not null); return invoke; From 2e2fff1e6f651adacdee1f573a00644e3fd750b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:28:36 +0200 Subject: [PATCH 5/9] Add assertions to GetInvokeMethod for delegate types --- src/libraries/System.Private.CoreLib/src/System/Delegate.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs index 90df19f867cb42..b7891e660f900e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs @@ -221,6 +221,8 @@ public bool MoveNext() internal static MethodInfo GetInvokeMethod(Type delegateType) { Debug.Assert(delegateType.IsAssignableTo(typeof(Delegate))); + Debug.Assert(delegateType != typeof(Delegate)); + Debug.Assert(delegateType != typeof(MulticastDelegate)); MethodInfo? invoke = delegateType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); Debug.Assert(invoke is not null); From 5336e46e534594e262ec767b88d346fb5fe609fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:34:32 +0200 Subject: [PATCH 6/9] Refactor Delegate class for clarity and performance Removed unnecessary IsDynamicDelegate property and updated DebuggerGuidedStepThrough attribute. --- .../nativeaot/System.Private.CoreLib/src/System/Delegate.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index 0ea971cbfc2969..fb81136768e5b8 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -32,8 +32,6 @@ public abstract partial class Delegate : ICloneable, ISerializable private IntPtr _methodPtr; private nint _extraFunctionPointerOrData; - private bool IsDynamicDelegate => GetThunk(MulticastThunk) == IntPtr.Zero; - public object? Target { get @@ -271,7 +269,7 @@ private IntPtr GetActualTargetFunctionPointer(object thisObject) return OpenMethodResolver.ResolveMethod(_extraFunctionPointerOrData, thisObject); } - [DebuggerGuidedStepThroughAttribute] + [DebuggerGuidedStepThrough] protected virtual object? DynamicInvokeImpl(object?[]? args) { DynamicInvokeInfo dynamicInvokeInfo = ReflectionAugments.GetDelegateDynamicInvokeInfo(GetType()); From 59239a5110fd3d46588aceb1bb6898da65bac29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:42:16 +0200 Subject: [PATCH 7/9] Refactor null checks to use 'is null' syntax --- .../src/System/Delegate.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs index b7891e660f900e..b91cda57a35c22 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs @@ -47,7 +47,7 @@ public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(Dyn internal object GetTargetForSingleCastInstanceDelegate() { - Debug.Assert(HasSingleTarget && Target == _target && _target != null); + Debug.Assert(HasSingleTarget && Target == _target && _target is not null); return _target; } @@ -98,7 +98,7 @@ public TDelegate Current public bool MoveNext() { int index = _index + 1; - if ((_current = Unsafe.As(_delegate?.TryGetAt(index))) == null) + if ((_current = Unsafe.As(_delegate?.TryGetAt(index))) is null) { return false; } @@ -164,10 +164,10 @@ public bool MoveNext() public static Delegate? Remove(Delegate? source, Delegate? value) { - if (source == null) + if (source is null) return null; - if (value == null) + if (value is null) return source; if (!RuntimeHelpers.TypeEquivalent(source, value)) @@ -193,7 +193,7 @@ public bool MoveNext() [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(Delegate? d1, Delegate? d2) { - // Test d2 first to allow branch elimination when inlined for null checks (== null) + // Test d2 first to allow branch elimination when inlined for null checks // so it can become a simple test if (d2 is null) { @@ -206,7 +206,7 @@ public bool MoveNext() [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Delegate? d1, Delegate? d2) { - // Test d2 first to allow branch elimination when inlined for not null checks (!= null) + // Test d2 first to allow branch elimination when inlined for not null checks // so it can become a simple test if (d2 is null) { @@ -303,7 +303,7 @@ private static bool TrySetSlot(ref Delegate? d, Delegate o) if (previous is null) { previous = Interlocked.CompareExchange(ref d, o, null); - if (previous == null) + if (previous is null) return true; } @@ -355,7 +355,7 @@ protected Delegate CombineImpl(Delegate? d) } } - if (resultList == null) + if (resultList is null) { resultList = new Wrapper[BitOperations.RoundUpToPowerOf2((uint)resultCount)]; invocationList.CopyTo(resultList); @@ -433,11 +433,10 @@ private static Wrapper[] DeleteFromInvocationList(ReadOnlySpan invocati [MethodImpl(MethodImplOptions.AggressiveInlining)] public sealed override bool Equals([NotNullWhen(true)] object? obj) { - if (obj == null) - return false; if (ReferenceEquals(this, obj)) return true; - if (!RuntimeHelpers.TypeEquivalent(this, obj)) + + if (obj is null || !RuntimeHelpers.TypeEquivalent(this, obj)) return false; // Since this is a Delegate, and we know the types are the same, obj should also be a Delegate From 647c4a17a850bad33796f32baa7e9d2f0d818528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:44:45 +0200 Subject: [PATCH 8/9] Simplify Debug.Assert for Delegate type check --- src/libraries/System.Private.CoreLib/src/System/Delegate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs index b91cda57a35c22..d1ad6676603c45 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Delegate.cs @@ -440,7 +440,7 @@ public sealed override bool Equals([NotNullWhen(true)] object? obj) return false; // Since this is a Delegate, and we know the types are the same, obj should also be a Delegate - Debug.Assert(obj is Delegate, "Shouldn't have failed here since we already checked the types are the same!"); + Debug.Assert(obj is Delegate); return EqualsCore(Unsafe.As(obj)); } #endif From 19227b74ea2582bf7da560d241e4ccf2e66e932d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Wed, 22 Jul 2026 04:42:55 +0200 Subject: [PATCH 9/9] Remove unnecessary blank line in Delegate.cs --- .../nativeaot/System.Private.CoreLib/src/System/Delegate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs index fb81136768e5b8..6ffd2bf3585c92 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs @@ -268,7 +268,7 @@ private IntPtr GetActualTargetFunctionPointer(object thisObject) { return OpenMethodResolver.ResolveMethod(_extraFunctionPointerOrData, thisObject); } - + [DebuggerGuidedStepThrough] protected virtual object? DynamicInvokeImpl(object?[]? args) {