-
Notifications
You must be signed in to change notification settings - Fork 128
First draft of blog post on fixing warnings #1946
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
Changes from all commits
660e3e4
4942438
c98be03
d08a430
1acd4df
0680936
4dfa391
81c095a
54b8832
f47a479
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
|
||
# Trim warnings in .NET 6 | ||
|
||
[In .NET Core 3.1 and 5.0 we introduced | ||
trimming](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/) as a new preview feature | ||
for self-contained .NET core applications. Conceptually the feature is very simple: when | ||
publishing the application the .NET SDK analyzes the entire application and removes all unused | ||
code. In the time trimming has been in preview, we've learned that trimming is very powerful -- | ||
it can reduce application size by half or more. However, we've also learned about the | ||
difficulties in safely trimming applications. | ||
|
||
The most difficult question in trimming is what is unused, or more precisely, what is used. For | ||
most standard C# code this is trivial -- the trimmer can easily walk method calls, field and | ||
property references, etc, and determine what code is used. Unfortunately, some features, like | ||
reflection, present a significant problem. Consider the following code: | ||
|
||
```C# | ||
string s = Console.ReadLine(); | ||
Type type = Type.GetType(s); | ||
foreach (var m in type.GetMethods()) | ||
{ | ||
Console.WriteLine(m.Name); | ||
} | ||
``` | ||
|
||
In this example, `Type.GetType` dynamically requests a type with an unknown name, and then prints | ||
the names of all of its methods. Because there's no way to know at publish time what type name is | ||
going to be used, there's no way for the trimmer to know which type to preserve in the output. | ||
It's very likely that this code could have worked before trimming (as long as the input is | ||
something known to exist in the target framework), but would probably produce a null reference | ||
exception after trimming (due to `Type.GetType` returning null). | ||
|
||
This is a frustrating situation. Trimming often works just fine but occasionally it can produce | ||
breaking behavior, sometimes in rare code paths, and it can be very hard to trace down the cause. | ||
|
||
For .NET 6 we want to bring a new feature to trimming: trim warnings. Trim warnings happen | ||
because the trimmer sees a call which may access other code in the app but the trimmer can't | ||
determine which code. This could mean that the trimmer would trim away code which is used at | ||
runtime. | ||
|
||
## Reacting to trim warnings | ||
|
||
Trim warnings are meant to bring predictability to trimming. The problem with trimming is that some | ||
code patterns can depend on code in a way that isn't understandable by the trimmer. Whenever the | ||
trimmer encounters code like that, that's when you should expect a trim warning. | ||
|
||
There are two big categories of warnings which you will likely see: | ||
|
||
1. `RequiresUnreferencedCode` | ||
2. `DynamicallyAccessedMembers` | ||
|
||
### RequiresUnreferencedCode | ||
|
||
[RequiresUnreferencedCode](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.requiresunreferencedcodeattribute?view=net-5.0) is simple: it's an attribute that can be placed on methods to indicate | ||
that the method is not trim-compatible, meaning that it might use reflection or some other | ||
mechanism to access code that may be trimmed away. This attribute is used when it's not possible | ||
for the trimmer to understand what's necessary, and a blanket warning is needed. This would often | ||
be true for methods which use the C# `dynamic` keyword, `Assembly.LoadFrom`, or other runtime | ||
code generation technologies. | ||
An example would be: | ||
|
||
```C# | ||
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")] | ||
void MethodWithAssemblyLoad() { ... } | ||
|
||
void TestMethod() | ||
{ | ||
// IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute' | ||
// can break functionality when trimming application code. Use 'MethodFriendlyToTrimming' instead. | ||
MethodWithAssemblyLoad(); | ||
} | ||
``` | ||
|
||
There aren't many workarounds for `RequiresUnreferencedCode`. The best way is to avoid calling | ||
the method at all when trimming and use something else which is trim-compatible. If you're | ||
writing a library and it's not in your control whether or not to call the method and you just | ||
want to communicate to *your* caller, you can also add `RequiresUnreferencedCode` to your own | ||
method. This silences all trimming warnings in your code, but will produce a warning whenever | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this produce a warning rather than an error? If the method is marked that its not going to work, and is called, then why only warn? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because in almost all cases the method might work in some cases and might not in others. For example We use this attribute basically to say "If you ignore this, you're on your own and will have to test at runtime". There are also cases where the method is sooo close to being trim compatible except for some weird special case - we can't mark it as compatible (since it's not 100%), but it almost always is. And as with any other trim analysis - the linker plays it super safe, the annotations do that as well. We don't want to block people completely if we are too careful. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty much everything in trimming is a warning, because many things just happen to work. For instance, if you call a method that has RUC does a lot of reflection on its parameter, but the parameter type is in user code (and therefore untrimmed) and nothing important from the framework was removed, it all just works. Of course, this is just luck, which is why the warning is there. |
||
someone calls your method. | ||
|
||
If you can somehow determine that the call is safe, and all the code that's needed won't be | ||
trimmed away, you can also suppress the warning using | ||
[UnconditionalSuppressMessageAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute?view=net-5.0). | ||
For example: | ||
|
||
```C# | ||
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")] | ||
void MethodWithAssemblyLoad() { ... } | ||
|
||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", | ||
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we explain/link to how to preserve things manually? If I were to read this blog post looking for answers on how to solve my current broken app I would see that I can:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A good question, but not really a post I'm prepared to write. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well just in case someone reads these comments, an XML descriptor file or DynamicDependencyAttribute could help to preserve stuff on the app |
||
void TestMethod() | ||
{ | ||
MethodWithAssemblyLoad(); // Warning suppressed | ||
} | ||
``` | ||
|
||
`UnconditionalSuppressMessage` is like `SuppressMessage` but it is preserved into IL, so the | ||
trimmer can see the suppression even after build and publish. `SuppressMessage` and `#pragma` | ||
directives are only present in source so they can't be used to silence warnings from the | ||
trimmer. Be very careful when suppressing trim warnings: it's possible that the call may be | ||
trim-compatible now, but as you change your code that may change and you may forget to review all | ||
the suppressions. | ||
|
||
### DynamicallyAccessedMembers | ||
|
||
[DynamicallyAccessedMembers](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-5.0) is usually about reflection. Unlike `RequiresUnreferencedCode`, | ||
reflection can sometimes be understood by the trimmer as long as it's annotated correctly. | ||
Let's take another look at the original example: | ||
|
||
```C# | ||
string s = Console.ReadLine(); | ||
Type type = Type.GetType(s); | ||
foreach (var m in type.GetMethods()) | ||
{ | ||
Console.WriteLine(m.Name); | ||
} | ||
``` | ||
|
||
In the example above, the real problem is `Console.ReadLine()`. Because *any* type could | ||
be read the trimmer has no way to know if you need methods on `System.Tuple` or `System.Guid` | ||
or any other type. On the other hand, if your code looked like, | ||
|
||
```C# | ||
Type type = typeof(System.Tuple); | ||
foreach (var m in type.GetMethods()) | ||
{ | ||
Console.WriteLine(m.Name); | ||
} | ||
``` | ||
|
||
This would be fine. Here the trimmer can see the exact type being referenced: `System.Tuple`. Now | ||
it can use flow analysis to determine that it needs to keep all public methods. So where does | ||
`DynamicallyAccessMembers` come in? What happens if the reflection is split across methods? | ||
|
||
```C# | ||
void Method1() | ||
{ | ||
Method2(typeof(System.Tuple)); | ||
} | ||
void Method2(Type type) | ||
{ | ||
var methods = type.GetMethods(); | ||
... | ||
} | ||
``` | ||
|
||
If you compile the above, now you see a warning: | ||
|
||
``` | ||
Trim analysis warning IL2070: net6.Program.Method2(Type): 'this' argument does not satisfy | ||
'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The | ||
parameter 'type' of method 'net6.Program.Method2(Type)' 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. | ||
``` | ||
|
||
For performance and stability flow analysis isn't performed between | ||
methods, so annotation is needed to pass information upward, from the reflection call | ||
(`GetMethods`) to the source of the `Type` (`typeof`). In the above example, the trimmer warning | ||
is saying that `GetMethods` requires the `PublicMethods` annotation on types, but the `type` | ||
variable doesn't have the same requirement. In other words, we need to pass the requirements from | ||
`GetMethods` up to the caller: | ||
|
||
```C# | ||
void Method1() | ||
{ | ||
Method2(typeof(System.Tuple)); | ||
} | ||
void Method2( | ||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) | ||
{ | ||
var methods = type.GetMethods(); | ||
... | ||
} | ||
``` | ||
|
||
Now the warning disappears, because the trimmer knows exactly which members to preserve, and | ||
which type(s) to preserve them on. In general, this is the best way to deal with | ||
`DynamicallyAccessedMembers` warnings: add annotations so the trimmer knows what to preserve. | ||
|
||
As with `RequiresUnreferencedCode` warnings, adding `RequiresUnreferencedCode` or | ||
`UnconditionalSuppressMessage` attributes also works, but none of these options make the code | ||
compatible with trimming, while adding `DynamicallyAccessedMembers` does. | ||
|
||
## Conclusion | ||
|
||
This description should cover the most common situations you end up in while trimming your | ||
application. Over time we'll continue to improve the diagnostic experience and tooling. | ||
|
||
As we continue developing trimming we hope to see more code that's fully annotated, so users can | ||
trim with confidence. Because trimming involves the whole application, trimming is as much a | ||
feature of the ecosystem as it is of the product and we're depending on all developers to help | ||
improve the ecosystem. |
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.
What is "the trimmer" ? We have not been using that term and most developers are familiar with linker term. Do we really have to confuse the community with another new name?
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 all locations other than
mono/linker
we've been trying to avoid the term "linker" because most people understand it as the "platform linker". We use "trim tool" or sometimes "trimmer" (I would prefer "trim tool" if possible).For the same reason we use "trimming" and not "linking" everywhere.
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.
Are you sure? Few references to public documentation that clearly uses IL Linker term to describe the tool.
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'm intentionally "ignoring" the mono/Xamarin side of things - those will likely use "linker" a lot. Most of our docs and blog posts use "Trimming" - we also use it everywhere in the messages (RUC) in the framework. I don't see us going back to "linking".
That said - I understand the confusion for Xamarin users - we might want to mention this on the first use of the term.
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 not referring to trimming but to "trimmer". Even .NET Core MSDN documentation is written in the way that it uses the term since 3.1.
I'm really not sure we have to rename everything to cause more confusion.
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 probably just not give it a name here. We can update the sentence to say "For most standard C# code this is trivial --
the trimmerwe can easily walk method calls, field and"That said,
https://en.wikipedia.org/wiki/Linker_(computing)
This is the definition of the term used in computer engineering. It's not what IL Linker is doing. It's confusing to anyone who has ever used a linker in the past.
Reusing this term in .NET was a mistake. We should have gone with something like Remover 8000. Now Xamarin often needs to qualify the word "linker" with the word "native" when referring to what everyone outside .NET calls "linker". It's a pretty sad place to be in and puts us (the authors of .NET) into a bad light (trying to steal a well-established term).
I've seen this cause confusion in the past. "Linker failed. Oh, not that one, the other one."
We should phase this name out before it moves out of the niche parts of .NET (both Xamarin and Blazor-WASM are still niches).
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 use of the term "trimmer" here is in its most generic sense -- it is the thing which "trims." The other documentation about a specific tool which is responsible for trimming is incorrect in this case because the only supported method of trimming applications in .NET 6 will be passing
PublishTrimmed=true
to the SDK. Rather than talk specifically about the SDK, I wanted to just use a generic term.I don't really see the need to get more specific here and talk about any specific tool. The term "linker" is certainly incorrect for .NET 6 since we do not plan on shipping a separate linker tool for supported scenarios.