Skip to content
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

Allow MakeGenericType/MakeArrayType of arbitrary types #112986

Merged
merged 3 commits into from
Mar 4, 2025

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Feb 27, 2025

When we worked on .NET Native, we didn't have static analysis but at least wanted to have some predictability in how dynamic code fails with AOT. We decided we want MakeGenericType & co. to be the thing that fails instead of subsequent operations that try to obtain a type handle (one needs a type handle to do "interesting" reflection operations).

As we refined the model and introduced static analysis, we decided it's fine if trim/AOT unsafe code can get at things that are in "undefined" state 1 (named types that might not have type handles, MethodInfos that don't have parameter names, etc.). The static analysis warning is always at the problematic operation and what happens next can be anything.

But MakeGenericType failing early sometimes got in the way. For example xUnit uses MakeGenericType to create a new interface at runtime and check IsAssignableFrom with it. We don't actually need a usable type handle for this. We had an opt-out switch that we only enabled for testing to get xUnit working. But that also means we were not testing the shipping configuration whenever xUnit was involved.

This PR makes it so MakeGeneric/MakeArray can actually succeed even if we don't have the code/data structures for the type. Trying to do "interesting" reflection with it is going to throw the good old MissingMetadata-like exception - the difference is just in the timing.

I'm also changing things to allow obtaining TypeHandle on generic types even if they're in the "necessary" form only. This will only bring us to parity with what already happens for non-generic types. We already have blocks in places to make sure such TypeHandle cannot be used for much more than casting (e.g. GetUninitializedObject has a preexisting block for this).

Cc @dotnet/ilc-contrib

Footnotes

  1. We say we're fine with undefined behavior for trim-unsafe code, but that does not include GC holes.

When we worked on .NET Native, we didn't have static analysis but at least wanted to have some predictability in how dynamic code fails with AOT. We decided we want `MakeGenericType` & co. to be the thing that fails instead of subsequent operations that try to obtain a type handle (one needs a type handle to do "interesting" reflection operations).

As we refined the model and introduced static analysis, we decided it's fine if trim/AOT unsafe code can get at things that are in "undefined" state (named types that might not have type handles, MethodInfos that don't have parameter names, etc.). The static analysis warning is always at the problematic operation and what happens next can be anything.

But `MakeGenericType` failing early sometimes got in the way. For example xUnit uses `MakeGenericType` to create a new interface at runtime and check `IsAssignableFrom` with it. We don't actually need a usable type handle for this. We had an opt-out switch that we only enabled for testing to get xUnit working. But that also means we were not testing the shipping configuration whenever xUnit was involved.

This PR makes it so `MakeGeneric`/`MakeArray` can actually succeed even if we don't have the code/data structures for the type. Trying to do "interesting" reflection with it is going to throw the good old MissingMetadata-like exception - the difference is just in the timing.

I'm also changing things to allow obtaining `TypeHandle` on generic types even if they're in the "necessary" form only. This will only bring us to parity with what already happens for non-generic types. We already have blocks in places to make sure such `TypeHandle` cannot be used for much more than casting (e.g. `GetUninitializedObject` has a preexisting block for this). We say we're fine with undefined behavior for trim-unsafe code, but that does not include GC holes.
Copy link
Contributor

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

I think the part about allowing this API to succeed always can be up to debate. For #111610 I'd only need the part where we can obtain a TypeHandle for a "necessary" generic type if one was generated by the compiler. We can currently only obtain such type handle for named non-generic types.

I just thought that if we're allowing a limited TypeHandle, we could just go all the way and also allow no TypeHandle at all.

I'm still a bit squeamish about these TypeHandles and wonder if we should do more to prevent them leaking out. For example RuntimeType.TypeHandle could still throw for them (use the same heuristic with counting the vtable slots) and we could have a private RuntimeType.TypeHandleMaybeNullMaybeUnconstructed for places where we're ready to deal with these. But not sure if it's worth it.

@MichalStrehovsky MichalStrehovsky marked this pull request as ready for review February 28, 2025 07:29
@jkotas
Copy link
Member

jkotas commented Mar 4, 2025

I think it is fine to be as lazy as possible when throwing missing metadata exceptions. Users who follow the guidance and pay attention to the static analysis should never see these exceptions.

Should the same change be made in MakeGenericMethod for parity?

MethodBaseInvoker _ = methodInfo.MethodInvoker; // For compatibility with other Make* apis, trigger any missing metadata exceptions now rather than later.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@MichalStrehovsky
Copy link
Member Author

Should the same change be made in MakeGenericMethod for parity?

Sounds reasonable. Turns out grabbing the method invoker early was also validating the MakeGenericMethod doesn't break constraints. So added a check for that.

(We do this check implicitly for MakeGenericType because we attempt to get a TypeHandle as part of System.Type interning logic.)

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.

2 participants