-
Notifications
You must be signed in to change notification settings - Fork 128
Annotate private methods kept because of reflection #506
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
Conversation
|
Depends on mono/mono#13588 |
|
How is this attribute different from the existing |
| Context.LogMessage ($"Duplicate preserve in {_xmlDocumentLocation} of {method.FullName}"); | ||
|
|
||
| Annotations.Mark (method); | ||
| Annotations.MarkReflected (method); |
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.
How does preserving something via xml necessarily mean that it is reflected?
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.
If a method is private and accessed via a method other than the visible number of callers in the assembly, the call happens through a method other than traditional C#-compiled function calls. This would require either managed reflection or an equivalent unmanaged reflection of the method through the metadata tables.
If the word "Reflected" portrays a false connotation that this runtime code reflection / invocation happens through the managed System.Reflection, I'm happy to take any suggestions to rename it to.
I wanted to clarify that it was more specific than just "ExternallyVisible" though. This won't be applied to public methods, only private ones that we know can be accessed.
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 was thinking of methods called from native code. It wasn't clear to me if you wanted to capture these as well.
I don't have a better naming suggestion at the moment.
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 wanted to clarify that it was more specific than just "ExternallyVisible" though. This won't be applied to public methods, only private ones that we know can be accessed.
Are you also going to prevent the non-reflectable methods from being invoked via reflection (or other indirect means) at runtime?
Otherwise, you can get very weird crashes when the IL linker guesses wrong and the optimizations takes advantage of invariant that does not hold.
We have a very similar attribute for .NET Native, just with a opposite polarity: https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/src/System/Runtime/CompilerServices/ReflectionBlockedAttribute.cs. The AOT compiler takes advantage of this attribute for optimizations, but the runtime also prevents these methods from being invoked by reflection.
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.
The opposite polarity is nice because it means that files that IL Linker didn't touch (because we didn't run it) will be set up the right way ("assume everything can be accessed by reflection").
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.
Oh, nice. I didn't think there already was something with those semantics.
I was trying to avoid the size increase of tagging the majority of methods with an attribute. There's definitely soundness gains from doing it the way suggested here though.
src/linker/Linker.Steps/SweepStep.cs
Outdated
| } | ||
|
|
||
| protected virtual void SweepMethods (Collection<MethodDefinition> methods) | ||
| void AddReflectedAttr (MethodDefinition method, AssemblyDefinition assembly) |
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.
Couple things
- From a design perspective I don't think a "SweepStep" should inject additional attributes. The steps role is to "Sweep".
- For any injection of new attributes, I would like to have a switch to turn it off. I don't see why we would want these extra attributes in our UnityLinker. It would just increase code size.
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.
For #2, mono embedders using the LLVM AOT backend can expect to see code size reductions when using this attribute: mono/mono#13697
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.
For #2, mono embedders using the LLVM AOT backend can expect to see code size reductions when using this attribute: mono/mono#13697
That's good to know.
Currently, we don't use the LLVM AOT backend so for us it would only increase size.
I don't really care what the switch looks like. Something like the following may kill two birds with one stone.
protected virtual GetMonoCodeReflectedAttributeCtor()
{
return System.Type.GetType ("Mono.Codegen+Reflected")?.GetConstructor (Array.Empty<System.Type> ());
}
void AddReflectedAttr (MethodDefinition method, AssemblyDefinition assembly)
{
// Public methods have non-visible call sites by default, this attribute doesn't
// help us at all.
//
// See mono_aot_can_specialize in aot-compiler.c in mono
if (!method.IsPrivate)
return;
var reflectionMethod = GetMonoCodeReflectedAttributeCtor();
if (reflectionMethod == null)
return;
MethodReference methodRef = assembly.MainModule.Import (reflectionMethod);
method.CustomAttributes.Add(new CustomAttribute (methodRef));
}
We could override GetMonoCodeReflectedAttributeCtor to turn this behavior off. And the null check would fix the tests that are failing with this PR when ran on windows against the .NET Framework assemblies.
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.
Or you could pull this logic out into it's own step. Solving the slightly confusing situation of this being in the SweepStep and then giving us the ability to simply not register this new step.
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 moved this to the CodeRewriter step. Can definitely put it in it's own step instead.
| var expectedAttrs = GetExpectedAttributes (src).ToList (); | ||
| var linkedAttrs = FilterLinkedAttributes (linked).ToList (); | ||
|
|
||
| for (int i=0; i < linkedAttrs.Count; i++) { |
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 don't like this. It effectively hides a code size increasing behavior from all tests. I believe this change is the first time monolinker would begin adding to size. That makes me less inclined to believe it is OK to hide this behavior.
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 can make this optional if the addition of attributes is too much, but the codegen size savings are going to motivate it being the default for Xamarin when possible.
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.
My comment here is with regards to how tests should work. If I look at a test file, I should be able to understand the changes the linker is expected to make. Quietly filtering out an attribute injected by the linker makes it so that one can no longer understand all of the changes the linker will make.
Whether or not this new behavior is on or off by default is a separate discussion.
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.
It's easy enough for me to just add a set of these tests with it disabled and a set with it enabled. I'll do that instead.
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 agree with Mike this is not the correct way to filter the attribute and in reality we want exact opposite
|
PreserveAttribute is applied when we want the linker to act as if the annotated element was a root. We don't currently apply it to elements we find when traversing the source. It seems to have different semantics. This attribute is applied to every private method that is called directly by reflection or is marked in the XML as called by an external caller. It's also worth noting that PreserveAttribute is a message from the coder, made for the linker. This is a message made by the linker, for the backend code generator. They're used differently, intend to communicate different things, and probably have edge cases in which they disagree. Most of the time, if you set PreserveAttribute on a private method, you should see the Reflected attribute also propagated to it. The majority of the |
|
It would be good to have some tests. This will require adding support for a new assertion attribute. Ex: |
|
Should methods marked by the |
|
I think this can be done in a way where not just the LLVM backend benefits from this (i.e. we can get a size reduction in all scenarios). If a private method is not used from reflection, flip the accessibility of the method to We get an immediate size reduction for the IL assembly because we no longer need to carry around names of such private methods. At LLVM codegen time, you can assume that The methods will not be particularly useful for reflection without their name (I'm not sure if reflection surfaces them in |
src/linker/Linker.Steps/SweepStep.cs
Outdated
| if (!method.IsPrivate) | ||
| return; | ||
|
|
||
| var reflectionMethod = System.Type.GetType ("Mono.Codegen+Reflected").GetConstructor (Array.Empty<System.Type> ()); |
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.
Won't think lookup the type in the currently running linker process?
c6fec93 to
96133c0
Compare
| AddReflectedAttr (method); | ||
| } | ||
|
|
||
| void AddReflectedAttr (MethodDefinition method) |
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.
Please rename the method
| return; | ||
|
|
||
| if (noOptAttr == null) { | ||
| var methodImpl = typeof (System.Runtime.CompilerServices.MethodImplAttribute); |
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 is wrong, you need to use target reference type. Use FindPredefinedType instead
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 gave that a try before and Cecil reported that it couldn't import the MethodImplAttribute class when doing some emitter pass. I'll look back into it.
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.
Still running into cecil issues, cecil doesn't see any of the constructors for that type beyond the copy constructor.
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.
Also this:
doesn't actually change the emitted method's attributes at all.
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.
It's also definitely not tested, because if I remove those lines the tests pass.
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.
Fyi, the test infrastructure does not assert ImplAttributes currently. See VerifyMethodKept.VerifyMethodKept. We should hook up the same pattern used for VerifyPseudoAttributes
| var methodImpl = typeof (System.Runtime.CompilerServices.MethodImplAttribute); | ||
| var option = typeof(System.Runtime.CompilerServices.MethodImplOptions); | ||
|
|
||
| noOptAttrArg = (MethodImplOptions) Enum.Parse(option, "NoOptimization"); |
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.
use Mono.Cecil.MethodImplAttributes value directly
src/linker/Mono.Linker.csproj
Outdated
| <Reference Include="System" /> | ||
| <Reference Include="System.Core" /> | ||
| <Reference Include="System.Xml" /> | ||
| <Reference Include="System.Reflection" /> |
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.
Please undo this change
src/linker/Mono.Linker.csproj
Outdated
| <Compile Include="Linker.Steps\PreserveCalendarsStep.cs" /> | ||
| <Compile Include="Linker.Steps\RemoveFeaturesStep.cs" /> | ||
| <Compile Include="Linker.Steps\CodeRewriterStep.cs" /> | ||
| <Compile Include="Linker.Steps\AccessAnnotatorStep.cs" /> |
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.
Where is this file?
| var expectedAttrs = GetExpectedAttributes (src).ToList (); | ||
| var linkedAttrs = FilterLinkedAttributes (linked).ToList (); | ||
|
|
||
| for (int i=0; i < linkedAttrs.Count; i++) { |
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 agree with Mike this is not the correct way to filter the attribute and in reality we want exact opposite
src/linker/Linker/Driver.cs
Outdated
| } | ||
| continue; | ||
|
|
||
| case "--annotate-method-access": |
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 needs to be documented and please try to come up with more specific name (this is too vague)
src/linker/Linker.Steps/MarkStep.cs
Outdated
|
|
||
| protected void MarkMethodReflected (MethodDefinition method) | ||
| { | ||
| Annotations.MarkReflected (method); |
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 conditional
| // help us at all. | ||
| // | ||
| // See mono_aot_can_specialize in aot-compiler.c in mono | ||
| if (!method.IsPrivate) |
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 check should happen before you populate the list
src/linker/Linker/Driver.cs
Outdated
| AddCustomStep (p, custom_step); | ||
|
|
||
| if (annotateAccesses) | ||
| p.AddStepAfter (typeof (CodeRewriterStep), new AccessAnnotatorStep ()); |
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.
Please add tests for the new logic
96133c0 to
17cfca3
Compare
| } | ||
| noOptAttr = assembly.MainModule.ImportReference (reflectionMethod); | ||
| if (noOptAttr == null) | ||
| throw new Exception("Could not find System.Runtime.CompilerServices.ReflectionBlockedAttribute in BCL."); |
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.
Something to watch out for.
You can run the linker tests on windows. When you do, the tests are linked against the .net framework assemblies. I doubt they have this type which I suspect means this exception is going to be hit.
As long as --annotate-unseen-callers is opt-in, the solution is simple. Any tests you add that turn on --annotate-unseen-callers will likely need to be ignored on Windows.
| using Mono.Linker.Tests.Cases.Expectations.Assertions; | ||
| using Mono.Linker.Tests.Cases.Expectations.Metadata; | ||
|
|
||
| namespace Mono.Linker.Tests.Cases.Reflection |
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 would move this out into it's own top level directory and add a new test method w/ [TestCaseSource] in TestSuites.cs. Two reasons
- To date, the
Reflectionfolder has been focused on tests for the detection & marking of reflection usage. I feel like this is a separate behavior from that. - It will be easy to ignore all of these tests on Windows if they all fall under a single
[TestCaseSource]method . See https://github.com/mono/linker/pull/506/files#r270871566
src/linker/Linker/LinkContext.cs
Outdated
|
|
||
| public CodeOptimizations DisabledOptimizations { get; set; } | ||
|
|
||
| public bool AnnotateUnseenCallers { get; set; } |
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.
Indent
| method.CustomAttributes.Add (cattr); | ||
|
|
||
| Annotations.Mark(cattr); | ||
| Annotations.Mark(cattr.AttributeType); |
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 believe you will need to call Resolve() on cattr.AttributeType and cattr.Constructor and then mark the TypeDefinition and MethodDefinition instead. Marking the TypeReference and MethodReference will not ensure they survive in all cases
|
What we can also do @mrvoorhe is to strip out the usage of this attribute in https://github.com/mono/mono/tree/master/mcs/tools/cil-strip if you'd like to have that information available at compile-time, but not at run-time. |
718972e to
e6ecf91
Compare
|
@mrvoorhe looks like CI doesn't have that attribute available. |
|
@alexanderkyte I'm guessing the .NET Core builds fail because .NET Core doesn't have the type |
I don't entirely follow. If this attribute injection is an opt-in command line option then that works for our needs. |
| [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.CodegenAnnotationTests))] | ||
| public void CodegenAnnotationTests (TestCase testCase) | ||
| { | ||
| Run (testCase); |
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.
Into this method I would add
if (Windows)
Assert.Ignore("These tests are not valid when linking against .NET Framework");
#if NETCOREAPP
Assert.Ignore("These tests are not valid when linking against .NET Core");
#endif
| [TestCaseSource (typeof (TestDatabase), nameof (TestDatabase.CodegenAnnotationTests))] | ||
| public void CodegenAnnotationTests (TestCase testCase) | ||
| { | ||
| if (Windows) |
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.
Sorry, I should have been more clear. When I said Windows, I meant "Replace me with the verbose way .NET makes you check if you are on windows"
SystemEnvironment.OSVersion.Platform == PlatformID.Win32
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 thought you had some variables inherited from test infrastructure or something. That makes more sense. Thanks
8c7b700 to
b347d5f
Compare
|
3 tests with CustomAttribute issues. The source for the check that does the DebuggableAttribute assertions say: |
|
These failures are on master. One of my PR's that was just merged had a logical conflict with a different PR that was merged while it was open. I will open a PR to fix the tests. |
|
Here's the fix PR #514 . Once that lands rebase and you should be good to go. |
b347d5f to
37c9aa8
Compare
| var cattr = new CustomAttribute (noOptAttr); | ||
| method.CustomAttributes.Add (cattr); | ||
|
|
||
| Annotations.Mark (cattr); |
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 has to happen during Mark step when we know we'll need the type, see for example https://github.com/mono/linker/blob/37c9aa8a9617e4999f07de457ef2ebe02e5e3f64/src/linker/Linker.Steps/MarkStep.cs#L1894 how to mark it
| // help us at all. | ||
| // | ||
| // See mono_aot_can_specialize in aot-compiler.c in mono | ||
| if (!method.IsPrivate) |
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.
Move this to mark step
| continue; | ||
| if (ref_method.Parameters.Count != 0) | ||
| continue; | ||
| reflectionMethod = ref_method; |
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.
You should cache this, we don't need to do the loop up in each assembly
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.
Hmm, we need the context to look up the type reference, and the assembly to look up the definition. I'm not sure how much we can cache, but I'll look into it.
|
|
||
| void ProcessType (TypeDefinition type) | ||
| { | ||
| foreach (var method in type.Methods) { |
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.
The attribute can be applied to types. It might be better to optimize for that as it'll be really used a lot otherwise (or maybe even do it at assembly level)
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.
If all of the private methods can have this attribute, I now hoist it and apply it to the type.
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.
The meaning of the attribute is to completely block reflection, not just the private methods on a type - there's nothing in the attribute's name implying it would be for privates only. .NET Native and CoreRT will discard metadata completely when this attribute is applied on a type (we get like 20% size on disk savings from reflection blocking in general, so aggressively discarding metadata is pretty huge).
There is an attribute with the semantic we would want for this: DisablePrivateReflectionAttribute. I would suggest using that. I know the AttributeUsage doesn't allow using it on types, but tools typically don't care. We could file a CoreFX issue to allow extending the AttributeUsage.
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.
@MichalStrehovsky Did that here: 7dfa1ec
| using System.Runtime.CompilerServices; | ||
|
|
||
| namespace Mono.Linker.Steps { | ||
| public class UnseenCallerAnnotateStep : BaseStep |
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.
Rename this to ReflectionBlockedStep
src/linker/Linker/Driver.cs
Outdated
| Console.WriteLine (" --strip-resources Remove XML descriptor resources for linked assemblies. Defaults to true"); | ||
| Console.WriteLine (" --strip-security Remove metadata and code related to Code Access Security. Defaults to true"); | ||
| Console.WriteLine (" --used-attrs-only Any attribute is removed if the attribute type is not used. Defaults to false"); | ||
| Console.WriteLine (" --annotate-unseen-callers Mark methods without calls through reflection, assist JIT codegen. Defaults to false"); |
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'd suggest this to be more explicit e.g. --no-reflection-methods Mark all methods never used using reflection. Defaults to false`
src/linker/Linker.Steps/MarkStep.cs
Outdated
| Tracer.Push ($"Reflection-{method}"); | ||
| try { | ||
| MarkMethod (method); | ||
| if (_context.AnnotateUnseenCallers) |
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.
You need to add same to remaining UsedViaReflection* methods (e.g. for methods or accessors)
| Context.LogMessage ($"Duplicate preserve in {_xmlDocumentLocation} of {method.FullName}"); | ||
|
|
||
| Annotations.Mark (method); | ||
| if (Context.AnnotateUnseenCallers) |
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.
Why is it needed?
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.
The methods named here are methods that may have call sites that we do not see, or they may be reflected.
| public static void Main() | ||
| { | ||
| var obj = new A(); | ||
| var method = typeof(A).GetMethod("FooPrivRefl", BindingFlags.NonPublic); |
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.
Please add tests for ctors and accessors
37c9aa8 to
9ebfbd2
Compare
9ebfbd2 to
7dfa1ec
Compare
| void GetNoOptAttr () | ||
| { | ||
| if (noOptAttr == null) { | ||
| noOptAttr = assembly.MainModule.ImportReference (GetReflectionBlockedAttr (Context)); |
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 thought you have to call assembly.MainModule.ImportReference once for each assembly? This caching in noOptAttr looks like it would prevent that from happening.
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.
Ah, good catch. If this has to be regenerated once per assembly, I'll do that where I assign the assembly to the assembly object field. I'll push that change shortly.
7dfa1ec to
2a4986b
Compare
Commit migrated from dotnet/linker@e053b68
This attribute is going to be used by the AOT code generator to identify private methods that can have code generated to the visible call sites.
These methods with the Reflected attribute will necessarily have call sites that cannot be seen, meaning that we cannot specialize the definition.