Skip to content

Commit a2d3b56

Browse files
authored
Trim static interfaces (dotnet/linker#2741)
Don't mark static interface methods when called on a concrete type, and removed the MethodImpl / Override if the interface method is kept. Co-authored-by: vitek-karas <[email protected]> Co-authored-by: vitek-karas <[email protected]> Commit migrated from dotnet/linker@a073a68
1 parent 6dbb1d4 commit a2d3b56

8 files changed

Lines changed: 258 additions & 30 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Removal Behavior for interfaces
2+
3+
## `unusedinterfaces` optimization
4+
5+
The `unusedinterfaces` optimization controls whether or not trimmin may remove the `interfaceimpl` annotation which denotes whether a class implements an interface. When the optimization is off, the linker will not remove the annotations regardless of the visibility of the interface (even private interface implementations will be kept). However, unused methods from interfaces may still be removed, as well as `.override` directives from methods that implement an interface method. When the optimization is on and the linker can provably determine that an interface will not be used on a type, the annotation will be removed. In order to know whether it is safe to remove an interface implementation, it is necessary to have a "global" view of the code. In other words, if an assembly is passed without selecting `link` for the `action` option for the linker, all classes that implement interfaces from that assembly will keep those interface implementation annotations. For example, if you implement `System.IFormattable` from the `System.Runtime` assembly, but pass the assembly with `--action copy System.Runtime`, the interface implementation will be kept even if your code doesn't use it.
6+
7+
## Static abstract interface methods
8+
9+
The linker's behavior for methods declared on interfaces as `static abstract` like below are defined in the following cases using the example interface and class below:
10+
11+
```C#
12+
interface IFoo
13+
{
14+
static abstract int GetNum();
15+
}
16+
17+
class C : IFoo
18+
{
19+
static int GetNum() => 1;
20+
}
21+
```
22+
23+
### Method call on concrete type
24+
25+
On a direct call to a static method which implements a static interface method, only the body is rooted, not its associated `MethodImpl`. Similarly, the interface method which it implements is not rooted.
26+
27+
Example:
28+
29+
In the following program, `C.GetNum()` would be kept, but `IFoo.GetNum()` would be removed.
30+
31+
```C#
32+
public class Program
33+
{
34+
public static void Main()
35+
{
36+
C.GetNum();
37+
}
38+
}
39+
```
40+
41+
### Method call on a constrained type parameter
42+
43+
On a call to a static abstract interface method that is accessed through a constrained type parameter, the interface method is rooted, as well as every implementation method on every type.
44+
45+
Example:
46+
47+
In the following program, `C.GetNum()`, `IFoo.GetNum()`, and `C2.GetNum()` are all kept.
48+
49+
```C#
50+
public class C2 : IFoo
51+
{
52+
static int GetNum() => 2;
53+
}
54+
public class Program
55+
{
56+
public static void Main()
57+
{
58+
M<C>();
59+
}
60+
public static void M<T>() where T : IFoo
61+
{
62+
T.GetNum();
63+
}
64+
}
65+
```
66+
67+
# `.override` directive and `MethodImpl` marking
68+
69+
The linker currently does not mark `.override` or `MethodImpl` relationships. It will only remove the relationship if one of the methods is removed. These relationships are not always necessary, and future updates could add the ability to remove these relationships even if both methods are not trimmed.

src/tools/illink/src/linker/Linker.Steps/MarkStep.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,10 @@ void ProcessVirtualMethods ()
573573
}
574574
}
575575

576+
/// <summary>
577+
/// Does extra handling of marked types that have interfaces when it's necessary to know what types are marked or instantiated.
578+
/// e.g. Marks the "implements interface" annotations and removes override annotations for static interface methods.
579+
/// </summary>
576580
void ProcessMarkedTypesWithInterfaces ()
577581
{
578582
// We may mark an interface type later on. Which means we need to reprocess any time with one or more interface implementations that have not been marked
@@ -2288,7 +2292,7 @@ void MarkGenericParameter (GenericParameter parameter)
22882292

22892293
/// <summary>
22902294
/// Returns true if any of the base methods of the <paramref name="method"/> passed is in an assembly that is not trimmed (i.e. action != trim).
2291-
/// Meant to be used to determine whether methods should be marked regardless of whether it is instantiated or not.
2295+
/// Meant to be used to determine whether methods should be marked regardless of whether it is instantiated or not.
22922296
/// </summary>
22932297
/// <remarks>
22942298
/// When the unusedinterfaces optimization is on, this is used to mark methods that override an abstract method from a non-link assembly and must be kept.
@@ -3049,8 +3053,16 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo
30493053
}
30503054
}
30513055

