Runtime: Add unmanaged constraint to MemoryMarshal.Cast

Created on 15 Apr 2018  路  13Comments  路  Source: dotnet/runtime

With C# 7.3 there's now the unmanaged generic constraint and it looks like it should be applied to MemoryMarshal.Cast before that becomes a breaking change

area-System.Memory

Most helpful comment

Right, the unmanaged constrain is both too limited and too dangerous at the same time. I do not think it is very useful. I wish that C# just removed the restriction on what can pointers point to instead (e.g. turned pointers pointing to potentially non-blittable types into warnings). It would get us on path towards removing dependency on S.R.CS.Unsafe and similar hacks.

All 13 comments

cc: @ahsonkhan, @jkotas

What do older compilers do when they see this constraint on that API?

@jnm2
The unmanaged constraint is implemented as modreq, with version 2.3 of roslyn you get

error CS0315: The type 'Consumer.Test' cannot be used as type parameter 'T' in the generic type or method 'MemoryMarshal.Cast<T>(T)'. There is no boxing conversion from 'Consumer.Test' to '?'.
error CS0648: '' is a type not supported by the language

Would also apply to anything that checked IsReferenceOrContainsReferences and then threw an exception?

public static partial class MemoryMarshal
{
    public static T Read<T>(System.ReadOnlySpan<byte> source) where T : unmanaged { throw null; }
    public static bool TryRead<T>(ReadOnlySpan<byte> source, out T value) where T : unmanaged { throw null; }
    public static void Write<T>(Span<byte> destination, ref T value) where T : unmanaged { throw null; }
    public static bool TryWrite<T>(Span<byte> destination, ref T value) where T : unmanaged { throw null; }
    public static Span<byte> AsBytes<T>(Span<T> span) where T : unmanaged { throw null; }
    public static ReadOnlySpan<byte> AsBytes<T>(ReadOnlySpan<T> span) where T : unmanaged { throw null; }
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span)
            where TFrom : unmanaged where TTo : unmanaged { throw null; }
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span)
            where TFrom : unmanaged where TTo : unmanaged { throw null; }
}

The unmanaged constraint is implemented as modreq, with version 2.3 of roslyn you get
error CS0315: The type 'Consumer.Test' cannot be used as type parameter 'T' in the generic type or method 'MemoryMarshal.Cast<T>(T)'. There is no boxing conversion from 'Consumer.Test' to '?'. error CS0648: '' is a type not supported by the language

Would that mean you could only call these apis with C#7.3+ ?

@benaadams Yes

In my opinion, lifting the required C# version to 7.3 isn't too much of a price to pay as all the Span stuff requires C# 7.2+ anyway and we gain additional compile time safety through this.

The unmanaged constrains are using modreq in a position where it was not ever used before. There are know issues in the existing tooling and runtimes that are not handling them correctly. It makes it risky to start using these at this point for 2.1. It has unpredictable bug-tail.

Other related discussions that touch of problems with unmanaged constrains:

My recomendation is won't fix.

The check and throw

if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
    ThrowHelper.ThrowInvalidTypeWithPointersNotSupported(typeof(T));

could be left in the method body to act as a runtime guard where its not valid?

i.e. the runtime doesn't need to rely on modreq working correctly; but it would act as a compile time discovery if/when the tooling is handling it correctly?

Though, I don't really have enough of an understanding of modreq to form an opinion.

@GrabYourPitchforks pointed out it would be problematic to call these functions from a method that you switch paths on an if statement with RuntimeHelpers.IsReferenceOrContainsReferences<T>() as the non-reference path then couldn't call the AsBytes method for example as it wouldn't be correctly constrained (even though the test validated it was a correct type)

Right, the unmanaged constrain is both too limited and too dangerous at the same time. I do not think it is very useful. I wish that C# just removed the restriction on what can pointers point to instead (e.g. turned pointers pointing to potentially non-blittable types into warnings). It would get us on path towards removing dependency on S.R.CS.Unsafe and similar hacks.

@jkotas Is T* for arbitrary T actually valid IL? Or is it one of those things that just fails at JIT time if T happens to contain references, so as long as we never JIT such a method we're good?

T* is valid IL, for any T. It is just one of those things that C# does not let you create.

You can actually get arbitrary T* locals in C# because of C# allows consuming T* for any T:

  • Write helper method in IL that looks like this T* AsPointerOfT<T>(void *p) and just returns p. BTW: I have looked into adding it to System.Runtime.CompilerServices.Unsafe but gave up on it because of the plumbing for the reference assembly would be very non-trivial.
  • Now you can write code like this that works perfectly fine:
static void foo<T>(void * p, int length)
{
    // pointerOfT is T*, for any T !!!
    var pointerOfT = AsPointerOfT<T>(p);
    for (int i = 0; i < length; i++)
        pointerOfT[i].GetHashCode();
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

btecu picture btecu  路  3Comments

nalywa picture nalywa  路  3Comments

matty-hall picture matty-hall  路  3Comments

omajid picture omajid  路  3Comments

noahfalk picture noahfalk  路  3Comments