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

Delegate.CreateDelegate is not trimming safe #46857

Closed
MichalStrehovsky opened this issue Jan 12, 2021 · 5 comments · Fixed by #47017
Closed

Delegate.CreateDelegate is not trimming safe #46857

MichalStrehovsky opened this issue Jan 12, 2021 · 5 comments · Fixed by #47017
Assignees
Labels
area-System.Reflection linkable-framework Issues associated with delivering a linker friendly framework
Milestone

Comments

@MichalStrehovsky
Copy link
Member

#45984 flipped public static Delegate? CreateDelegate(Type, Type, string, bool, bool) to being trimming safe, but from how I understand things, it isn't:

using System;

Delegate.CreateDelegate(typeof(Action), typeof(Derived), "Method").DynamicInvoke();

class Base
{
    private static void Method() => Console.WriteLine("Hello");
}

class Derived : Base { }

Linker can remove Base.Method since private methods declared on base types don't count into NonPublicMethods of the declaring type:

https://github.com/mono/linker/blob/7a095faf3925689f2a3450da1a76fdd79a070de9/src/linker/Linker.Dataflow/DynamicallyAccessedMembersBinder.cs#L125-L128

I've noticed this in NativeAOT since NativeAOT implements reflection stack in C# and annotations were not matching up.

Cc @vitek-karas @eerhardt

@MichalStrehovsky MichalStrehovsky added area-System.Reflection linkable-framework Issues associated with delivering a linker friendly framework labels Jan 12, 2021
@ghost
Copy link

ghost commented Jan 12, 2021

Tagging subscribers to 'linkable-framework': @eerhardt, @vitek-karas, @LakshanF
See info in area-owners.md if you want to be subscribed.

Issue Details

#45984 flipped public static Delegate? CreateDelegate(Type, Type, string, bool, bool) to being trimming safe, but from how I understand things, it isn't:

using System;

Delegate.CreateDelegate(typeof(Action), typeof(Derived), "Method").DynamicInvoke();

class Base
{
    private static void Method() => Console.WriteLine("Hello");
}

class Derived : Base { }

Linker can remove Base.Method since private methods declared on base types don't count into NonPublicMethods of the declaring type:

https://github.com/mono/linker/blob/7a095faf3925689f2a3450da1a76fdd79a070de9/src/linker/Linker.Dataflow/DynamicallyAccessedMembersBinder.cs#L125-L128

I've noticed this in NativeAOT since NativeAOT implements reflection stack in C# and annotations were not matching up.

Cc @vitek-karas @eerhardt

Author: MichalStrehovsky
Assignees: -
Labels:

area-System.Reflection, linkable-framework

Milestone: -

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jan 12, 2021
@eerhardt
Copy link
Member

Is this fix here to change

public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(AllMethods)] Type target, string method) => CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!;
public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(AllMethods)] Type target, string method, bool ignoreCase) => CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!;

to

        public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type target, string method) => CreateDelegate(type, target, method, ignoreCase: false, throwOnBindFailure: true)!;
        public static Delegate CreateDelegate(Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type target, string method, bool ignoreCase) => CreateDelegate(type, target, method, ignoreCase, throwOnBindFailure: true)!;

This will preserve everything in the Type hierarchy, even Base.Method above.

@eerhardt eerhardt self-assigned this Jan 12, 2021
@eerhardt eerhardt removed the untriaged New issue has not been triaged by the area owner label Jan 12, 2021
@eerhardt eerhardt added this to the 6.0.0 milestone Jan 12, 2021
@MichalStrehovsky
Copy link
Member Author

I'll let Vitek speak to how All operates. In my head All should be a union of all the flags because that's how it's easy to reason about but I know linker does All differently - that behavior is accidental and undocumented as far as I'm concerned. AFAIK it keeps more than necessary because we don't rely on that definition of All in the class libraries. This would be the first spot that relies on that behavior.

I would just keep them as RequiresUnreferencedCode. People can still use the overload that takes MethodInfo. If we get feedback that it's unacceptable, we can look at our options again:

  • do .All and document the way linker does it, or
  • add a new MemberTypes to the enum, or
  • intrinsically recognize the API in illink and make it safe if the parameters are known.

@vitek-karas
Copy link
Member

The special All behavior is intentional - it was done in reaction to discussion in the API review. I think the main point was that we have a certain set of values on the DynamicallyAccessedMemberTypes which cover some portion of the "things which can be preserved" but not all. If we were to implement All as a simple union of everything it would need to change its behavior if/when we added more things to the enum.

The other argument was basically along the lines of this issue - that the other enum values do not cover everything (which was also intentional - we modeled them by BindingFlags both in naming and in functionality) and there was a desire to have one value which would simply preserve the whole type - no exceptions.

At least that's how I remember it - I can't find it in the API review issue though...

@eerhardt
Copy link
Member

Given the axiom stated in https://github.com/dotnet/designs/blob/main/accepted/2020/linking-libraries.md

An application that works is preferred over a smaller application that doesn't.

My preference is to annotate the Type as All rather than marking it as RequiresUnreferencedCode. Developers don't see ILLink warnings by default today, so I'd rather be more conservative and ensure an app using this API still works. People can still use the overload that takes MethodInfo, if they decide the app is too large and this is causing size issues.

I can't find it in the API review issue though...

Here's where we discussed All on the video
https://www.youtube.com/watch?t=2900&v=1GkjoDSR7dE&feature=youtu.be

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jan 14, 2021
eerhardt added a commit to eerhardt/runtime that referenced this issue Jan 14, 2021
The underlying code will find non-public methods on base types, but the current annotation won't preserve these methods. Use All annotation on the Type to ensure they are preserved.

Fix dotnet#46857
eerhardt added a commit that referenced this issue Jan 20, 2021
* Make Delegate.CreateDelegate trimming safe

The underlying code will find non-public methods on base types, but the current annotation won't preserve these methods. Use All annotation on the Type to ensure they are preserved.

Fix #46857

* Add a trimming test
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jan 20, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Feb 19, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Reflection linkable-framework Issues associated with delivering a linker friendly framework
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants