Skip to content

Relax validtion of MakeGeneric* on struct and unmanaged constraint#1888

Merged
vitek-karas merged 3 commits intodotnet:mainfrom
vitek-karas:StructConstraint
Mar 12, 2021
Merged

Relax validtion of MakeGeneric* on struct and unmanaged constraint#1888
vitek-karas merged 3 commits intodotnet:mainfrom
vitek-karas:StructConstraint

Conversation

@vitek-karas
Copy link
Member

struct and unmanaged constraint also imply new() constraint, but any type allowable for struct or unmanaged constraints always has public parameterless constructor. Or rather the type can be constructed without parameters (the .ctor itself is actually not accessible through reflection).

Since MakeGenericType and MakeGenericMethod both enforce that only types compatible with struct or unmanaged constraints can be passed as type arguments to such generic parameters, there's no need for linker to validate this as well. At runtime it will always work - and on top of that, there's nothing for linker to mark - struct and unmanaged don't have a parameterless .ctor in IL - it's just that at runtime they behave as if they do.

So enforcing the new() constraint in MakeGenericMethod or MakeGenericType makes sense only when it's just new() constraint, not if it's implied from struct or unmanaged constraints.

Fixes #1887

`struct` and `unmanaged` constraint also imply `new()` constraint, but any type allowable for `struct` or `unmanaged` constraints always has public parameterless constructor. Or rather the type can be constructed without parameters (the .ctor itself is actually not accessible through reflection).

Since MakeGenericType and MakeGenericMethod both enforce that only types compatible with `struct` or `unmanaged` constraints can be passed as type arguments to such generic parameters, there's no need for linker to validate this as well. At runtime it will always work - and on top of that, there's nothing for linker to mark - struct and unmanaged don't have a parameterless .ctor in IL - it's just that at runtime they behave as if they do.

So enforcing the `new()` constraint in `MakeGenericMethod` or `MakeGenericType` makes sense only when it's just `new()` constraint, not if it's implied from `struct` or `unmanaged` constraints.
@vitek-karas
Copy link
Member Author

/cc @eerhardt

@eerhardt
Copy link
Member

I was thinking a little more about this tonight. I wonder if we could change Roslyn to only put a HasDefaultConstructorConstraint marker in the IL only if someone actually calls new T() in the generic code. But when a "normal" where T : struct is used, and no code calls new T(), then don't put the HasDefaultConstructorConstraint marker in the IL.

This would solve all the cases in dotnet/runtime where it is currently warning - Nullable<T>, EnumConverter<T>, and NullableConverter<T>. All those classes have where T : struct, but no one calls new T().

@MichalStrehovsky
Copy link
Member

Oh this is annoying. Roslyn is considering adding support for parameterless constructors in C# 10 so this fix might potentially make us miss constructors. I would expect this to print "Hello" but I don't know if there's anything that would prevent us from stripping the constructor and make this go with the implicit instance of Foo instead.

typeof(Factory<>).MakeGenericType(typeof(Foo)).GetMethod("MakeNew").Invoke(null, Array.Empty<object>());

class Factory<T> where T: struct
{
    public static T MakeNew() => new T();
}

struct Foo
{
    public Foo() => Console.WriteLine("Hello");
}
Here's ILDASM
//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.7.3081.0
//  Copyright (c) Microsoft Corporation.  All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
}
.assembly NetCoreConsoleApp1
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                                   63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

  // --- The following custom attribute is added automatically, do not uncomment -------
  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) 

  .custom instance void [mscorlib]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 18 2E 4E 45 54 43 6F 72 65 41 70 70 2C 56   // ....NETCoreApp,V
                                                                                                              65 72 73 69 6F 6E 3D 76 35 2E 30 01 00 54 0E 14   // ersion=v5.0..T..
                                                                                                              46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 79   // FrameworkDisplay
                                                                                                              4E 61 6D 65 00 )                                  // Name.
  .custom instance void [mscorlib]System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 12 4E 65 74 43 6F 72 65 43 6F 6E 73 6F 6C   // ...NetCoreConsol
                                                                                                      65 41 70 70 31 00 00 )                            // eApp1..
  .custom instance void [mscorlib]System.Reflection.AssemblyConfigurationAttribute::.ctor(string) = ( 01 00 05 44 65 62 75 67 00 00 )                   // ...Debug..
  .custom instance void [mscorlib]System.Reflection.AssemblyFileVersionAttribute::.ctor(string) = ( 01 00 07 31 2E 30 2E 30 2E 30 00 00 )             // ...1.0.0.0..
  .custom instance void [mscorlib]System.Reflection.AssemblyInformationalVersionAttribute::.ctor(string) = ( 01 00 05 31 2E 30 2E 30 00 00 )                   // ...1.0.0..
  .custom instance void [mscorlib]System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 12 4E 65 74 43 6F 72 65 43 6F 6E 73 6F 6C   // ...NetCoreConsol
                                                                                                      65 41 70 70 31 00 00 )                            // eApp1..
  .custom instance void [mscorlib]System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 12 4E 65 74 43 6F 72 65 43 6F 6E 73 6F 6C   // ...NetCoreConsol
                                                                                                    65 41 70 70 31 00 00 )                            // eApp1..
  .permissionset reqmin
             = {[mscorlib]System.Security.Permissions.SecurityPermissionAttribute = {property bool 'SkipVerification' = bool(true)}}
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module NetCoreConsoleApp1.dll
// MVID: {6FDF96EC-2A11-4F4E-901E-CAB0C5621F24}
.custom instance void [mscorlib]System.Security.UnverifiableCodeAttribute::.ctor() = ( 01 00 00 00 ) 
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00020003    //  ILONLY 32BITPREFERRED
// Image base: 0x000002AD80060000


// =============== CLASS MEMBERS DECLARATION ===================

.class private abstract auto ansi sealed beforefieldinit '<Program>$'
       extends [mscorlib]System.Object
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  .method private hidebysig static void  '<Main>$'(string[] args) cil managed
  {
    .entrypoint
    // Code size       57 (0x39)
    .maxstack  8
    IL_0000:  ldtoken    Factory`1
    IL_0005:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_000a:  ldc.i4.1
    IL_000b:  newarr     [mscorlib]System.Type
    IL_0010:  dup
    IL_0011:  ldc.i4.0
    IL_0012:  ldtoken    Foo
    IL_0017:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_001c:  stelem.ref
    IL_001d:  callvirt   instance class [mscorlib]System.Type [mscorlib]System.Type::MakeGenericType(class [mscorlib]System.Type[])
    IL_0022:  ldstr      "MakeNew"
    IL_0027:  callvirt   instance class [mscorlib]System.Reflection.MethodInfo [mscorlib]System.Type::GetMethod(string)
    IL_002c:  ldnull
    IL_002d:  call       !!0[] [mscorlib]System.Array::Empty<object>()
    IL_0032:  callvirt   instance object [mscorlib]System.Reflection.MethodBase::Invoke(object,
                                                                                              object[])
    IL_0037:  pop
    IL_0038:  ret
  } // end of method '<Program>$'::'<Main>$'

} // end of class '<Program>$'

.class private auto ansi beforefieldinit Factory`1<valuetype .ctor ([mscorlib]System.ValueType) T>
       extends [mscorlib]System.Object
{
  .method public hidebysig static !T  MakeNew() cil managed
  {
    // Code size       6 (0x6)
    .maxstack  8
    IL_0000:  call       !!0 [mscorlib]System.Activator::CreateInstance<!T>()
    IL_0005:  ret
  } // end of method Factory`1::MakeNew

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Factory`1::.ctor

} // end of class Factory`1

.class private sequential ansi sealed beforefieldinit Foo
       extends [mscorlib]System.ValueType
{
  .pack 0
  .size 1
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       12 (0xc)
    .maxstack  8
    IL_0000:  ldstr      "Hello"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  nop
    IL_000b:  ret
  } // end of method Foo::.ctor

} // end of class Foo


// =============================================================

// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file NetCoreConsoleApp1.res

@MichalStrehovsky
Copy link
Member

The C# team just discussed it in the LDM: https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-03-10.md#parameterless-struct-constructors

There's mentions of private parameterless constructors so that might mess with things. The meeting notes are kind of hand-wavy "This may be a case where we have to simply hold our noses and compromise on letting the runtime exception happen.".

Maybe we'll just have to hold our noses too. I haven't sorted all of the implications of this in my head yet.

@MichalStrehovsky
Copy link
Member

What we could do is that we would extend the analysis of MakeGenericType so that if we find out there's annotations, we would still try to prove they're save (if there's direct typeof like in my example above, we would keep the ctor - if there's a System.Type, we would require it to be annotated).

It's linker work though and not sure how messy that would be on the consumption side of things.

@vitek-karas
Copy link
Member Author

We could also just simply mark all parameterless .ctors on structs. Currently this won't do anything on C# generated code, if C# adds the feature we would start marking those .ctors. It's not ideal from size perspective, but it's probably not terrible either. as @eerhardt pointed out earlier - there are way too many places in the framework itself where Nullable<> is used with MakeGenericType and where it would be really hard if not impossible to enforce the PublicParameterlessConstructor annotation.

We could obviously special case Nullable<> in the linker, but it doesn't feel right either.

@vitek-karas
Copy link
Member Author

Discussed this with @MichalStrehovsky and given the C# development we're not comfortable with just allowing all struct constraints through. Ideally C# would add a new syntax for a struct without new constraints, as they're effectively breaking the struct constrain as it works today.

So the new proposed behavior of linker is:

  • Continue treating struct as new (as it was before this PR)
  • Make an exception for Nullable<> - calling typeof(Nullable<>).MakeGenericType() will not warn. This is basically hardcoding the fact that Nullable<> doesn't make use of the implied new() constraint and technically should not have it.

… a special case.

Nullable<> doesn't make use of the implied new() constraint, so there's no reason for linker to enforce it. It's by far the most common case where this is problematic - that's why the special handling.
foreach (var genericParameter in typeValue.TypeRepresented.GenericParameters) {
if (_context.Annotations.FlowAnnotations.GetGenericParameterAnnotation (genericParameter) != DynamicallyAccessedMemberTypes.None ||
genericParameter.HasDefaultConstructorConstraint) {
(genericParameter.HasDefaultConstructorConstraint && !typeValue.TypeRepresented.IsTypeOf ("System", "Nullable"))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember - does Cecil also try to unmangle C# or should this rather be

Suggested change
(genericParameter.HasDefaultConstructorConstraint && !typeValue.TypeRepresented.IsTypeOf ("System", "Nullable"))) {
(genericParameter.HasDefaultConstructorConstraint && !typeValue.TypeRepresented.IsTypeOf ("System", "Nullable`1"))) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - I wrote a test for it, but then forgot to call the test :-(
All fixed now.

@vitek-karas vitek-karas merged commit 47c00c1 into dotnet:main Mar 12, 2021
@vitek-karas vitek-karas deleted the StructConstraint branch March 12, 2021 13:53
agocke pushed a commit to dotnet/runtime that referenced this pull request Nov 16, 2022
…otnet/linker#1888)

`struct` and `unmanaged` constraint also imply `new()` constraint, but any type allowable for `struct` or `unmanaged` constraints always has public parameterless constructor. Or rather the type can be constructed without parameters (the .ctor itself is actually not accessible through reflection).

Since MakeGenericType and MakeGenericMethod both enforce that only types compatible with `struct` or `unmanaged` constraints can be passed as type arguments to such generic parameters, there's no need for linker to validate this as well. At runtime it will always work - and on top of that, there's nothing for linker to mark - struct and unmanaged don't have a parameterless .ctor in IL - it's just that at runtime they behave as if they do.

So enforcing the `new()` constraint in `MakeGenericMethod` or `MakeGenericType` makes sense only when it's just `new()` constraint, not if it's implied from `struct` or `unmanaged` constraints.

* Change the behavior to not relax but instead single out Nullable<> as a special case.

Nullable<> doesn't make use of the implied new() constraint, so there's no reason for linker to enforce it. It's by far the most common case where this is problematic - that's why the special handling.

Commit migrated from dotnet/linker@47c00c1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New IL2055 warnings with latest linker

3 participants