Skip to content

Commit cd7a8ba

Browse files
authored
First draft of blog post on fixing warnings (dotnet/linker#1946)
Co-authored-by: Michal Strehovský <[email protected]> Commit migrated from dotnet/linker@4ef464a
1 parent 10580d4 commit cd7a8ba

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
2+
# Trim warnings in .NET 6
3+
4+
[In .NET Core 3.1 and 5.0 we introduced
5+
trimming](https://devblogs.microsoft.com/dotnet/app-trimming-in-net-5/) as a new preview feature
6+
for self-contained .NET core applications. Conceptually the feature is very simple: when
7+
publishing the application the .NET SDK analyzes the entire application and removes all unused
8+
code. In the time trimming has been in preview, we've learned that trimming is very powerful --
9+
it can reduce application size by half or more. However, we've also learned about the
10+
difficulties in safely trimming applications.
11+
12+
The most difficult question in trimming is what is unused, or more precisely, what is used. For
13+
most standard C# code this is trivial -- the trimmer can easily walk method calls, field and
14+
property references, etc, and determine what code is used. Unfortunately, some features, like
15+
reflection, present a significant problem. Consider the following code:
16+
17+
```C#
18+
string s = Console.ReadLine();
19+
Type type = Type.GetType(s);
20+
foreach (var m in type.GetMethods())
21+
{
22+
Console.WriteLine(m.Name);
23+
}
24+
```
25+
26+
In this example, `Type.GetType` dynamically requests a type with an unknown name, and then prints
27+
the names of all of its methods. Because there's no way to know at publish time what type name is
28+
going to be used, there's no way for the trimmer to know which type to preserve in the output.
29+
It's very likely that this code could have worked before trimming (as long as the input is
30+
something known to exist in the target framework), but would probably produce a null reference
31+
exception after trimming (due to `Type.GetType` returning null).
32+
33+
This is a frustrating situation. Trimming often works just fine but occasionally it can produce
34+
breaking behavior, sometimes in rare code paths, and it can be very hard to trace down the cause.
35+
36+
For .NET 6 we want to bring a new feature to trimming: trim warnings. Trim warnings happen
37+
because the trimmer sees a call which may access other code in the app but the trimmer can't
38+
determine which code. This could mean that the trimmer would trim away code which is used at
39+
runtime.
40+
41+
## Reacting to trim warnings
42+
43+
Trim warnings are meant to bring predictability to trimming. The problem with trimming is that some
44+
code patterns can depend on code in a way that isn't understandable by the trimmer. Whenever the
45+
trimmer encounters code like that, that's when you should expect a trim warning.
46+
47+
There are two big categories of warnings which you will likely see:
48+
49+
1. `RequiresUnreferencedCode`
50+
2. `DynamicallyAccessedMembers`
51+
52+
### RequiresUnreferencedCode
53+
54+
[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
55+
that the method is not trim-compatible, meaning that it might use reflection or some other
56+
mechanism to access code that may be trimmed away. This attribute is used when it's not possible
57+
for the trimmer to understand what's necessary, and a blanket warning is needed. This would often
58+
be true for methods which use the C# `dynamic` keyword, `Assembly.LoadFrom`, or other runtime
59+
code generation technologies.
60+
An example would be:
61+
62+
```C#
63+
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
64+
void MethodWithAssemblyLoad() { ... }
65+
66+
void TestMethod()
67+
{
68+
// IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
69+
// can break functionality when trimming application code. Use 'MethodFriendlyToTrimming' instead.
70+
MethodWithAssemblyLoad();
71+
}
72+
```
73+
74+
There aren't many workarounds for `RequiresUnreferencedCode`. The best way is to avoid calling
75+
the method at all when trimming and use something else which is trim-compatible. If you're
76+
writing a library and it's not in your control whether or not to call the method and you just
77+
want to communicate to *your* caller, you can also add `RequiresUnreferencedCode` to your own
78+
method. This silences all trimming warnings in your code, but will produce a warning whenever
79+
someone calls your method.
80+
81+
If you can somehow determine that the call is safe, and all the code that's needed won't be
82+
trimmed away, you can also suppress the warning using
83+
[UnconditionalSuppressMessageAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.unconditionalsuppressmessageattribute?view=net-5.0).
84+
For example:
85+
86+
```C#
87+
[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
88+
void MethodWithAssemblyLoad() { ... }
89+
90+
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
91+
Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
92+
void TestMethod()
93+
{
94+
MethodWithAssemblyLoad(); // Warning suppressed
95+
}
96+
```
97+
98+
`UnconditionalSuppressMessage` is like `SuppressMessage` but it is preserved into IL, so the
99+
trimmer can see the suppression even after build and publish. `SuppressMessage` and `#pragma`
100+
directives are only present in source so they can't be used to silence warnings from the
101+
trimmer. Be very careful when suppressing trim warnings: it's possible that the call may be
102+
trim-compatible now, but as you change your code that may change and you may forget to review all
103+
the suppressions.
104+
105+
### DynamicallyAccessedMembers
106+
107+
[DynamicallyAccessedMembers](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-5.0) is usually about reflection. Unlike `RequiresUnreferencedCode`,
108+
reflection can sometimes be understood by the trimmer as long as it's annotated correctly.
109+
Let's take another look at the original example:
110+
111+
```C#
112+
string s = Console.ReadLine();
113+
Type type = Type.GetType(s);
114+
foreach (var m in type.GetMethods())
115+
{
116+
Console.WriteLine(m.Name);
117+
}
118+
```
119+
120+
In the example above, the real problem is `Console.ReadLine()`. Because *any* type could
121+
be read the trimmer has no way to know if you need methods on `System.Tuple` or `System.Guid`
122+
or any other type. On the other hand, if your code looked like,
123+
124+
```C#
125+
Type type = typeof(System.Tuple);
126+
foreach (var m in type.GetMethods())
127+
{
128+
Console.WriteLine(m.Name);
129+
}
130+
```
131+
132+
This would be fine. Here the trimmer can see the exact type being referenced: `System.Tuple`. Now
133+
it can use flow analysis to determine that it needs to keep all public methods. So where does
134+
`DynamicallyAccessMembers` come in? What happens if the reflection is split across methods?
135+
136+
```C#
137+
void Method1()
138+
{
139+
Method2(typeof(System.Tuple));
140+
}
141+
void Method2(Type type)
142+
{
143+
var methods = type.GetMethods();
144+
...
145+
}
146+
```
147+
148+
If you compile the above, now you see a warning:
149+
150+
```
151+
Trim analysis warning IL2070: net6.Program.Method2(Type): 'this' argument does not satisfy
152+
'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The
153+
parameter 'type' of method 'net6.Program.Method2(Type)' does not have matching annotations. The
154+
source value must declare at least the same requirements as those declared on the target
155+
location it is assigned to.
156+
```
157+
158+
For performance and stability flow analysis isn't performed between
159+
methods, so annotation is needed to pass information upward, from the reflection call
160+
(`GetMethods`) to the source of the `Type` (`typeof`). In the above example, the trimmer warning
161+
is saying that `GetMethods` requires the `PublicMethods` annotation on types, but the `type`
162+
variable doesn't have the same requirement. In other words, we need to pass the requirements from
163+
`GetMethods` up to the caller:
164+
165+
```C#
166+
void Method1()
167+
{
168+
Method2(typeof(System.Tuple));
169+
}
170+
void Method2(
171+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
172+
{
173+
var methods = type.GetMethods();
174+
...
175+
}
176+
```
177+
178+
Now the warning disappears, because the trimmer knows exactly which members to preserve, and
179+
which type(s) to preserve them on. In general, this is the best way to deal with
180+
`DynamicallyAccessedMembers` warnings: add annotations so the trimmer knows what to preserve.
181+
182+
As with `RequiresUnreferencedCode` warnings, adding `RequiresUnreferencedCode` or
183+
`UnconditionalSuppressMessage` attributes also works, but none of these options make the code
184+
compatible with trimming, while adding `DynamicallyAccessedMembers` does.
185+
186+
## Conclusion
187+
188+
This description should cover the most common situations you end up in while trimming your
189+
application. Over time we'll continue to improve the diagnostic experience and tooling.
190+
191+
As we continue developing trimming we hope to see more code that's fully annotated, so users can
192+
trim with confidence. Because trimming involves the whole application, trimming is as much a
193+
feature of the ecosystem as it is of the product and we're depending on all developers to help
194+
improve the ecosystem.

0 commit comments

Comments
 (0)