Skip to content

[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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

VSadov
Copy link
Member

@VSadov VSadov commented Jul 24, 2025

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:

  • getting an async variant by name: not possible.
    Query by name skips async variants, returns actual methods in IL. This was the case even before this change.
  • getting "real" infrastructure methods like AsyncHelpers.Await via reflection: possible.
  • invoking infrastructure methods like AsyncHelpers.Await via MethodInfo Invoke: throws
    The message says: "Async infrastructure methods are not supported in reflection invocation."
  • Getting an async variant by slot index: not possible now.
    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.
  • dynamic IL as in regular Reflection.Emit case (not DynamicMethod case):
  • dynamic Task-returning methods can be called/awaited in async methods
  • dynamic callers can call Task-returning methods and are not confused by variants.
  • dynamic Task-returning methods decorated as 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)
  • dynamic IL as in DynamicMethod case:
  • ??

Copy link
Contributor

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

@@ -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")
Copy link
Member Author

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> -->
Copy link
Member Author

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

@VSadov VSadov requested a review from jkotas July 25, 2025 18:20
@VSadov VSadov marked this pull request as ready for review July 25, 2025 18:20
@Copilot Copilot AI review requested due to automatic review settings July 25, 2025 18:20
Copy link
Contributor

@Copilot Copilot AI left a 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()
Copy link
Member

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.

Copy link
Member

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())
Copy link
Member

@jkotas jkotas Jul 26, 2025

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.

Copy link
Member

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?
Copy link
Member

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.

Copy link
Member Author

@VSadov VSadov Jul 27, 2025

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());
}
Copy link
Member

@jkotas jkotas Jul 26, 2025

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RuntimeAsync] Dynamic methods (System.Reflection.Emit). [RuntimeAsync] Reflection/introspection support
2 participants