-
Notifications
You must be signed in to change notification settings - Fork 5.1k
[RuntimeAsync] Tweak some reflection scenarios around async methods. #118045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Tagging subscribers to this area: @mangod9 |
@@ -709,7 +709,7 @@ RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableRiscV64Zbb, W("EnableRiscV64 | |||
#endif | |||
|
|||
// Runtime-async | |||
RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 0, "Enables runtime async method support") | |||
RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 1, "Enables runtime async method support") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: this is to enable async tests. undo before merging
@@ -6,7 +6,7 @@ | |||
|
|||
<PropertyGroup> | |||
<!-- runtime async testing in main repo NYI --> | |||
<DisableProjectBuild>true</DisableProjectBuild> | |||
<!-- <DisableProjectBuild>true</DisableProjectBuild> --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: this is to enable async tests. undo before merging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR tweaks reflection behavior around async methods by ensuring async variant methods are not visible through reflection APIs. The changes implement a consistent approach where async variants "do not exist" in reflection since they're not present in IL, while making infrastructure helpers like AsyncHelpers.Await
visible but non-invokable through reflection.
Key changes:
- Async variant methods are filtered out from reflection queries and interface mappings
- Infrastructure async methods throw when invoked via reflection
- Tests are added to verify the new reflection behavior with async methods
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
src/tests/async/reflection/reflection-simple.csproj | Enables the test project by adding process isolation and priority settings |
src/tests/async/reflection/reflection-simple.cs | Adds comprehensive tests for async method reflection behavior including dynamic invocation, interface mapping, and dynamic IL scenarios |
src/tests/async/Directory.Build.targets | Uncomments the DisableProjectBuild property to enable async test execution |
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx | Adds error message for unsupported async infrastructure method invocation |
src/coreclr/vm/runtimehandles.cpp | Filters out async variant methods from RuntimeTypeHandle_GetMethodAt |
src/coreclr/vm/reflectioninvocation.cpp | Adds check to throw NotSupportedException when invoking async methods via reflection |
src/coreclr/inc/clrconfigvalues.h | Enables RuntimeAsync feature by default (changes default from 0 to 1) |
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs | Updates GetInterfaceMap to handle null method handles and resize arrays when async variants are filtered out |
} | ||
|
||
[Fact] | ||
public static void DynamicAsyncMethod() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two different scenarios: Reflection.Emit (DefineDynamicAssembly) and DynamicMehods (new DynamicMethod
). We should have coverage for both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for opening issue for DynamicMethods. I have commented on it: #118074 (comment) . There is nothing to test wrt DynamicMethods since the scenario is effectively blocked.
// Unless it is an async thunk. | ||
// A thunk cannot be overriden, bur shares methoddef with the real method, | ||
// which could be overriden, but we do not want that override. | ||
if (!IsAsyncThunkMethod()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not look like right that we check IsAsyncThunkMethod here to deal with reflection.emit case and in GetRVA to deal with non-reflection.emit case. Can the check from GetRVA()
be moved up so that it is in just one place?
I see checked for IsUnboxingStub
in BOOL HasILHeader()
. Should this mirror checks for unboxing stubs? Unboxing stubs have same problem - the real method IL should not be used for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have noticed that MethodDescGetRVA
method is unused. You can delete it while you are on it.
|
||
// Sadly the following does not throw and results in UB | ||
// We cannot completely prevent putting a token of an Async method into IL stream. | ||
// CONSIDER: perhaps JIT could throw? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the UB that this is results in currently?
If users can call these helpers in C#, we should make sure that JIT throw (or behaves deterministically) when it finds them being used in unexpected way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling an Await helper from a nonasync caller results effectively in a calling convention mismatch. There is an extra continuation return that caller does not know about. The commented scenario passes under debugger and crashes with nullref in normal execution (or the other way around, I do not remember).
Accessing Await helpers in C# will cause warnings due to RequiresPreviewFeatures
. We can teach Roslyn that only async callers can call these methods, but with reflection it is possible to refer to everything by name and get around warnings/errors.
I think JIT should treat calls to non-task-returning async methods (of which we have only Await helpers) in nonasync methods as invalid IL. We have legitimate "violations" in thunks that bridge sync/async boundaries, so it could be a bit tricky.
Loader only allows non-task returning async methods if they are in the corlib, so this is not some random set of methods. There are very few.
// the following should not crash | ||
del(Task.CompletedTask); | ||
del(FooTask()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more interesting scenario are UnsafeAccessor methods (https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute).
We may want to add a few tests to validate that UnsafeAccessors work fine for Task-returning methods with runtime async. I think it should just work, but it would not hurt to have a test case for it.
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Closes: #115099
Closes: #115101
This is a follow up that addresses a few concerns around reflection behavior with Async/Task-returning methods and adds tests.
The general theme for reflection is that async variant methods "do not exist". This is the least confusing approach, since these methods are not present in the IL. It also avoids issues with representation of the variants and their invokability.
There is one caveat around infrastructure helpers like
Await
/AwaitAwaiter
. These are visible in reflection, since they are present in the IL, but they are async methods.Ideally they should not be invokable form non-async methods and this change makes reflection invoke to throw for them.
NOTE: It is still possible to construct dynamic IL that calls the helpers from non-async caller. This is a fairly advanced scenario that is unlikely to happen by accident. It may be ok to leave this for v11.
In this change:
Query by name skips async variants, returns actual methods in IL. This was the case even before this change.
AsyncHelpers.Await
via reflection: possible.AsyncHelpers.Await
via MethodInfo Invoke: throwsThe message says: "Async infrastructure methods are not supported in reflection invocation."
Getting methods by a slot number is an internal API. There is observable impact on the
GetInterfaceMap
, which before this change could return duplicate mappings for Task-returning methods due to presence of other variants.MethodImpl.Async
- can await via Await pattern, can be called and can be awaited (TODO: need a test, like recursive await in a dynamic method, maybe)