dlemstra / Magick.NET

The .NET library for ImageMagick
Apache License 2.0
3.48k stars 415 forks source link

Workaround single trimming warning. #1499

Open artyomszasa opened 11 months ago

artyomszasa commented 11 months ago

Is your feature request related to a problem? Please describe

When using Trimming and/or AOT while targeting .NET8 Magick.NET.Core emits single warning:

/_/src/Shared/TypeHelper.cs(20): Trim analysis warning IL2075: ImageMagick.TypeHelper.GetCustomAttributes<T>(Enum): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields' in call to 'System.Type.GetField(String)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

IMHO (see alternatives) it is not critical yet workarounding this would make the warning go away making "using Magick.NET" experience event greater.

Describe the solution you'd like

Looking at the code (https://github.com/dlemstra/Magick.NET/blob/main/src/Shared/TypeHelper.cs) IMHO it can be easily avoided by adding DynamicallyAccessedMembersAttribute to the type parameter. I am aware Magick.NET is targeting nestandard thus built-in attributes are not available, yet it can be workarounded by adding own internal attribute(s) that are sematically equal to the ones the compiler is using:

namespace System.Diagnostics.CodeAnalysis
{
    //
    // Summary:
    //     Specifies the types of members that are dynamically accessed. This enumeration
    //     has a System.FlagsAttribute attribute that allows a bitwise combination of its
    //     member values.
    [Flags]
    internal enum DynamicallyAccessedMemberTypes
    {
        //
        // Summary:
        //     Specifies all members.
        All = -1,
        //
        // Summary:
        //     Specifies no members.
        None = 0,
        //
        // Summary:
        //     Specifies the default, parameterless public constructor.
        PublicParameterlessConstructor = 1,
        //
        // Summary:
        //     Specifies all public constructors.
        PublicConstructors = 3,
        //
        // Summary:
        //     Specifies all non-public constructors.
        NonPublicConstructors = 4,
        //
        // Summary:
        //     Specifies all public methods.
        PublicMethods = 8,
        //
        // Summary:
        //     Specifies all non-public methods.
        NonPublicMethods = 16,
        //
        // Summary:
        //     Specifies all public fields.
        PublicFields = 32,
        //
        // Summary:
        //     Specifies all non-public fields.
        NonPublicFields = 64,
        //
        // Summary:
        //     Specifies all public nested types.
        PublicNestedTypes = 128,
        //
        // Summary:
        //     Specifies all non-public nested types.
        NonPublicNestedTypes = 256,
        //
        // Summary:
        //     Specifies all public properties.
        PublicProperties = 512,
        //
        // Summary:
        //     Specifies all non-public properties.
        NonPublicProperties = 1024,
        //
        // Summary:
        //     Specifies all public events.
        PublicEvents = 2048,
        //
        // Summary:
        //     Specifies all non-public events.
        NonPublicEvents = 4096,
        //
        // Summary:
        //     Specifies all interfaces implemented by the type.
        Interfaces = 8192
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false)]
    internal sealed class DynamicallyAccessedMembersAttribute : Attribute
    {
        public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
        {
            MemberTypes = memberTypes;
        }

        public DynamicallyAccessedMemberTypes MemberTypes { get; }
    }
}

Describe alternatives you've considered

As I can see from code the affected member is used almost only here: https://github.com/dlemstra/Magick.NET/blob/main/src/Magick.NET.Core/Profiles/Exif/Values/ExifValue%7BTValueType%7D.cs#L59

So adding dynamic dependency to the internal attribute (using stringified singnature) makes trimming/aot perfectly safe, yet the warning persists making it impossible to achive warningless AOT build.

Additional context

Trimming and AOT are IMHO great features of the .NET and I think (and hope) it will be pushed by Microsoft in all further versions of the .NET. A lot of libraries are not Trim/AOT compatible (yet?) but the Magick.NET actually is compatible out-of-box, yet the single warning making the whole library is reported to be not compatible with Trimming/AOT...

dlemstra commented 11 months ago

Marking the library as trimmable would require me to target at least net6. I think I will switch from netstandard21 to net6 in the next major release of this library and keep that target framework up to date with the oldest LTS version of dotnet.

But I think that this part of the code should be generated with a source code generator to avoid me using reflection. Once I target net6 I will check what needs to be done to make the library trimmable.

dlemstra commented 2 months ago

I just published version 14.0.0 that targets dotnet 8.0 and uses source generators. The GetCustomAttribute method is still used but not in the spot that you mentioned (next time please use a permalink). Can you check if trimming and AOT now works? And if it doesn't then please describe how I can test that myself?

Ruben2776 commented 2 months ago

When compiling my project using AOT and trimming, Magick.Native-Q8-OpenMP-x64.dll will be in the output directory, rather than being trimmed. I've tried with Magick.NET-Q8-AnyCPU and it produces Magick.Native-Q8-x64.dll to the output folder on an x64 release. It seems to me that you're producing specific builds depending on CPU architecture? Perhaps that is why trimming doesn't work?

I've noticed that in your .csproj file, there's no mention of trimming support. To enable trimming support, you need to set IsTrimmable to true, E.g.:

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming?pivots=dotnet-8-0

dlemstra commented 2 months ago

I enabled <EnableTrimAnalyzer>true</EnableTrimAnalyzer> in the .props file of the source folder. The docs are also not clear on what you should do for a library.