3056+
// Mark overridden methods except for static interface methods
30523057
if (method.HasOverrides) {
30533058
foreach (MethodReference ov in method.Overrides) {
3059+
// Method implementing a static interface method will have an override to it - note nonstatic methods usually don't unless they're explicit.
3060+
// Calling the implementation method directly has no impact on the interface, and as such it should not mark the interface or its method.
3061+
// Only if the interface method is referenced, then all the methods which implemented must be kept, but not the other way round.
3062+
if (Context.Resolve (ov)?.IsStatic == true
3063+
&& Context.Resolve (ov.DeclaringType)?.IsInterface == true) {
3064+
continue;
3065+
}
30543066
MarkMethod (ov, new DependencyInfo (DependencyKind.MethodImplOverride, method));
30553067
MarkExplicitInterfaceImplementation (method, ov);
30563068
}

src/tools/illink/src/linker/Linker.Steps/SweepStep.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)
453453

454454
SweepCustomAttributes (method.MethodReturnType);
455455

456+
SweepOverrides (method);
457+
456458
if (!method.HasParameters)
457459
continue;
458460

@@ -467,6 +469,15 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)
467469
}
468470
}
469471

472+
void SweepOverrides (MethodDefinition method)
473+
{
474+
for (int i = 0; i < method.Overrides.Count;) {
475+
if (Context.Resolve (method.Overrides[i]) is MethodDefinition ov && ShouldRemove (ov))
476+
method.Overrides.RemoveAt (i);
477+
else
478+
i++;
479+
}
480+
}
470481
void SweepDebugInfo (Collection<MethodDefinition> methods)
471482
{
472483
List<ScopeDebugInformation>? sweptScopes = null;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace Mono.Linker.Tests.Cases.Expectations.Assertions
7+
{
8+
/// <summary>
9+
/// Used to ensure that a method should keep an 'override' annotation for a method in the supplied base type.
10+
/// The absence of this attribute does not enforce that the override is removed -- this is different from other Kept attributes
11+
/// To enforce the removal of an override, use <see cref="RemovedOverrideAttribute"/>.
12+
/// Fails in tests if the method doesn't have the override method in the original or linked assembly.
13+
/// </summary>
14+
/// <seealso cref="RemovedOverrideAttribute" />
15+
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
16+
public class KeptOverrideAttribute : KeptAttribute
17+
{
18+
public Type TypeWithOverriddenMethodDeclaration;
19+
20+
public KeptOverrideAttribute (Type typeWithOverriddenMethod)
21+
{
22+
if (typeWithOverriddenMethod == null)
23+
throw new ArgumentNullException (nameof (typeWithOverriddenMethod));
24+
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
25+
}
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace Mono.Linker.Tests.Cases.Expectations.Assertions
7+
{
8+
/// <Summary>
9+
/// Used to ensure that a method should remove an 'override' annotation for a method in the supplied base type.
10+
/// Fails in tests if the method has the override method in the linked assembly,
11+
/// or if the override is not found in the original assembly
12+
/// </Summary>
13+
/// <seealso cref="KeptOverrideAttribute" />
14+
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
15+
public class RemovedOverrideAttribute : BaseInAssemblyAttribute
16+
{
17+
public Type TypeWithOverriddenMethodDeclaration;
18+
public RemovedOverrideAttribute (Type typeWithOverriddenMethod)
19+
{
20+
if (typeWithOverriddenMethod == null)
21+
throw new ArgumentException ("Value cannot be null or empty.", nameof (typeWithOverriddenMethod));
22+
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
23+
}
24+
}
25+
}

src/tools/illink/test/Mono.Linker.Tests.Cases/Inheritance.Interfaces/InterfaceVariants.cs

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,84 @@ public class InterfaceVariants
1717
public static void Main ()
1818
{
1919
Type t = typeof (UninstantiatedPublicClassWithInterface);
20-
t = typeof (UninstantiatedPublicClassWithImplicitlyImplementedInterface);
20+
t = typeof (UninstantiatedClassWithImplicitlyImplementedInterface);
2121
t = typeof (UninstantiatedPublicClassWithPrivateInterface);
22+
t = typeof (ImplementsUsedStaticInterface.InterfaceMethodUnused);
23+
t = typeof (ImplementsUnusedStaticInterface.InterfaceMethodUnused);
2224

23-
UninstantiatedPublicClassWithInterface.InternalStaticInterfaceMethodUsed ();
24-
InstantiatedClassWithInterfaces.InternalStaticInterfaceMethodUsed ();
25-
25+
ImplementsUnusedStaticInterface.InterfaceMethodUsedThroughImplementation.InternalStaticInterfaceMethodUsedThroughImplementation ();
26+
GenericMethodThatCallsInternalStaticInterfaceMethod
27+
<ImplementsUsedStaticInterface.InterfaceMethodUsedThroughInterface> ();
2628
// Use all public interfaces - they're marked as public only to denote them as "used"
2729
typeof (IPublicInterface).RequiresPublicMethods ();
2830
typeof (IPublicStaticInterface).RequiresPublicMethods ();
31+
var ___ = new InstantiatedClassWithInterfaces ();
32+
}
33+
34+
[Kept]
35+
internal static void GenericMethodThatCallsInternalStaticInterfaceMethod<T> () where T : IStaticInterfaceUsed
36+
{
37+
T.StaticMethodUsedThroughInterface ();
38+
}
39+
40+
[Kept]
41+
class ImplementsUsedStaticInterface
42+
{
43+
44+
[Kept]
45+
[KeptInterface (typeof (IStaticInterfaceUsed))]
46+
internal class InterfaceMethodUsedThroughInterface : IStaticInterfaceUsed
47+
{
48+
[Kept]
49+
[KeptOverride (typeof (IStaticInterfaceUsed))]
50+
public static void StaticMethodUsedThroughInterface ()
51+
{
52+
}
53+
public static void UnusedMethod () { }
54+
}
55+
56+
[Kept]
57+
[KeptInterface (typeof (IStaticInterfaceUsed))]
58+
internal class InterfaceMethodUnused : IStaticInterfaceUsed
59+
{
60+
[Kept]
61+
[KeptOverride (typeof (IStaticInterfaceUsed))]
62+
public static void StaticMethodUsedThroughInterface ()
63+
{
64+
}
65+
public static void UnusedMethod () { }
66+
}
67+
}
68+
69+
[Kept]
70+
internal class ImplementsUnusedStaticInterface
71+
{
72+
[Kept]
73+
internal class InterfaceMethodUsedThroughImplementation : IStaticInterfaceUnused
74+
{
75+
[Kept]
76+
[RemovedOverride (typeof (IStaticInterfaceUnused))]
77+
public static void InternalStaticInterfaceMethodUsedThroughImplementation () { }
78+
}
2979

30-
var a = new InstantiatedClassWithInterfaces ();
80+
[Kept]
81+
internal class InterfaceMethodUnused : IStaticInterfaceUnused
82+
{
83+
public static void InternalStaticInterfaceMethodUsedThroughImplementation () { }
84+
}
3185
}
3286

3387
[Kept]
3488
[KeptInterface (typeof (IEnumerator))]
3589
[KeptInterface (typeof (IPublicInterface))]
3690
[KeptInterface (typeof (IPublicStaticInterface))]
37-
[KeptInterface (typeof (IInternalStaticInterfaceWithUsedMethod))] // https://github.com/dotnet/linker/issues/2733
3891
[KeptInterface (typeof (ICopyLibraryInterface))]
3992
[KeptInterface (typeof (ICopyLibraryStaticInterface))]
4093
public class UninstantiatedPublicClassWithInterface :
4194
IPublicInterface,
4295
IPublicStaticInterface,
4396
IInternalInterface,
4497
IInternalStaticInterface,
45-
IInternalStaticInterfaceWithUsedMethod,
4698
IEnumerator,
4799
ICopyLibraryInterface,
48100
ICopyLibraryStaticInterface
@@ -69,8 +121,6 @@ public static void InternalStaticInterfaceMethod () { }
69121

70122
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
71123

72-
[Kept]
73-
public static void InternalStaticInterfaceMethodUsed () { }
74124

75125
[Kept]
76126
[ExpectBodyModified]
@@ -101,9 +151,9 @@ static void ICopyLibraryStaticInterface.CopyLibraryExplicitImplementationStaticI
101151

102152
[Kept]
103153
[KeptInterface (typeof (IFormattable))]
104-
public class UninstantiatedPublicClassWithImplicitlyImplementedInterface : IInternalInterface, IFormattable
154+
public class UninstantiatedClassWithImplicitlyImplementedInterface : IInternalInterface, IFormattable
105155
{
106-
internal UninstantiatedPublicClassWithImplicitlyImplementedInterface () { }
156+
internal UninstantiatedClassWithImplicitlyImplementedInterface () { }
107157

108158
public void InternalInterfaceMethod () { }
109159

@@ -122,15 +172,13 @@ public string ToString (string format, IFormatProvider formatProvider)
122172
[KeptInterface (typeof (IEnumerator))]
123173
[KeptInterface (typeof (IPublicInterface))]
124174
[KeptInterface (typeof (IPublicStaticInterface))]
125-
[KeptInterface (typeof (IInternalStaticInterfaceWithUsedMethod))] // https://github.com/dotnet/linker/issues/2733
126175
[KeptInterface (typeof (ICopyLibraryInterface))]
127176
[KeptInterface (typeof (ICopyLibraryStaticInterface))]
128177
public class InstantiatedClassWithInterfaces :
129178
IPublicInterface,
130179
IPublicStaticInterface,
131180
IInternalInterface,
132181
IInternalStaticInterface,
133-
IInternalStaticInterfaceWithUsedMethod,
134182
IEnumerator,
135183
ICopyLibraryInterface,
136184
ICopyLibraryStaticInterface
@@ -158,9 +206,6 @@ public static void InternalStaticInterfaceMethod () { }
158206

159207
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
160208

161-
[Kept]
162-
public static void InternalStaticInterfaceMethodUsed () { }
163-
164209
[Kept]
165210
bool IEnumerator.MoveNext () { throw new PlatformNotSupportedException (); }
166211

@@ -225,13 +270,20 @@ internal interface IInternalStaticInterface
225270
static abstract void ExplicitImplementationInternalStaticInterfaceMethod ();
226271
}
227272

228-
// The interface methods themselves are not used, but the implentation of these methods is
229-
// https://github.com/dotnet/linker/issues/2733
273+
// The interface methods themselves are not used, but the implementation of these methods is
274+
internal interface IStaticInterfaceUnused
275+
{
276+
static abstract void InternalStaticInterfaceMethodUsedThroughImplementation ();
277+
}
278+
279+
// The interface methods themselves are used through the interface
230280
[Kept]
231-
internal interface IInternalStaticInterfaceWithUsedMethod
281+
internal interface IStaticInterfaceUsed
232282
{
233-
[Kept] // https://github.com/dotnet/linker/issues/2733
234-
static abstract void InternalStaticInterfaceMethodUsed ();
283+
[Kept]
284+
static abstract void StaticMethodUsedThroughInterface ();
285+
286+
static abstract void UnusedMethod ();
235287
}
236288

237289
private interface IPrivateInterface

src/tools/illink/test/Mono.Linker.Tests.Cases/Libraries/RootLibrary.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ public void InternalInterfaceMethod () { }
217217
void IInternalInterface.ExplicitImplementationInternalInterfaceMethod () { }
218218

219219
[Kept]
220+
[RemovedOverride (typeof (IInternalStaticInterface))]
220221
public static void InternalStaticInterfaceMethod () { }
221222

222223
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
@@ -300,6 +301,7 @@ public void InternalInterfaceMethod () { }
300301
void IInternalInterface.ExplicitImplementationInternalInterfaceMethod () { }
301302

302303
[Kept]
304+
[RemovedOverride (typeof (IInternalStaticInterface))]
303305
public static void InternalStaticInterfaceMethod () { }
304306

305307
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
@@ -366,7 +368,6 @@ internal interface IInternalInterface
366368
[Kept]
367369
internal interface IInternalStaticInterface
368370
{
369-
[Kept] // https://github.com/dotnet/linker/issues/2733
370371
static abstract void InternalStaticInterfaceMethod ();
371372

372373
static abstract void ExplicitImplementationInternalStaticInterfaceMethod ();

0 commit comments

Comments
 (0)