dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.65k forks source link

GetType() behave strange #93720

Open kant2002 opened 2 years ago

kant2002 commented 2 years ago

I attempt to annotate WinForms for linker.

I have code approximately like this

// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] // fix
class DataGridViewCell
{
    public virtual object Clone()
    {
        DataGridViewCell dataGridViewCell = (DataGridViewCell)System.Activator.CreateInstance(GetType()); // no warning
        return dataGridViewCell;
    }
}

class DataGridViewCellConverter
{
  public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
  {
      ArgumentNullException.ThrowIfNull(destinationType);

      if (destinationType == typeof(InstanceDescriptor) && value is DataGridViewCell cell)
      {
          ConstructorInfo? ctor = cell.GetType().GetConstructor(Array.Empty<Type>());  // warning
          if (ctor is not null)
          {
              return new InstanceDescriptor(ctor, Array.Empty<object>(), false);
          }
      }

      return base.ConvertTo(context, culture, value, destinationType);
  }
}

If I add [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] to DataGridViewCell type, then issue in Clone goes away, but in Converter stays, and complain that it needs PublicParameterlessConstructor.

In my understanding these two versions of code absolutely identical and either I should have a fix for both places, or have warnings in both places.

https://github.com/dotnet/winforms/blob/5427959215dfc57d4ecf082ba5f65dc597e2c2b9/src/System.Windows.Forms/src/System/Windows/Forms/DataGridViewCell.cs#L1106-L1111 and https://github.com/dotnet/winforms/blob/5427959215dfc57d4ecf082ba5f65dc597e2c2b9/src/System.Windows.Forms/src/System/Windows/Forms/DataGridViewCellConverter.cs#L25-L39

marek-safar commented 2 years ago

/cc @vitek-karas

vitek-karas commented 2 years ago

This looks like a limitation of the current analysis. The analysis doesn't really track local variables as standalone values, it only tracks what is assigned into them. So in the case it sees the cell variable as having a value of "The parameter value" and the static type of that parameter is object.

A workaround for this would be to use for example local function for the code in the if branch, so something like:

      if (destinationType == typeof(InstanceDescriptor) && value is DataGridViewCell cell)
      {
          return CreateInstanceDescriptor(cell);
      }

      static object? CreateInstanceDescriptor(DataGridViewCell cell)
      {
          ConstructorInfo? ctor = cell.GetType().GetConstructor(Array.Empty<Type>());  // warning
          if (ctor is not null)
          {
              return new InstanceDescriptor(ctor, Array.Empty<object>(), false);
          }
      }

We'll look into fixing this...

kant2002 commented 2 years ago

Will linker perserve public parameterless constructor for derived class? For example in such snippet. I do think that's not affects me, so asking for my cuiriosity.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
public class DataGridViewCell
{
    public virtual object Clone()
    {
        DataGridViewCell dataGridViewCell = (DataGridViewCell)System.Activator.CreateInstance(GetType()); // no warning
        return dataGridViewCell;
    }
}

public class DerivedDataGridViewCell : DataGridViewCell
{
    public DerivedDataGridViewCell() {}
    public DerivedDataGridViewCell( int x ) {}
}

var x = new DerivedDataGridViewCell(5);
x.Clone();
vitek-karas commented 2 years ago

Yes - the annotation is applied to all derived types. This also works for interfaces, that you can put the annotation on the interface and all types implementing the interface will have that annotation applied to them.

kant2002 commented 2 years ago

Thank you, that was not obvious to me, since all missing interface member implementations screams one me that implementation member should be annotated in same was as interface member. But for classes annotations silently applied. That's why I was a bit puzzled do I do proper thing or not.

vitek-karas commented 2 years ago

since all missing interface member implementations screams one me that implementation member should be annotated in same was as interface member

Could you please clarify this? Maybe an example where this happens? The "screaming" should only happen if the implementation method doesn't have the same annotations as the interface method it implements.

vitek-karas commented 2 years ago

@sbomer @tlakollo @MichalStrehovsky @agocke @jtschuster - design discussion:

The problem is that MethodBodyScanner doesn't actually track local variables or stack slots as "entities", we only track external values through local variables and stack slots. So in the case of this bug, we track a value of the method parameter which is of static type object. When it gets assigned to a local variable of static type MyType we "loose" that information and we still keep tracking the method parameter with type of object.

Potential high-level solutions:

