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

Respecting EnumMemberAttribute in AOT'd applications #97737

Closed
eerhardt opened this issue Jan 31, 2024 · 6 comments · Fixed by #100814 or #105351
Closed

Respecting EnumMemberAttribute in AOT'd applications #97737

eerhardt opened this issue Jan 31, 2024 · 6 comments · Fixed by #100814 or #105351
Labels
area-NativeAOT-coreclr in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@eerhardt
Copy link
Member

I'd like to NativeAOT compile a library that respects the EnumMemberAttribute. This attribute can be applied to enum fields to rename what the field name is used when serializing values of an enum.

The issue is, I can't statically know all the enum types that will be passed into my code. So I need to write code like this:

https://github.com/microsoft/kiota-abstractions-dotnet/blob/02afdff08829667ad7411e87b9dfa797809243a0/src/RequestInformation.cs#L187-L210

        private static object ReplaceEnumValueByStringRepresentation(object source)
        {
            if(source is Enum enumValue && GetEnumName(enumValue) is string enumValueName)
            {
                return enumValueName;
            }
            
            return source;
        }
        private static string? GetEnumName<T>(T value) where T : Enum
        {
            var type = value.GetType();

            if(Enum.GetName(type, value) is not { } name)
                throw new ArgumentException($"Invalid Enum value {value} for enum of type {type}");

            if(type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>() is { } attribute)
                return attribute.Value;

            return name.ToFirstCharacterLowerCase();

But I am getting a warning on value.GetType() and using it to call type.GetField(name).

In talking with @MichalStrehovsky and @agocke, one recommendation is that since enum fields cannot be trimmed, we can suppress this warning.

We should document this so people can justify suppressing this warning.

Additionally, it would be great if the analysis tooling understood that since T : Enum, and we are calling GetField on a System.Type that is guaranteed to be an enum, we shouldn't warn.

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jan 31, 2024
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jan 31, 2024
@eerhardt eerhardt added area-NativeAOT-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jan 31, 2024
@ghost
Copy link

ghost commented Jan 31, 2024

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

Issue Details

I'd like to NativeAOT compile a library that respects the EnumMemberAttribute. This attribute can be applied to enum fields to rename what the field name is used when serializing values of an enum.

The issue is, I can't statically know all the enum types that will be passed into my code. So I need to write code like this:

https://github.com/microsoft/kiota-abstractions-dotnet/blob/02afdff08829667ad7411e87b9dfa797809243a0/src/RequestInformation.cs#L187-L210

        private static object ReplaceEnumValueByStringRepresentation(object source)
        {
            if(source is Enum enumValue && GetEnumName(enumValue) is string enumValueName)
            {
                return enumValueName;
            }
            
            return source;
        }
        private static string? GetEnumName<T>(T value) where T : Enum
        {
            var type = value.GetType();

            if(Enum.GetName(type, value) is not { } name)
                throw new ArgumentException($"Invalid Enum value {value} for enum of type {type}");

            if(type.GetField(name)?.GetCustomAttribute<EnumMemberAttribute>() is { } attribute)
                return attribute.Value;

            return name.ToFirstCharacterLowerCase();

But I am getting a warning on value.GetType() and using it to call type.GetField(name).

In talking with @MichalStrehovsky and @agocke, one recommendation is that since enum fields cannot be trimmed, we can suppress this warning.

We should document this so people can justify suppressing this warning.

Additionally, it would be great if the analysis tooling understood that since T : Enum, and we are calling GetField on a System.Type that is guaranteed to be an enum, we shouldn't warn.

Author: eerhardt
Assignees: -
Labels:

untriaged, area-NativeAOT-coreclr

Milestone: -

@teo-tsirpanis
Copy link
Contributor

I think that instead of type.GetValue() it should be using typeof(T). There is also a generic overload of Enum.GetName in modern frameworks.

@eerhardt
Copy link
Member Author

I think that instead of type.GetValue() it should be using typeof(T). There is also a generic overload of Enum.GetName in modern frameworks.

Initially I agreed, until I saw the code in the top method:

        private static object ReplaceEnumValueByStringRepresentation(object source)
        {
            if(source is Enum enumValue && GetEnumName(enumValue) is string enumValueName)

here is a call to GetEnumName<System.Enum>(enumValue). So when this call happens, typeof(T) == typeof(System.Enum), which doesn't have the field, nor will it work with Enum.GetName.

The issue is, I can't statically know all the enum types that will be passed into my code.

@agocke agocke added this to the 9.0.0 milestone Jan 31, 2024
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jan 31, 2024
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this issue Apr 9, 2024
Fixes dotnet#97737.

Trimming ensures we keep all the public fields on enums. `typeof(T).GetFields()` is safe when the `T` is constrained to be `System.Enum` or a derived type.
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Apr 9, 2024
MichalStrehovsky added a commit that referenced this issue Apr 9, 2024
Fixes #97737.

Trimming ensures we keep all the public fields on enums. `typeof(T).GetFields()` is safe when the `T` is constrained to be `System.Enum` or a derived type.
matouskozak pushed a commit to matouskozak/runtime that referenced this issue Apr 30, 2024
…#100814)

Fixes dotnet#97737.

Trimming ensures we keep all the public fields on enums. `typeof(T).GetFields()` is safe when the `T` is constrained to be `System.Enum` or a derived type.
@github-actions github-actions bot locked and limited conversation to collaborators May 10, 2024
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this issue May 30, 2024
…#100814)

Fixes dotnet#97737.

Trimming ensures we keep all the public fields on enums. `typeof(T).GetFields()` is safe when the `T` is constrained to be `System.Enum` or a derived type.
@MichalStrehovsky
Copy link
Member

Reopening, we also need to handle the var type = value.GetType().GetField(...); case.

@MichalStrehovsky MichalStrehovsky removed the in-pr There is an active PR which will close this issue when it is merged label Jul 17, 2024
@MichalStrehovsky
Copy link
Member

@sbomer, I reopened this because the original use case was actually calling out for a variation of what was fixed. The same variation was also hit in microsoft/OpenAPI.NET#1717 (comment). I think we'd want to fix this, but the fix doesn't look exactly trivial on the ILLinker side.

The native AOT side is probably just:

diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/HandleCallAction.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/HandleCallAction.cs
index 74fe05d9bd8..fcb9695a485 100644
--- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/HandleCallAction.cs
+++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/HandleCallAction.cs
@@ -15,6 +15,7 @@
 using DependencyList = ILCompiler.DependencyAnalysisFramework.DependencyNodeCore<ILCompiler.DependencyAnalysis.NodeFactory>.DependencyList;
 using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;
 using WellKnownType = ILLink.Shared.TypeSystemProxy.WellKnownType;
+using System.Diagnostics.CodeAnalysis;
 
 #nullable enable
 
@@ -392,8 +393,29 @@ private partial bool TryHandleIntrinsic (
                             TypeDesc? staticType = (valueNode as IValueWithStaticType)?.StaticType?.Type;
                             if (staticType is null || (!staticType.IsDefType && !staticType.IsArray))
                             {
-                                // We don't know anything about the type GetType was called on. Track this as a usual "result of a method call without any annotations"
-                                AddReturnValue(_reflectionMarker.Annotations.GetMethodReturnValue(calledMethod, _isNewObj));
+                                DynamicallyAccessedMemberTypes annotation = default;
+                                if (staticType is { IsSignatureVariable: true })
+                                {
+                                    var genericParam = (GenericParameterDesc)staticType.InstantiateSignature(_callingMethod.OwningType.Instantiation, _callingMethod.Instantiation);
+                                    foreach (TypeDesc constraint in genericParam.TypeConstraints)
+                                    {
+                                        if (constraint.IsWellKnownType(Internal.TypeSystem.WellKnownType.Enum))
+                                        {
+                                            annotation = DynamicallyAccessedMemberTypes.PublicFields;
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                if (annotation != default)
+                                {
+                                    AddReturnValue(_reflectionMarker.Annotations.GetMethodReturnValue(calledMethod, _isNewObj, annotation));
+                                }
+                                else
+                                {
+                                    // We don't know anything about the type GetType was called on. Track this as a usual "result of a method call without any annotations"
+                                    AddReturnValue(_reflectionMarker.Annotations.GetMethodReturnValue(calledMethod, _isNewObj));
+                                }
                             }
                             else if (staticType.IsSealed() || staticType.IsTypeOf("System", "Delegate"))
                             {

However, on ILLinker side this is running into the following big comment:

// Note that valueNode can be statically typed in IL as some generic argument type.
// For example:
// void Method<T>(T instance) { instance.GetType().... }
// Currently this case will end up with null StaticType - since there's no typedef for the generic argument type.
// But it could be that T is annotated with for example PublicMethods:
// void Method<[DAM(PublicMethods)] T>(T instance) { instance.GetType().GetMethod("Test"); }
// In this case it's in theory possible to handle it, by treating the T basically as a base class
// for the actual type of "instance". But the analysis for this would be pretty complicated (as the marking
// has to happen on the callsite, which doesn't know that GetType() will be used...).
// For now we're intentionally ignoring this case - it will produce a warning.
// The counter example is:
// Method<Base>(new Derived);
// In this case to get correct results, trimmer would have to mark all public methods on Derived. Which
// currently it won't do.

I.e. the StaticType is null and cannot be used (it is a TypeDefinition in Cecil, so cannot be a generic parameter). Do you have some idea of how we could proceed here?

@sbomer
Copy link
Member

sbomer commented Jul 18, 2024

It looks like we could change StaticType to be a TypeReference instead - seems like that would better match what TypeDesc can represent in ILC. I'll give this a try.

@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Jul 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-NativeAOT-coreclr in-pr There is an active PR which will close this issue when it is merged
Projects
Archived in project
5 participants