-
Notifications
You must be signed in to change notification settings - Fork 127
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
Trim static interfaces #2741
Trim static interfaces #2741
Conversation
This will make sure that we keep all interfaces and all interface method implementations on such type.
…ad of Interface specific version
Check for optimization before skipping marking interface methods for PreservedScope Add doc comments
Rename IsVirtualNeededByInstantiatedTypeDueToPreservedScope to IsOverrideNeededByInstantiatedTypeDueToPreservedScope Added more comments describing purpose of methods Moved the static interface method check to the method that is called on all types regardless of instantiation
This doesn't change product behavior, only renamed methods and improved comments. Added new tests for the RootLibrary: - Added a dependency to an "copy" assembly (mainly because I can define a static interface in it) - Added more combinations to the interfaces/classes in the test - Since this uses static interface methods I had to enable "preview" language features for the test project and for the test infra.
src/linker/Linker.Steps/MarkStep.cs
Outdated
} | ||
} | ||
foreach (var overrideToRemove in staticInterfaceOverrideIndices) { | ||
method.Overrides.Remove (overrideToRemove); |
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 happens if we decide to keep the method because it's called? It would probably still remain removed from the overrides, right? That sounds problematic. I don't know the exact reasons why the compiler generates the explicit override, but I assume there is a reason - and linker should not go against 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.
Great point, I changed it to only remove the annotation if the interface method is also removed
src/linker/Linker.Steps/MarkStep.cs
Outdated
/// <summary> | ||
/// Removes the 'override' annotation for implementation of static interface methods when the interface method is removed. | ||
/// </summary> | ||
void RemoveStaticInterfaceOverrideAnnotations (TypeDefinition 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.
Is there a way to write a test for this?
I don't know if the test infra actually verifies the "overrides" (maybe it should at least as an opt-in)
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 doesn't look like there's anything in the test infra yet, but I'll look into it more and add a KeptOverride
/NoOverride
attribute to check for that
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've added a couple attribute to check for the presence/removal of the override annotations. They only perform a check if there is a KeptOverride
or RemovedOverride
, so it doesn't affect any other tests.
I'm not sure how this implementation would affect a derived class that inherits from a base class that implements a static interface when the derived class overrides the method. I can't think of any issues that this would cause, though. |
I'm not sure I understand the goal here. Just to clarify some terminology, in C#, interface methods are considered implemented, but not overridden. My understanding of the intent here is that you want to trim particular interface implementations for static virtual interface methods. I'm just not sure of the conditions when. If we take the example: interface I1
{
static abstract int M1();
}
class A : I1
{
public static int M1() => 0;
}
class B : A, I1
{
static int I1.M1() => 2;
} In what circumstances would I1.M1 implementations be trimmed? Or, in other words, what would need to be done to mark their implementations? |
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -3049,8 +3076,15 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo | |||
} | |||
} | |||
|
|||
// Mark overrides except for static interface 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.
Don't we want to mark the MethodImpl for these calls if and only if there's a constrained call to the target 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.
Yes, but here we are marking the methods themselves, not the MethodImpl. The static abstract interface methods themselves are marked elsewhere when the linker finds constrained calls to them. At the moment there's still no real "marking" of the MethodImpl
, it just checks if the overridden method is marked, and if it's not, it removes the MethodImpl / Override.
We could separate marking the Overrides list from marking the Overridden methods themselves, but I feel like that would lead to more (issues where the Override / MethodImpl is incorrectly marked and the override method is correctly removed) than (issues where the Override / MethodImpl is correctly marked and the override method is incorrectly removed).
@sbomer do you have any thoughts on whether to mark overrides separately?
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 actually like that we don't mark overrides/methodimpl explicitly:
- With your change in the sweep step - there's no way to make the IL inconsistent - it will fix itself and linker just needs to decide if it keeps a certain method or on.
- Linker in general doesn't "mutate" things, it only "deletes" them. So it should not be linker's decision if two methods should have the methodimpl between them or not - that decision should come from the input. If linker decides to keep both methods - the methodimpl between should be there if and only if it was there on the input.
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.
Agreed, I like handling this in SweepStep and I like @vitek-karas's reasoning here.
// Use all public interfaces - they're marked as public only to denote them as "used" | ||
typeof (IPublicInterface).RequiresPublicMethods (); | ||
typeof (IPublicStaticInterface).RequiresPublicMethods (); | ||
|
||
var a = new InstantiatedClassWithInterfaces (); | ||
} | ||
|
||
[Kept] | ||
internal static void GenericMethodThatCallsInternalStaticInterfaceMethod<T> () where T : IInternalStaticInterfaceWithInterfaceDefinitionUsed |
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.
Personally, I think these names are getting long enough that they're not very readable, but I'll accede 😊
|
||
[Kept] | ||
[KeptInterface (typeof (IInternalStaticInterfaceWithInterfaceDefinitionUsed))] | ||
internal class ImplementsInternalStaticInterfaceWithInterfaceDefinitionNotUsedThroughGeneric : IInternalStaticInterfaceWithInterfaceDefinitionUsed |
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't quite tell if this is meant to mean that the type is never used through a generic substitution. If so, that would mean that the InternalStaticInterfaceUsedThroughInterface
method below has its override kept regardless.
I don't necessarily think this is the wrong behavior, but it's good to note. We might want to file a bug about a potential improvement in the future, if that's something we're interested in.
docs/removal-behavior.md
Outdated
|
||
## Static abstract interface methods | ||
|
||
The linker's behavior for methods declared on interfaces as `static abstract` like below are defined in the following cases: |
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 follow a consistent style here of
[rule]
[example]
Instead of something more like prose. Basically, this is code, but in English.
I think the phrasing could also be more specific in what's rooted and what's not.
Something like:
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.
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.
Overall, if we imagine we had a complete spec here, I expect it would look something like:
[IL instruction]
[description of things it roots]
[example]
UninstantiatedPublicClassWithInterface.InternalStaticInterfaceMethodUsed (); | ||
InstantiatedClassWithInterfaces.InternalStaticInterfaceMethodUsed (); | ||
|
||
UninstantiatedPublicClassWithInterface.InternalStaticInterfaceMethodUsedThroughImplementation (); |
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's the significance of Internal
here? It's repeated a lot throughout the code. I can't think of a reason why that would make a difference.
/// Used to ensure that a method should keep an 'override' annotation for a method in the supplied base type | ||
/// Fails in tests if the method doesn't have the override method in the original or linked 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.
Please add a comment that this behaves differently from the other Kept
attributes - almost all the other ones test both sides, that is if the attribute is not there it means the thing should NOT be in the output. For example if a certain type doesn't have a Kept
attribute on it, we expected the type to not survive linking.
We do have some other attributes which are like this, but I think it's a good idea to at least document it - and maybe mention the other attribute RemoveOverrideAttribute as its counterpart.
src/linker/Linker.Steps/MarkStep.cs
Outdated
// Don't mark overrides for static interface methods. | ||
// Since they can only be called on a concrete type and not the interface, these methods can safely be removed in some cases |
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.
As discussed offline I wonder if this can be done for all interface methods (static and non-static) - but that's a separate discussion.
I think the comment here is a bit misleading, maybe a better way to describe this would be something like:
// Method implementing a static interface method will have an override to it.
// Calling the implementation method directly has no impact on the interface, and as such it should not mark the interface or its method.
// Only if the interface method is referenced, then all the methods which implemented must be kept, but not the other way round.
src/linker/Linker.Steps/MarkStep.cs
Outdated
@@ -3049,8 +3076,15 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo | |||
} | |||
} | |||
|
|||
// Mark overrides except for static interface 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.
I actually like that we don't mark overrides/methodimpl explicitly:
- With your change in the sweep step - there's no way to make the IL inconsistent - it will fix itself and linker just needs to decide if it keeps a certain method or on.
- Linker in general doesn't "mutate" things, it only "deletes" them. So it should not be linker's decision if two methods should have the methodimpl between them or not - that decision should come from the input. If linker decides to keep both methods - the methodimpl between should be there if and only if it was there on the input.
Interesting approach -- this is slightly more conservative than necessary, right? For instance, if the given type is never substituted for a generic parameter, we can be certain that the methodimpl isn't necessary for that method. |
I wonder if we could get into trouble with something like void Method (IFoo x)
{
MethodInfo method = (TypeConstrainedGenericMethod);
method.MakeGenericMethod(x.GetType());
method.Invoke();
} I think we could perhaps delay marking all implementations of static interface methods, then only mark the override if the InterfaceImpl is marked by something else / remove the MethodImpl if the interfaceImpl is not marked. From there we could even remove the implementation method if it's not called directly and there's no MethodImpl. I'm just not sure the size reductions would be worth the complexity required, though. |
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.
Thank you!
Yeah, I didn't really go into reflection here. I guess my more general point was: it sounds like what was being described was that the presence of an abstract interface method and an associated implementation would entail the MethodImpl between them. I'm not sure this is required, simply from a general "rule-based" point of view. However, I'm fine with going down that path if it's safer, or conceptually simpler, or just easier. Overall, I'm just trying to determine what's necessary vs. what's chosen. In this case it sounds like we're choosing to keep the MethodImpl, but I'm not sure it's necessary and I like to record these things somewhere in case someone comes along later and asks either why we're doing it or whether it could be removed. |
@agocke you're right. I was implicitly including the fact that linker currently doesn't have the infra to mark specific overrides and so I assumed we would not be adding that. It's definitely doable though. I don't think it's worth it right now, but it's something we should mention in the doc since we now have one. |
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.
LGTM
This reverts commit a073a68.
* Revert "Trim static interfaces (#2741)" This reverts commit a073a68. * Revert "Don't remove MethodImpl if overridden method is not in a link assembly (#2771)" This reverts commit 4461068. * Revert "Fix NullReferenceException when sweeping unused static interface (#2783)" This reverts commit 00e9a15.
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
* Revert "Trim static interfaces (dotnet/linker#2741)" This reverts commit dotnet/linker@a073a68. * Revert "Don't remove MethodImpl if overridden method is not in a link assembly (dotnet/linker#2771)" This reverts commit dotnet/linker@4461068. * Revert "Fix NullReferenceException when sweeping unused static interface (dotnet/linker#2783)" This reverts commit dotnet/linker@00e9a15. Commit migrated from dotnet/linker@eb6144b
This removes a static interface method from the list of
Overrides
of the implementation method. Since the interface method is removed as an override, it will not mark the interface method by default. However, if the interface method is called through the interface on a constrained type parameter (T.StaticInterfaceMethodImplementation() where T: IInterfaceWithStaticMethod
).Note we have to completely remove the interface method from the list of overrides. If we only skip the override from the marking, Cecil will fail to write the assembly because the overridden method can't be found anywhere.