Personally I would prefer a full solution over other things, so that means either "Typed stack" or "Static typed values" or "Typed wrappers". From an architecture point of view I like the "Static typed values" a bit more (it doesn't need a side channel to pass type information nor it needs "unwrapping" everywhere). The downside is that we would need to add this info on all values (so basically to SingleValue itself) and we would not have singletons for UnknownValue and NullValue anymore (since they would carry a type as well).

It's likely that "inlining" of compiler generated code will exaggerate this problem as it will apply to all of the fields on closures as well (the plan to track them as basically local variables, or something very similar).

We've discussed the idea of "wrapper" values for a while now, and actually used that for by-ref values. Maybe we should embrace it fully and swallow the associated cost - it is potentially very flexible and powerful (not only the above and by-refs, but also better warnings, easier compiler generated code and so on).

Ideas, opinions???

vitek-karas commented 2 years ago

https://github.com/dotnet/linker/pull/2823 adds a test for this (currently expects the warning though).

sbomer commented 2 years ago

For me the main architectural question is whether to store the types as part of the tracked values, or as part of the containers:

With GetType we are already using more than just the static type info: the static type of the instance is object, but the intrinsic handling relies on us passing along the "static" type of the argument in the caller. So the question is whether we want to keep that as a special case, or generalize it so that we can track "dynamic" types too.

What behavior do we want for this case?

public class C {
    public void M() {
        Base b = new Derived();
        var t = b.GetType();
        var methods = t.GetMethods(); // should this warn?
    }
}

class Base {}

[DAM(DAMT.PublicMethods)]
class Derived : Base {}

If we want to avoid warning above, I think that would eliminate the "Typed stack" approach.

The static types would probably be straightforward to track in the analyzer since every IOperation has a Type already. I don't even think we need a side-channel in the common case.

For tracking dynamic types, if we track them as part of the values this will naturally extend to the way we use values in the analyzer too. I agree that the "Local variable value" approach isn't the best since it's incomplete (and I find the concept confusing) - so for dynamic types that leaves "Static typed values" (which I would actually call "dynamic typed values" 😉 ) or "Typed wrappers".

I don't have a strong preference between the two and I would prioritize simplicity unless we have evidence that there are performance problems. FWIW I don't think the "typed wrappers" will be overly complex, and I generally like composing types from simpler types because it leverages the type system to help provide correctness. (It might make it harder to accidentally forget to take the type info into account if you have to unwrap the typed wrappers).

agocke commented 2 years ago

I'm not sure I follow the problem. Looking at the IL, the section that sounds like the problem is:

 .maxstack 5
        .locals init (
            [0] class DataGridViewCell cell,
            [1] class [System.Runtime]System.Reflection.ConstructorInfo ctor
        )

        IL_0000: ldarg.s destinationType
        IL_0002: ldstr "destinationType"
        IL_0007: call void [System.Runtime]System.ArgumentNullException::ThrowIfNull(object, string)
        IL_000c: ldarg.s destinationType
        IL_000e: ldtoken [System.ComponentModel.TypeConverter]System.ComponentModel.Design.Serialization.InstanceDescriptor
        IL_0013: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
        IL_0018: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type, class [System.Runtime]System.Type)
        IL_001d: brfalse.s IL_004a

        IL_001f: ldarg.3
        IL_0020: isinst DataGridViewCell
        IL_0025: stloc.0
        IL_0026: ldloc.0
        IL_0027: brfalse.s IL_004a

        IL_0029: ldloc.0
        IL_002a: callvirt instance class [System.Runtime]System.Type [System.Runtime]System.Object::GetType()
        IL_002f: call !!0[] [System.Runtime]System.Array::Empty<class [System.Runtime]System.Type>()
        IL_0034: callvirt instance class [System.Runtime]System.Reflection.ConstructorInfo [System.Runtime]System.Type::GetConstructor(class [System.Runtime]System.Type[])
        IL_0039: stloc.1
        IL_003a: ldloc.1

It looks like we're doing a callvirt to GetType() on ldloc.0 which has a static type of DataGridViewCell. Why is the static type object here?

sbomer commented 2 years ago

The problem is what @vitek-karas described here: https://github.com/dotnet/runtime/issues/93720. To rephrase, we don't take into account the static type of the local even though the information is all there in IL. We just track "a value that comes from the parameter of type object", even as it's assigned to the local with a more specific type.

vitek-karas commented 2 years ago

Prioritization (based on offline discussion): This is not super important, except for this bug we didn't run into this problem anywhere yet. And while the workaround is not great it does exist, and the situation should be pretty rare (the usage GetType() is already pretty rare on its own).

MichalStrehovsky commented 2 years ago

I think the .NET Native analyzer used the typed stack approach. The analyzer also ran on all code (not just the code that does something interesting) and it was fine perf-wise.

Handling stack merge points would become a bit more tricky since the type of the thing on the evaluation stack could be different, coming through different paths (e.g. System.Object coming from one branch and System.String from another). We would probably need to merge to a common parent.

ghost commented 11 months ago

Tagging subscribers to this area: @agocke, @sbomer, @vitek-karas See info in area-owners.md if you want to be subscribed.

Issue Details
I attempt to annotate WinForms for linker. I have code approximately like this ```csharp // [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] // fix class DataGridViewCell { public virtual object Clone() { DataGridViewCell dataGridViewCell = (DataGridViewCell)System.Activator.CreateInstance(GetType()); // no warning return dataGridViewCell; } } class DataGridViewCellConverter { public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { ArgumentNullException.ThrowIfNull(destinationType); if (destinationType == typeof(InstanceDescriptor) && value is DataGridViewCell cell) { ConstructorInfo? ctor = cell.GetType().GetConstructor(Array.Empty()); // warning if (ctor is not null) { return new InstanceDescriptor(ctor, Array.Empty(), false); } } return base.ConvertTo(context, culture, value, destinationType); } } ``` If I add `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]` to `DataGridViewCell` type, then issue in Clone goes away, but in Converter stays, and complain that it needs `PublicParameterlessConstructor`. In my understanding these two versions of code absolutely identical and either I should have a fix for both places, or have warnings in both places. https://github.com/dotnet/winforms/blob/5427959215dfc57d4ecf082ba5f65dc597e2c2b9/src/System.Windows.Forms/src/System/Windows/Forms/DataGridViewCell.cs#L1106-L1111 and https://github.com/dotnet/winforms/blob/5427959215dfc57d4ecf082ba5f65dc597e2c2b9/src/System.Windows.Forms/src/System/Windows/Forms/DataGridViewCellConverter.cs#L25-L39
Author: kant2002
Assignees: -
Labels: `area-Tools-ILLink`, `needs-area-label`
Milestone: Future
vitek-karas commented 11 months ago

The problem exists in all implementation, trimmer, analyzer, NativeAOT currently.