-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add reflection introspection support for function pointers #71516
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/area-system-reflection Issue DetailsDetecting Mono errors etc
|
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs
Outdated
Show resolved
Hide resolved
src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.FunctionPointer.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Type.FunctionPointer.cs
Outdated
Show resolved
Hide resolved
2528f1f
to
66f942e
Compare
Added When you commit this breaking change:
Tagging @dotnet/compat for awareness of the breaking change. |
@MichalStrehovsky thoughts on arm64 NativeAOT CI failure? Looks like this path: runtime/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs Line 531 in f54c15a
Line 18 in 10288c9
Lines 127 to 130 in 10288c9
|
We're not supposed to get to the ConstructedEEType path from ReflectableFieldNode for function pointers. There's a very explicit check for that case here. Lines 86 to 89 in f54c15a
Similar thing was reported in #71063 but I never got a repro I could look at. Happy we now have a repro. I'll take a look and report back with a fix. |
For now, run in release build (not checked or debug). I'm working on local changes that resolve the contract issues. |
src/libraries/System.Private.CoreLib/src/System/Reflection/FunctionPointerInfo.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
// Normalize the calling conventions. | ||
if (!foundCallingConvention) |
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.
This should be the other way around. sigCallingConvention
should be checked first and the modopts should be read only for SignatureCallingConvention.Unmanaged
.
That's how it is done in the runtime and Roslyn:
runtime/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Lines 1642 to 1650 in 527f1d1
case MethodSignatureFlags.UnmanagedCallingConvention: | |
if (TryGetUnmanagedCallingConventionFromModOpt(signature, out CorInfoCallConvExtension callConvMaybe, out suppressGCTransition)) | |
{ | |
return callConvMaybe; | |
} | |
else | |
{ | |
return (CorInfoCallConvExtension)PlatformDefaultUnmanagedCallingConvention(); | |
} |
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.
In addition to normalizing the value from GetFunctionPointerCallingConventions
, this also "normalizes" the value from GetOptionalCustomModifiers()
, meaning for example:
- If
sigCallingConvention
is StdCall (and thus no StdCall mod opt) then we add the corresponding Stdcall modopt. - If
sigCallingConvention
is Unmanaged and there is a modopt for StdCall, then we leave as-is.
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.
This normalization can still be done even when sigCallingConvention
is checked first.
@@ -2953,13 +2955,36 @@ TypeHandle ClassLoader::CreateTypeHandleForTypeKey(TypeKey* pKey, AllocMemTracke | |||
else if (pKey->GetKind() == ELEMENT_TYPE_FNPTR) | |||
{ | |||
Module *pLoaderModule = ComputeLoaderModule(pKey); |
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.
ComputeLoaderModule
needs to be modified to take the custommods into account to handle the case where they are defined in unloaded assembly.
Also, we need to call EnsureInstantiation
or something like that to capture the dependencies.
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 think 'll hold off on this until we decide if we want to go with a restricted approach such as option (2) or (3) above since it wouldn't be needed assuming all calling conventions are in the runtime (e.g. System.Private.CoreLib) we don't have to worry about it being unloaded.
I have been thinking about this over the weekend:
So my vote would be to go with option (3). If we get requests to expose full modop details or the full calling conventions details, we would add the |
@jkotas thanks for this and @MichalStrehovsky thanks for bringing up the concerns with modifiers. We don't want to introduce something that causes issues elsewhere including managed C++. I'll likely close this PR and re-schedule for V8 since we are locking down for V7 features. |
The reason why I was leaning towards 2 is that the problem with calling convention modopts is self-inflicted - because we wanted to prevent a structural breaking change to the file format, we decided to stash this additional metadata wherever possible. If we allowed ourselves to change the format, this would likely not be a custom modifier because it has a different role. Going with option 3 will have a slight negative consequence that castability will work differently between calling conventions that can be represented the "old way" (in flags) vs calling conventions that are represented the "new way". object arr1 = new delegate* unmanaged<int, int>[1];
// Throws
var arr2 = (delegate* unmanaged[Cdecl]<int, int>[])arr1;
// Works
var arr3 = (delegate* unmanaged[MemberFunction]<int, int>[])arr1; But I can get behind option 3 too. It's how it works right now. It's a bit of a hole. |
For option 2 I assume we'd have to cherry-pick all of the mod opts we want to include. I thought it would be as simple as the CallConv* ones but Jan also mentioned |
IsCopyConstructed is applied to individual arguments (you cannot do that in C# today). |
Note that there is option 3a which proposes just managed vs. unmanaged and not the "CallConv" byte so it wouldn't have this inconsistency. I suppose we could add a new option 3b would be to just not use the "CallConv" byte at all. Either 3a or 3b should work in the runtime per discussion with @jkotas since the runtime doesn't really care about calling conventions except when invoking via However, I now wonder if option 2 has an advantage for future APIs, such as for a new IL Emit Note I did update the proposed API to include new "Signature" APIs that would allow all modifiers to be obtained that would be useful\compatible with either option 2 or 3*. |
Not including the calling convention sounds like a good option to me too. It feels cleaner than including only some of the calling conventions. It also eliminates potential class of question marks - |
The Signature APIs need to be able to deconstruct the types. For example, if you have an array of const pointers, you need to be able to lookup the const modopt on the array element. The const modopt won't be on the array itself. I think the needs to be something like: namespace System
{
public abstract class Type
{
// Throws InvalidOperationException if IsFunctionPointer is false.
+ public virtual Type GetFunctionPointerReturnType();
// Throws InvalidOperationException if IsFunctionPointer is false.
+ public virtual Type[] GetFunctionPointerParameterTypes();
// Note that IsPointer returns 'false' for function pointers.
public virtual bool IsPointer { get; }
+ public virtual bool IsFunctionPointer { get; }
+ public virtual bool IsUnmanagedFunctionPointer { get; }
public virtual Type[] GetOptionalCustomModifiers();
+ // GetRequiredCustomModifiers, GetOptionalCustomModifiers and GetCallingConventions work on signature types only.
+ // These APIs will throw InvalidOperationException on regular runtime types.
+ public virtual Type[] GetRequiredCustomModifiers();
+ public virtual Type[] GetOptionalCustomModifiers();
+ // This returns a subset of GetFunctionPointerReturnType().GetOptionalCustomModifiers() that start with the CallConv prefix
+ public virtual Type[] GetCallingConventions();
}
}
// Extensions in the MetadataLoadContext assembly only; we could add directly to the appropriate types instead
// if we also want the runtime to implement these as well (pending feedback).
+ public static class MetadataLoadContextExtensions
+ {
+ public static Type GetSignatureType(this FieldInfo fieldInfo);
+ public static Type GetSignatureType(this PropertyInfo propertyInfo);
// For MethodInfo, ContructorInfo and PropertyInfo with indexers
+ public static Type GetSignatureType(this ParameterInfo parameterInfo);
+ }
} |
Closing; we need to update the API and design based on feedback. |
See design at dotnet/designs#282 |
Function pointers are now supported via reflection introspection in both the runtime (CoreClr) and MetadataLoadContext. This closes a reflection introspection functionality gap that has existed since the beginning of .NET but has become more prevalent since .NET 5.0 with the C# support for function pointers. It is now possible to fully enumerate function pointers parameters and the return value to find all of the referenced types as well as inspect calling conventions.
Fixes #69273
Fixes #43791
Design\implementation notes:
System.Type
class through additional members includingbool IsFunctionPointer {get}
.Type.ToString()
returns a friendly representation of the function pointer but does not include calling conventions.Type.Name
returns*()
Type.FullName
andType.AssemblyQualifiedName
returnnull
. In V8 we can support a non-null version based on feedback; that would require a type name grammar update including custom modifiers as well as support forType.GetType()
,Assembly.GetType()
and any friend APIs that would need to parse that string and construct a function pointer.null
in open generic parameters cases as well.Type.IsPointer
returnsfalse
as to prevent another breaking change and because function pointers are a different type in the CLR and in ECMA 335.GetFunctionPointerCallingConventions()
which returns the correspondingSystem.Runtime.CompilerServices.CallConv*
types. This makes consuming the APIs easier and less error-prone that exposing both the raw "CallKind" enum along with the newer mod opts on the return parameter, since there can be more than one way to encode a calling convention by using those.Todo: