Roslyn: Issue with "unmanaged" constraint

Created on 18 May 2018  路  20Comments  路  Source: dotnet/roslyn

Version Used:
15.7.1

Here is the to demonstrate the issue:

class Program
{
    public interface IInterface<T>
        where T : unmanaged
    {
    }

    public struct Struct<T> : IInterface<T>
        where T : unmanaged
    {
    }

    public struct StructByte : IInterface<byte>
    {
    }

    public class Class<T, U>
        where T : unmanaged
        where U : unmanaged, IInterface<T>
    {
    }

    static void Main(string[] args)
    {
        var c1 = new Class<byte, StructByte>();     // ok
        var c2 = new Class<byte, Struct<byte>>();   // error CS8377
    }
}

CS8377:

The type 'Program.Struct' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as parameter 'U' in the generic type or method 'Program.Class'

Since the 'unmanaged' specs are quite limited, I ask it here: is it expected behaviour? Why Struct<byte> is considered as managed?

Most helpful comment

It's by-design so far and there is the same proposal in dotnet/csharplang#1504.

Not sure to follow the "by design". Why a structure having an unmanaged constraint is not propagated? This looks more like a "bug" to me. Why should it go through csharplang while it is mainly a fix in Roslyn, what do a spec would bring to the table for this? (I might miss something, so I'm really wondering what's missing)

All 20 comments

It's by-design so far and there is the same proposal in https://github.com/dotnet/csharplang/issues/1504.

We should track this issue at dotnet/csharplang#1504.

It's by-design so far and there is the same proposal in dotnet/csharplang#1504.

Not sure to follow the "by design". Why a structure having an unmanaged constraint is not propagated? This looks more like a "bug" to me. Why should it go through csharplang while it is mainly a fix in Roslyn, what do a spec would bring to the table for this? (I might miss something, so I'm really wondering what's missing)

Roslyn cannot fix this without a language and CLR change. There is no way to generate verifiable code for a fixed field of type Struct<T> where T: unmanaged. Yes, I agree it feels like it should work. But the language spec for unmanaged does not specify that it should work, and for good reason.

Roslyn cannot fix this without a language and CLR change. There is no way to generate verifiable code for a fixed field of type Struct where T: unmanaged. Yes, I agree it feels like it should work. But the language spec for unmanaged does not specify that it should work, and for good reason.

Thanks. So you are saying that this is blocked only by fixed field? (e.g public fixed T values[5]) For years, we were not able to use anything else than a primitive there (and even worse, on certain .NET platform, fixed was not even supported) and they can always be emulated with regular fields and a bit more plumber. I don't mind that fixed field would still not support unmanaged constraint while regular fields would. In that case, wouldn't it be easier for Roslyn then?

Btw, do you have an issue that link to this unverifiable problem for fixed field with unmanaged constraint?, this is not fully clear to me why it is not verifiable.

It is also stackalloc. We don鈥檛 have an issue because it isn鈥檛 a bug. You can check the verifier spec to see what it says about stackalloc.

but explicit stackalloc T[5] for me is in the same corner case than fixed fields.
Blocking unmanaged constraint to be used with fixed field in struct or stackalloc is perfectly fine, as long as we can unlock immediately standard generic field in structs. That's much more practical than waiting for years to get all the cases working. We would not have any breaking changes.
So why not unlocking the most common case today?

What does the verifier spec say about the constraints on what type may be the referent of a pointer type?

What do you mean by verifier spec? PEVerify?

PEVerify is an implementation of the verifier part of the spec. I mean what does the CLI (Common Language Infrastructure) specification, which documents the semantics of .NET assemblies, say about what types may be the referent of a pointer type?

what does the CLI (Common Language Infrastructure) specification, which documents the semantics of .NET assemblies, say about what types may be the referent of a pointer type?

@gafter I haven't been able to recover this information from the specs.

But I still don't understand that this is allowed:

```c#
public unsafe static void Process(T value) where T : unmanaged
{
// At IL level pValue is effectively T*
var pValue = &value;
....
}
struct MyStruct { public int a; }

// Allowed
Process();
// Allowed
Process();

but using a managed struct with a unmanaged constraint would not work:

```C#
struct MyGenericStruct<T> where T : unmanaged
{
      public int a;
      public T value;
}

// Not allowed
Process<MyGenericStruct<int>>

Which is perfectly valid at IL level. T* would work for MyGenericStruct<int> and it would be safe to use it because the compiler would be able to protect it. If the type MyGenericStruct is unmanaged and its <T> is unmanaged by constraint, the type MyGenericStruct<T> is unmanaged.

I don't understand the "verifiability" requirement here (I'm really not familiar with verifiability in .NET), but as soon as we are using pointers this is not verifiable either and we have been living with this for years (e.g typically T* above )

So I still stand that it looks to me a Roslyn issue and not a csharplang or CLR issue, the CLR being perfectly able to handle T* as long as T is unmanaged (cc: @jkotas can you confirm?)

@xoofx

搂23.3 in http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf

An unmanaged-type is any type that isn鈥檛 a reference-type, a type-parameter, or a constructed type

搂23.3 in http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf

An unmanaged-type is any type that isn鈥檛 a reference-type, a type-parameter, or a constructed type

I was looking at ECMA-335 (the CLI part). This ECMA C# specs is not updated with the new un-managed constraint either right? The unamanaged constraint is precisely unlocking this, so yeah, likely the "An unmanaged type is" should be reformulated. But I would like to understand why it would require a change to the CLR (apart the corner case of fixed field for example)

This typically just works, the CLR is not blocking anything here:

```C#
using System;

namespace ConsoleApp1
{
public struct MyUnmanagedConstructredType where T : unmanaged
{

}

public static class UnmanagedHelper
{
    public static unsafe void* Process<T>(T value) where T : unmanaged
    {
        return &value;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var value = new MyUnmanagedConstructredType<int>();
        var method = typeof(UnmanagedHelper).GetMethod("Process").MakeGenericMethod(typeof(MyUnmanagedConstructredType<int>));
        var pointer = method.Invoke(null, new object[] {value});
        Console.WriteLine($"0x{pointer:8X}");
    }
}

}
```

This is really blocking us at Unity to actually embrace the usage of the unmanaged constraint. We have to do our own "unmanaged" constraint at runtime to let the constraint T accepting any unmanaged types which is quite laborious and inefficient while we could have all of this resolved at compilation time.

And on the subject of fixed fields:

C# public unsafe struct MyUnmanagedConstructredType<T> where T : unmanaged { public fixed T Values[5]; }

This is not allowed today at compilation time, even with the unmanaged constraint. And if the CLR doesn't support this case fair enough. We can proceed later with a CLR refinement on this.

But blocking the propagation of the unamanaged constraints for generic constructed types because of this fixed field unmanaged types issue is misleading. We can proceed step by step, as it is already done today (fixed field with unmanaged types are not supported) and unlock restrictions that can be easily unlocked today (unmanaged generic constructed type).

It is also stackalloc. We don鈥檛 have an issue because it isn鈥檛 a bug. You can check the verifier spec to see what it says about stackalloc.

Side note for stackalloc with T unmanaged which is working today:

C# public static class UnmanagedHelper { public static unsafe void* Process<T>(T value) where T : unmanaged { var stack = stackalloc T[5]; stack[0] = value; return stack; // this is bad to a stack address, but just for example purpose } }

the CLR being perfectly able to handle T* as long as T is unmanaged

As far as CLR or CLI are concerned, pointers can point to anything. For example, you can have Object* just fine. It is valid non-verifiable IL.

unmanaged-type and unmanaged-type constrains are C# specific constructs unfortunately. unmanaged-type constrains are not enforced by CLR. You can easily violate them using reflection.

More discussion about this at https://github.com/dotnet/corefx/issues/29119#issuecomment-381437081

As far as CLR or CLI are concerned, pointers can point to anything. For example, you can have Object* just fine. It is valid non-verifiable IL.

But the CLR doesn't track Object* as a GC reference right?

unmanaged-type and unmanaged-type constrains are C# specific constructs unfortunately. unmanaged-type constrains are not enforced by CLR. You can easily violate them using reflection.

I'm a bit confused. We can violate many things with IL and reflection today. But this is the point of the unmanaged constraint to bring at least Roslyn compilation safety even if the CLR doesn't enforce it, so that we don't have to rely on IL-rewrite or to unrelax our generic constraints (and to check them at runtime instead with static generics).

Now, we can use T* in C#, so this is great. We are just asking to propagate the constraint so that T can be an unmanaged constructed type like MyStruct<T1> where T1 : unmanaged

But the CLR doesn't track Object* as a GC reference right?

Right, you have to ensure that the place that the pointer is pointing to is pinned. There is no difference between int* or Object* in this regard.

We can violate many things with IL and reflection today.

All constraints except for the unmanaged one are enforce by reflection today. It makes the unmanaged constraint an outlier.

So the unmanaged_type should not exclude constructed types as it does not require CLR changes.

I would like this to be put in the specs and we will come back here for Roslyn.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

NikChao picture NikChao  路  3Comments

vbcodec picture vbcodec  路  3Comments

MadsTorgersen picture MadsTorgersen  路  3Comments

ashmind picture ashmind  路  3Comments

AdamSpeight2008 picture AdamSpeight2008  路  3Comments