You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Throw MemberAccessException for reflection calls to static abstract methods - #135566
CoreCLR (Incorrect error message when trying to dynamically invoke a static abstract interface method #101320): on main before this PR, MethodInfo.Invoke and PropertyInfo.GetValue threw TargetInvocationException wrapping BadImageFormatException, and MethodInvoker.Invoke threw the BadImageFormatException itself. MethodInfo.CreateDelegate and Delegate.CreateDelegate by name returned a delegate that threw BadImageFormatException when called (EntryPointNotFoundException on 10.0).
Change. On CoreCLR and NativeAOT, MethodInfo.Invoke, MethodInvoker.Invoke, PropertyInfo.GetValue and SetValue, MethodInfo.CreateDelegate and Delegate.CreateDelegate(Type, [object,] MethodInfo, bool) on a static abstract method throw MemberAccessException without calling it, also with throwOnBindFailure: false, as NativeAOT 10.0's NotSupportedException was. None of these calls has an implementation to reach; they now fail at the reflection call with one exception type, instead of later or with another exception. The message is "Cannot call static abstract method 'I.M'.", except from CoreCLR's CreateDelegate, which throws from the VM: "Cannot bind a delegate to a static abstract method."
Delegate.CreateDelegate(Type, Type, string, ...) skips static abstract methods, as CoreCLR's lookup already skips generic method definitions, so a name that matches only one fails to bind: ArgumentException, or null with throwOnBindFailure: false.
RuntimeMethodHandle.GetFunctionPointer behaves as for an abstract instance method: CoreCLR returns the entry point, and NativeAOT throws PlatformNotSupportedException, as VirtualMethodInvoker does for an abstract method.
These earlier checks still come first: for the invoke calls, open generics (InvalidOperationException) and the argument count passed to MethodInvoker.Invoke; for GetFunctionPointer, open generics; for CreateDelegate, a generic method definition and, on NativeAOT, open generics and the delegate signature.
How:
CoreCLR: RuntimeMethodInfo sets NoInvoke for static abstract methods. Delegate_BindToMethodInfo throws for them and Delegate_BindToMethodName skips them, each right after its generic method definition check. ThrowNoInvokeException loses its IsAbstract branch, which only an abstract method blocked for another reason still reached. COMDelegate::BindToMethod no longer virtualizes a static method in its closed-delegate branch, as Fix COMDelegate::BindToMethod with static virtuals #125875 did for the open one: a delegate over a static virtual method with a body, closed over a non-null first argument, asserted there on a Checked runtime (IsVtableMethod()) and threw EntryPointNotFoundException on 10.0; it now calls the method. The protected Delegate(Type, string) constructor now throws ArgumentException when no method binds, as Delegate(object, string) does. It ignored the bind result, so skipping static abstract methods would have left the delegate unchanged for them, where 10.0 bound one and the call threw EntryPointNotFoundException. Only reflection reaches this constructor, on an existing delegate: a delegate type's own constructor is runtime implemented.
NativeAOT: RuntimeNamedMethodInfo.GetUncachedMethodInvoker returns a StaticAbstractMethodInvoker, unless the method is open generic. It does so before the invoker lookup rather than beside the open generic checks in ExecutionEnvironment.GetMethodInvoker, which for a property accessor is passed the PropertyInfo as the member its exceptions name, not the accessor. The invoker's members throw MemberAccessException, except LdFtnResult, behind GetFunctionPointer; its span Invoke checks the argument count first. The by-name lookup in ReflectionAugments skips static abstract methods. The compiler keeps them out of the invoke map (ShouldMethodBeInInvokeMap), so they no longer get an entry without an entry point. IsMethodSupportedInReflectionInvoke is unchanged: it also drives dependency and dataflow analysis, and excluding these methods there fails two ILCompiler.Trimming.Tests cases that Fix COMDelegate::BindToMethod with static virtuals #125875 updated. ReflectionVirtualInvokeMapNode now calls ShouldMethodBeInInvokeMap instead of repeating its checks. Type.GetInterfaceMap, which doesn't support these methods on NativeAOT (Type.GetInterfaceMap doesn't work for all cases #89157), now checks for them before its cast to VirtualMethodInvoker.
Mono: the RuntimeMethodInfo change is shared code, so it also reaches Mono's Invoke, MethodInvoker.Invoke, SetValue, and GetValue where it falls back to Invoke. There is no Mono-only change: Mono's delegate-based GetValue shortcut, CreateDelegate and GetFunctionPointer are unchanged.
Breaking change
A behavioural change, in .NET 12 (main), for code that reflects over static abstract methods, which have no implementation to call, for the protected Delegate(Type, string) constructor, and on CoreCLR for invoking an abstract method that returns ref to a ref struct:
The calls under "Change" throw MemberAccessException from the reflection call, instead of failing later or with another exception. On CoreCLR, CreateDelegate with a delegate type that doesn't match failed to bind before (ArgumentException, or null).
Delegate.CreateDelegate(Type, Type, string, ...) fails to bind instead of returning a delegate that fails when called (CoreCLR) or throwing NotSupportedException (NativeAOT 10.0).
NativeAOT: GetFunctionPointer on a method with no unbound generic parameters throws PlatformNotSupportedException instead of 10.0's NotSupportedException.
CoreCLR: the protected constructor throws ArgumentException when no method binds, for any name, instead of leaving the delegate unchanged.
CoreCLR: invoking an abstract method that returns ref to a ref struct throws NotSupportedException, as a method with a body does, instead of MemberAccessException.
I can't add labels: could this PR get the breaking-change label? I'll open the dotnet/docs issue once it merges.
Not changed
A delegate closed over the first argument of a static method whose first parameter is int: CoreCLR doesn't bind it (ArgumentException, or null with throwOnBindFailure: false); NativeAOT binds it, and the call returned a different value each run instead of the closed 42, on 10.0 and on this branch. With this PR, both throw MemberAccessException for a static abstract method there.
Delegate.CreateDelegate(Type, Type, string, ignoreCase: true) on a type with two static methods whose names differ only in case: CoreCLR binds one and NativeAOT throws AmbiguousMatchException, on 10.0 and on this branch. With a static abstract M and a static virtual m, the delegate CoreCLR binds now calls m (on 10.0 the call threw EntryPointNotFoundException); NativeAOT still throws AmbiguousMatchException.
MethodInvoker.Invoke(object, Span<object>) with too many or too few arguments for a method of an open generic type or a generic method definition, static or instance, abstract or not: CoreCLR throws TargetParameterCountException and NativeAOT InvalidOperationException, on 10.0 and on this branch. The fixed-arity overloads with no arguments and with two throw TargetParameterCountException there on both.
I can open separate issues for these.
Tests
New tests cover each API above on a static abstract method: generic interfaces and generic methods, Span<int> and ref Span<int> returns, closed delegates, a delegate type that doesn't match, a by-name lookup whose candidates differ only in case (both declaration orders), wrong argument counts and open generics, with a static virtual method with a body as the control and an abstract instance method beside GetFunctionPointer. InvokeRefReturn checks the abstract ref return change on an abstract class method and an interface method. They are in System.Reflection.Tests (MethodCommonTests, MethodInfoTests, MethodInvokerTests, PropertyInfoTests) and System.Runtime.Tests (DelegateTests, InvokeRefReturn, TypeTests). The delegate, function pointer and GetValue tests skip Mono, whose code for them is unchanged; the protected constructor and case-insensitive by-name tests are CoreCLR only, the GetInterfaceMap test NativeAOT only.
Without this PR's runtime and CoreLib changes (the compiler change kept), the new MemberAccessException, argument-count, by-name and protected constructor tests fail, and the closed static virtual delegate tests hit the IsVtableMethod() assert on a Checked CoreCLR.
System.Reflection.Tests and System.Runtime.Tests pass on CoreCLR and compiled with NativeAOT, and the three System.Reflection.Emit test projects on CoreCLR. ILCompiler.Trimming.Tests passes with the compiler change, and CI's trim check (-subset clr.corelib+libs -c Release /p:RunAnalyzersInBuild=true /p:RunILLinkInBuild=true) passes on Windows x64.
Benchmark
Ns per call, Mean ± StdDev, for the CoreCLR delegate paths that gain a check and for MethodInfo.Invoke, whose check is part of the invocation flags (computed once, cached):
Case
Base 1
PR 1
PR 2
Base 2
MethodInfo.CreateDelegate, static method
285.9 ± 6.3
289.8 ± 2.1
290.9 ± 1.3
286.9 ± 6.0
MethodInfo.CreateDelegate, static method of a static class
Delegate.CreateDelegate(Type, Type, string), the last of twenty static methods (found first)
350.1 ± 4.5
380.5 ± 7.9
391.6 ± 3.7
359.9 ± 8.2
Delegate.CreateDelegate(Type, Type, string), the first of twenty static methods (found last)
724.5 ± 8.4
1002.5 ± 24.0
1027.7 ± 25.2
745.9 ± 19.9
DynamicMethod.CreateDelegate
297.3 ± 1.9
315.3 ± 2.6
315.7 ± 2.7
307.1 ± 3.6
MethodInfo.Invoke, static method
16.5 ± 0.3
18.3 ± 0.5
17.9 ± 0.4
16.8 ± 0.3
The new checks test IsStatic() and the declaring type's IsAbstract(), both cached, before the method's attributes, which come from metadata: a static method of a type that isn't abstract reads none, while a static method of an abstract type, a C# static class included, reads its attributes. The closed static case also saves one metadata read: the closed-delegate branch of BindToMethod now tests IsStatic(), a cached bit, before IsVirtual(), which reads the method's attributes.
The DynamicMethod.CreateDelegate and MethodInfo.Invoke rows differ between base and PR although neither reads anything new; the cause is not known.
AI-generated, written at my direction and reviewed by me before posting. The behaviour before this PR was observed on NativeAOT 11.0 RC1 and 10.0, and on CoreCLR 10.0 and local builds of main and of this branch before the closed-delegate change. The tests ran on a local Windows x64 Checked build of this branch; System.Runtime.Tests ran with #135371's test revert applied, as the test it reverts fails discovery on this branch's base. The "Not changed" behaviour was observed with a local program on 10.0 and on this branch, on CoreCLR and NativeAOT. The benchmark ran in process with BenchmarkDotNet 0.16.0-preview.2, 15 iterations, on local all-Release Windows x64 builds of this branch with and without its CoreCLR runtime changes, in the order base, PR, PR, base. Mono was not run locally.
dotnet#125875 removed the exclusion of static virtual methods from
IsMethodSupportedInReflectionInvoke, so static abstract interface
methods got NativeAOT reflection invoke map entries without an entry
point. Reflection then called through a null entry point:
MethodInfo.Invoke, MethodInvoker.Invoke and PropertyInfo.GetValue
crashed the process, a delegate from CreateDelegate crashed when called,
and RuntimeMethodHandle.GetFunctionPointer returned zero.
Skip static abstract methods in ShouldMethodBeInInvokeMap. Reflection
reports them as not invokable with NotSupportedException, as before
dotnet#125875. IsMethodSupportedInReflectionInvoke is unchanged, so the
dependency and dataflow analysis dotnet#125875 enabled for these methods
stays, and static virtual methods with a body stay invokable.
ReflectionVirtualInvokeMapNode now calls ShouldMethodBeInInvokeMap
instead of repeating its checks, so the virtual invoke map, which is
only read for methods with an invoke map entry, matches it.
Fixdotnet#135565
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.
Reflection on a static abstract interface method failed late or crashed.
On CoreCLR, MethodInfo.Invoke, MethodInvoker.Invoke and
PropertyInfo.GetValue called into a method with no body and failed with
BadImageFormatException, MethodInfo.CreateDelegate and
Delegate.CreateDelegate by name returned a delegate that failed when
called, and RuntimeMethodHandle.GetFunctionPointer returned a non-null
pointer. On NativeAOT, the compiler change earlier in this PR made these
calls throw NotSupportedException instead of crashing.
Both runtimes now throw MemberAccessException naming the method from
Invoke, MethodInvoker.Invoke, GetValue, SetValue,
MethodInfo.CreateDelegate, Delegate.CreateDelegate(Type, [object,]
MethodInfo, bool) and GetFunctionPointer, before any call. The existing
checks still come first: open generic types, delegate binding, and the
argument count passed to MethodInvoker.Invoke. On CoreCLR,
RuntimeMethodInfo sets NoInvoke for these methods (shared with Mono),
CreateDelegateInternal throws once the delegate binds, so a delegate
type that doesn't match is still a bind failure, and GetFunctionPointer
throws when the runtime returns null for them. On NativeAOT,
GetUncachedMethodInvoker returns an invoker that throws from every
member, after the argument count check for a span of arguments, and
Type.GetInterfaceMap throws NotSupportedException at such a method
instead of failing a cast. Delegate.CreateDelegate by name skips these
methods on both runtimes, so it fails to bind.
COMDelegate::BindToMethod no longer virtualizes a static method when it
binds a closed delegate, as dotnet#125875 did for open ones. A delegate closed
over a non-null first argument asserted there on a Checked runtime, so
it never reached the new check, and the same delegate over a static
virtual method with a body now works.
The protected Delegate(Type, string) constructor on CoreCLR ignored the
bind result and left the delegate unchanged when no method bound, which
skipping static abstract methods would have extended to them. It now
throws ArgumentException, as Delegate(object, string) does.
Fixdotnet#101320
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
caraioniurie47
changed the title
Keep static abstract methods out of the NativeAOT invoke map
Throw MemberAccessException for reflection calls to static abstract methods
Oct 11, 2026
Delegate_BindToMethodInfo now rejects a static abstract method right
after its generic method definition check, before it checks the
delegate signature, instead of a managed check after the bind. It
throws MemberAccessException with an unformatted message, as the VM
throws managed resource strings without arguments.
RuntimeMethodHandle.GetFunctionPointer behaves as for an abstract
instance method: for a method with no unbound generic parameters,
CoreCLR returns the entry point again, and NativeAOT's static abstract
invoker throws PlatformNotSupportedException, as VirtualMethodInvoker
does for an abstract method.
Both checks test the declaring type's cached flags before IsAbstract(),
which reads metadata. Only abstract types, interfaces included, can
declare an abstract method. Sealed classes are skipped too, so a C#
static class (abstract and sealed) reads no metadata; a static abstract
method in an abstract sealed class, which C# can't declare, is missed.
Delegate_BindToMethodName also tests only once a name matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This is a NativeAOT-only limitation—the same interface map is supported on CoreCLR—so it should throw PlatformNotSupportedException, not NotSupportedException. This also matches the platform-gap handling already used below in this method. Please update the new test accordingly, and provide an actionable resource-backed message rather than an empty exception.
The description's first benchmark table shows delegate creation slower on the PGO-built runtime; the new IsStaticAbstractMethod helper has no PGO data, so that build compiles it out of line. With the helper marked FORCEINLINE, both callers inline it, and on the same PGO data (ns per call, Mean ± StdDev, in the order PR, FORCEINLINE, FORCEINLINE, PR):
Case
PR 1
FORCEINLINE 1
FORCEINLINE 2
PR 2
MethodInfo.CreateDelegate, static method
311.5 ± 7.8
278.7 ± 0.8
280.2 ± 2.0
294.1 ± 2.8
MethodInfo.CreateDelegate, static method of a static class
Delegate.CreateDelegate(Type, Type, string), the last of twenty static methods (found first)
361.4 ± 3.3
345.3 ± 3.8
356.5 ± 2.5
354.1 ± 1.3
Delegate.CreateDelegate(Type, Type, string), the first of twenty static methods (found last)
737.2 ± 2.7
725.0 ± 6.4
738.6 ± 5.8
737.4 ± 2.2
DynamicMethod.CreateDelegate
309.0 ± 1.2
299.5 ± 1.3
302.5 ± 2.4
311.4 ± 2.2
MethodInfo.Invoke, static method
17.2 ± 0.2
17.3 ± 0.1
16.7 ± 0.2
18.5 ± 0.1
Both FORCEINLINE runs are below both PR runs on the four MethodInfo.CreateDelegate rows and DynamicMethod.CreateDelegate; the by-name rows and MethodInfo.Invoke overlap.
Should the helper be FORCEINLINE in this PR, or is the cost acceptable as it is?
Note
AI-generated, written at my direction and reviewed by me before posting. Measured in process with BenchmarkDotNet 0.16.0-preview.2, 15 iterations, on local all-Release Windows x64 builds of this PR's runtime with the repository's PGO data (1.0.0-prerelease.26451.1), with and without FORCEINLINE on the helper; that both callers inline it was checked in the build's symbols.
The reason will be displayed to describe this comment to others. Learn more.
Done in 7220f15: the sealed class shortcut is gone, so a static abstract method of an abstract sealed class is rejected too, and CreateDelegate_EmittedStaticAbstract_ThrowsMemberAccessException has that row. The check still tests the type's IsAbstract() before the method's attributes: the type loader rejects an abstract method in a type that isn't abstract (BFA_AB_METHOD_IN_AB_CLASS).
The reason will be displayed to describe this comment to others. Learn more.
Done locally; before I push: with this change Delegate.CreateDelegate(Type, Type, string) gets slower, because every static method it visits in an abstract type, a C# static class included, now reads its attributes from metadata before the name compare. On a static class with twenty static methods, binding the one found last goes from ~735 ns to ~1015 ns (found first: ~355 ns to ~386 ns), on local Release builds with BenchmarkDotNet. Should I still make this change?
The reason will be displayed to describe this comment to others. Learn more.
Done in 7220f15: Delegate_BindToMethodName now skips a static abstract method right after the generic method definition check, in the same order as Delegate_BindToMethodInfo.
The reason will be displayed to describe this comment to others. Learn more.
Only when NoInvoke is set for another reason that no branch above handles; the case I found is an abstract method returning ref to a ref struct:
abstractclassA{publicabstractrefSpan<int>M();publicvirtualrefSpan<int>V()=>thrownewNotImplementedException();}classB:A{publicoverriderefSpan<int>M()=>thrownewNotImplementedException();}typeof(A).GetMethod("M").Invoke(newB(),null);// 10.0.12: MemberAccessException: Cannot access member.typeof(A).GetMethod("V").Invoke(newB(),null);// 10.0.12: NotSupportedException: ByRef to ByRef-like return values are not supported in reflection invocation.
I'll remove the IsAbstract branch in the next push, so M throws NotSupportedException as V does, with a test for an abstract class method and an interface method.
Use PlatformNotSupportedException with actionable NativeAOT message src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs:34
This is a NativeAOT-only limitation—the same interface map is supported on CoreCLR—so it should throw PlatformNotSupportedException, not NotSupportedException. This also matches the platform-gap handling already used below in this method. Please update the new test accordingly, and provide an actionable resource-backed message rather than an empty exception.
On Copilot's GetInterfaceMap note: the new check keeps NotSupportedException, which NativeAOT 10.0 throws for these methods and which the same loop throws for a method it can't resolve; PlatformNotSupportedException there is only for an abstract instance type.
MethodDesc::IsStaticAbstract() replaces the file-local helper in
comdelegate.cpp and no longer skips sealed classes, so a static
abstract method of an abstract sealed class, which IL allows, is
rejected too. It still tests the declaring type's cached IsAbstract()
before the method's IsAbstract(), which reads metadata: the type
loader rejects an abstract method in a type that isn't abstract.
Delegate_BindToMethodName skips static abstract methods right after
its generic method definition check, as Delegate_BindToMethodInfo
rejects them there.
RuntimeMethodInfo.ThrowNoInvokeException loses its IsAbstract branch.
Only an abstract method with another reason not to invoke it reached
the branch, such as one returning a ref to a ref struct; that method
now gets the NotSupportedException a method with a body gets, instead
of MemberAccessException.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A static abstract interface method has no implementation to call, but reflection on one failed late or crashed:
MethodInfo.Invoke,MethodInvoker.InvokeandPropertyInfo.GetValuecrashed the process with an access violation, a delegate fromMethodInfo.CreateDelegatecrashed when called, andRuntimeMethodHandle.GetFunctionPointerreturned zero. On 10.0 these calls threwNotSupportedException.mainbefore this PR,MethodInfo.InvokeandPropertyInfo.GetValuethrewTargetInvocationExceptionwrappingBadImageFormatException, andMethodInvoker.Invokethrew theBadImageFormatExceptionitself.MethodInfo.CreateDelegateandDelegate.CreateDelegateby name returned a delegate that threwBadImageFormatExceptionwhen called (EntryPointNotFoundExceptionon 10.0).Change. On CoreCLR and NativeAOT,
MethodInfo.Invoke,MethodInvoker.Invoke,PropertyInfo.GetValueandSetValue,MethodInfo.CreateDelegateandDelegate.CreateDelegate(Type, [object,] MethodInfo, bool)on a static abstract method throwMemberAccessExceptionwithout calling it, also withthrowOnBindFailure: false, as NativeAOT 10.0'sNotSupportedExceptionwas. None of these calls has an implementation to reach; they now fail at the reflection call with one exception type, instead of later or with another exception. The message is "Cannot call static abstract method 'I.M'.", except from CoreCLR'sCreateDelegate, which throws from the VM: "Cannot bind a delegate to a static abstract method."Delegate.CreateDelegate(Type, Type, string, ...)skips static abstract methods, as CoreCLR's lookup already skips generic method definitions, so a name that matches only one fails to bind:ArgumentException, ornullwiththrowOnBindFailure: false.RuntimeMethodHandle.GetFunctionPointerbehaves as for an abstract instance method: CoreCLR returns the entry point, and NativeAOT throwsPlatformNotSupportedException, asVirtualMethodInvokerdoes for an abstract method.InvalidOperationException) and the argument count passed toMethodInvoker.Invoke; forGetFunctionPointer, open generics; forCreateDelegate, a generic method definition and, on NativeAOT, open generics and the delegate signature.How:
RuntimeMethodInfosetsNoInvokefor static abstract methods.Delegate_BindToMethodInfothrows for them andDelegate_BindToMethodNameskips them, each right after its generic method definition check.ThrowNoInvokeExceptionloses itsIsAbstractbranch, which only an abstract method blocked for another reason still reached.COMDelegate::BindToMethodno longer virtualizes a static method in its closed-delegate branch, as Fix COMDelegate::BindToMethod with static virtuals #125875 did for the open one: a delegate over a static virtual method with a body, closed over a non-null first argument, asserted there on a Checked runtime (IsVtableMethod()) and threwEntryPointNotFoundExceptionon 10.0; it now calls the method. The protectedDelegate(Type, string)constructor now throwsArgumentExceptionwhen no method binds, asDelegate(object, string)does. It ignored the bind result, so skipping static abstract methods would have left the delegate unchanged for them, where 10.0 bound one and the call threwEntryPointNotFoundException. Only reflection reaches this constructor, on an existing delegate: a delegate type's own constructor is runtime implemented.RuntimeNamedMethodInfo.GetUncachedMethodInvokerreturns aStaticAbstractMethodInvoker, unless the method is open generic. It does so before the invoker lookup rather than beside the open generic checks inExecutionEnvironment.GetMethodInvoker, which for a property accessor is passed thePropertyInfoas the member its exceptions name, not the accessor. The invoker's members throwMemberAccessException, exceptLdFtnResult, behindGetFunctionPointer; its spanInvokechecks the argument count first. The by-name lookup inReflectionAugmentsskips static abstract methods. The compiler keeps them out of the invoke map (ShouldMethodBeInInvokeMap), so they no longer get an entry without an entry point.IsMethodSupportedInReflectionInvokeis unchanged: it also drives dependency and dataflow analysis, and excluding these methods there fails twoILCompiler.Trimming.Testscases that Fix COMDelegate::BindToMethod with static virtuals #125875 updated.ReflectionVirtualInvokeMapNodenow callsShouldMethodBeInInvokeMapinstead of repeating its checks.Type.GetInterfaceMap, which doesn't support these methods on NativeAOT (Type.GetInterfaceMap doesn't work for all cases #89157), now checks for them before its cast toVirtualMethodInvoker.RuntimeMethodInfochange is shared code, so it also reaches Mono'sInvoke,MethodInvoker.Invoke,SetValue, andGetValuewhere it falls back toInvoke. There is no Mono-only change: Mono's delegate-basedGetValueshortcut,CreateDelegateandGetFunctionPointerare unchanged.Breaking change
A behavioural change, in .NET 12 (
main), for code that reflects over static abstract methods, which have no implementation to call, for the protectedDelegate(Type, string)constructor, and on CoreCLR for invoking an abstract method that returnsrefto a ref struct:MemberAccessExceptionfrom the reflection call, instead of failing later or with another exception. On CoreCLR,CreateDelegatewith a delegate type that doesn't match failed to bind before (ArgumentException, ornull).Delegate.CreateDelegate(Type, Type, string, ...)fails to bind instead of returning a delegate that fails when called (CoreCLR) or throwingNotSupportedException(NativeAOT 10.0).GetFunctionPointeron a method with no unbound generic parameters throwsPlatformNotSupportedExceptioninstead of 10.0'sNotSupportedException.ArgumentExceptionwhen no method binds, for any name, instead of leaving the delegate unchanged.refto a ref struct throwsNotSupportedException, as a method with a body does, instead ofMemberAccessException.I can't add labels: could this PR get the
breaking-changelabel? I'll open the dotnet/docs issue once it merges.Not changed
int: CoreCLR doesn't bind it (ArgumentException, ornullwiththrowOnBindFailure: false); NativeAOT binds it, and the call returned a different value each run instead of the closed42, on 10.0 and on this branch. With this PR, both throwMemberAccessExceptionfor a static abstract method there.Delegate.CreateDelegate(Type, Type, string, ignoreCase: true)on a type with two static methods whose names differ only in case: CoreCLR binds one and NativeAOT throwsAmbiguousMatchException, on 10.0 and on this branch. With a static abstractMand a static virtualm, the delegate CoreCLR binds now callsm(on 10.0 the call threwEntryPointNotFoundException); NativeAOT still throwsAmbiguousMatchException.MethodInvoker.Invoke(object, Span<object>)with too many or too few arguments for a method of an open generic type or a generic method definition, static or instance, abstract or not: CoreCLR throwsTargetParameterCountExceptionand NativeAOTInvalidOperationException, on 10.0 and on this branch. The fixed-arity overloads with no arguments and with two throwTargetParameterCountExceptionthere on both.I can open separate issues for these.
Tests
Span<int>andref Span<int>returns, closed delegates, a delegate type that doesn't match, a by-name lookup whose candidates differ only in case (both declaration orders), wrong argument counts and open generics, with a static virtual method with a body as the control and an abstract instance method besideGetFunctionPointer.InvokeRefReturnchecks the abstractrefreturn change on an abstract class method and an interface method. They are inSystem.Reflection.Tests(MethodCommonTests,MethodInfoTests,MethodInvokerTests,PropertyInfoTests) andSystem.Runtime.Tests(DelegateTests,InvokeRefReturn,TypeTests). The delegate, function pointer andGetValuetests skip Mono, whose code for them is unchanged; the protected constructor and case-insensitive by-name tests are CoreCLR only, theGetInterfaceMaptest NativeAOT only.MemberAccessException, argument-count, by-name and protected constructor tests fail, and the closed static virtual delegate tests hit theIsVtableMethod()assert on a Checked CoreCLR.System.Reflection.TestsandSystem.Runtime.Testspass on CoreCLR and compiled with NativeAOT, and the threeSystem.Reflection.Emittest projects on CoreCLR.ILCompiler.Trimming.Testspasses with the compiler change, and CI's trim check (-subset clr.corelib+libs -c Release /p:RunAnalyzersInBuild=true /p:RunILLinkInBuild=true) passes on Windows x64.Benchmark
Ns per call, Mean ± StdDev, for the CoreCLR delegate paths that gain a check and for
MethodInfo.Invoke, whose check is part of the invocation flags (computed once, cached):MethodInfo.CreateDelegate, static methodMethodInfo.CreateDelegate, static method of a static classMethodInfo.CreateDelegate, static method, closedMethodInfo.CreateDelegate, instance method, closedDelegate.CreateDelegate(Type, Type, string), the last of twenty static methods (found first)Delegate.CreateDelegate(Type, Type, string), the first of twenty static methods (found last)DynamicMethod.CreateDelegateMethodInfo.Invoke, static methodThe new checks test
IsStatic()and the declaring type'sIsAbstract(), both cached, before the method's attributes, which come from metadata: a static method of a type that isn't abstract reads none, while a static method of an abstract type, a C# static class included, reads its attributes. The closed static case also saves one metadata read: the closed-delegate branch ofBindToMethodnow testsIsStatic(), a cached bit, beforeIsVirtual(), which reads the method's attributes.The
DynamicMethod.CreateDelegateandMethodInfo.Invokerows differ between base and PR although neither reads anything new; the cause is not known.Resolves #135565
Resolves #101320
Note
AI-generated, written at my direction and reviewed by me before posting. The behaviour before this PR was observed on NativeAOT 11.0 RC1 and 10.0, and on CoreCLR 10.0 and local builds of
mainand of this branch before the closed-delegate change. The tests ran on a local Windows x64 Checked build of this branch;System.Runtime.Testsran with #135371's test revert applied, as the test it reverts fails discovery on this branch's base. The "Not changed" behaviour was observed with a local program on 10.0 and on this branch, on CoreCLR and NativeAOT. The benchmark ran in process with BenchmarkDotNet 0.16.0-preview.2, 15 iterations, on local all-Release Windows x64 builds of this branch with and without its CoreCLR runtime changes, in the order base, PR, PR, base. Mono was not run locally.