Skip to content
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

Implement RuntimeHelpers.Box #100561

Merged
merged 12 commits into from
Apr 6, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,48 @@ private static unsafe void DispatchTailCalls(
}
}
}

/// <summary>
/// Create a boxed object of the specified type from the data located at the target reference.
/// </summary>
/// <param name="target">The target data</param>
/// <param name="type">The type of box to create.</param>
/// <returns>A boxed object containing the specified data.</returns>
/// <exception cref="ArgumentNullException">The specified type handle is <c>null</c>.</exception>
/// <exception cref="ArgumentException">The specified type cannot have a boxed instance of itself created.</exception>
/// <exception cref="NotSupportedException">The passed in type is a by-ref-like type.</exception>
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved
public static unsafe object? Box(ref byte target, RuntimeTypeHandle type)
{
if (type.IsNullHandle())
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.type);

TypeHandle handle = type.GetNativeTypeHandle();

if (handle.IsTypeDesc)
throw new ArgumentException(SR.Arg_TypeNotSupported);

MethodTable* pMT = handle.AsMethodTable();

if (pMT->ContainsGenericVariables)
throw new ArgumentException(SR.Arg_TypeNotSupported);

if (pMT->IsValueType)
{
if (pMT->IsByRefLike)
throw new NotSupportedException(SR.NotSupported_ByRefLike);

if (MethodTable.AreSameType(pMT, (MethodTable*)RuntimeTypeHandle.ToIntPtr(typeof(void).TypeHandle)))
throw new ArgumentException(SR.Arg_TypeNotSupported);

object? result = Box(pMT, ref target);
GC.KeepAlive(type);
return result;
}
else
{
return Unsafe.As<byte, object?>(ref target);
}
}
}
// Helper class to assist with unsafe pinning of arbitrary objects.
// It's used by VM code.
Expand Down Expand Up @@ -523,6 +565,7 @@ internal unsafe struct MethodTable

// WFLAGS_HIGH_ENUM
private const uint enum_flag_ContainsPointers = 0x01000000;
private const uint enum_flag_ContainsGenericVariables = 0x20000000;
private const uint enum_flag_HasComponentSize = 0x80000000;
private const uint enum_flag_HasTypeEquivalence = 0x02000000;
private const uint enum_flag_Category_Mask = 0x000F0000;
Expand Down Expand Up @@ -630,6 +673,8 @@ public bool IsConstructedGenericType
}
}

public bool ContainsGenericVariables => (Flags & enum_flag_ContainsGenericVariables) != 0;

/// <summary>
/// Gets a <see cref="TypeHandle"/> for the element type of the current type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ internal bool IsNullHandle()
return m_type == null;
}

internal TypeHandle GetNativeTypeHandle()
{
return m_type.GetNativeTypeHandle();
}

internal static bool IsTypeDefinition(RuntimeType type)
{
CorElementType corElemType = GetCorElementType(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public static T ReadUnaligned<T>(void* source)
throw new PlatformNotSupportedException();
}

/// <summary>
/// Reads a value of type <typeparamref name="T"/> from the given location.
/// </summary>
[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T ReadUnaligned<T>(ref readonly byte source)
{
throw new PlatformNotSupportedException();
}

/// <summary>
/// Copies bytes from the source address to the destination address.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ public static unsafe object RhNewArray(MethodTable* pEEType, int length)
[RuntimeExport("RhBox")]
public static unsafe object RhBox(MethodTable* pEEType, ref byte data)
{
// A null can be passed for boxing of a null ref.
_ = Unsafe.ReadUnaligned<byte>(ref data);
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved

ref byte dataAdjustedForNullable = ref data;

// Can box non-ByRefLike value types only (which also implies no finalizers).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,44 @@ public static unsafe object GetUninitializedObject(

return RuntimeImports.RhNewObject(mt);
}

/// <summary>
/// Create a boxed object of the specified type from the data located at the target reference.
/// </summary>
/// <param name="target">The target data</param>
/// <param name="type">The type of box to create.</param>
/// <returns>A boxed object containing the specified data.</returns>
/// <exception cref="ArgumentNullException">The specified type handle is <c>null</c>.</exception>
/// <exception cref="ArgumentException">The specified type cannot have a boxed instance of itself created.</exception>
/// <exception cref="NotSupportedException">The passed in type is a by-ref-like type.</exception>
public static unsafe object? Box(ref byte target, RuntimeTypeHandle type)
{
if (type.IsNull)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.type);

MethodTable* mt = type.ToMethodTable();

if (mt->ElementType == EETypeElementType.Void || mt->IsGenericTypeDefinition || mt->IsByRef || mt->IsPointer || mt->IsFunctionPointer)
throw new ArgumentException(SR.Arg_TypeNotSupported);

if (mt->NumVtableSlots == 0)
{
// This is a type without a vtable or GCDesc. We must not allow creating an instance of it
throw ReflectionCoreExecution.ExecutionEnvironment.CreateMissingMetadataException(Type.GetTypeFromHandle(type));
}
// Paranoid check: not-meant-for-GC-heap types should be reliably identifiable by empty vtable.
Debug.Assert(!mt->ContainsGCPointers || RuntimeImports.RhGetGCDescSize(mt) != 0);

if (!mt->IsValueType)
{
return Unsafe.As<byte, object>(ref target);
}

if (mt->IsByRefLike)
throw new NotSupportedException(SR.NotSupported_ByRefLike);

return RuntimeImports.RhBox(mt, ref target);
}
}

// CLR arrays are laid out in memory as follows (multidimensional array bounds are optional):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,18 @@ internal static RuntimeTypeHandle RawTargetTypeToken(TypedReference value)

public static unsafe object ToObject(TypedReference value)
{
RuntimeTypeHandle typeHandle = value._typeHandle;
if (typeHandle.IsNull)
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved
RuntimeTypeHandle handle = RawTargetTypeToken(value);

if (handle.IsNull)
ThrowHelper.ThrowArgumentException_ArgumentNull_TypedRefType();

MethodTable* eeType = typeHandle.ToMethodTable();
if (eeType->IsValueType)
{
return RuntimeImports.RhBox(eeType, ref value.Value);
}
else if (eeType->IsPointer || eeType->IsFunctionPointer)
MethodTable* mt = handle.ToMethodTable();
if (mt->IsPointer || mt->IsFunctionPointer)
{
return RuntimeImports.RhBox(MethodTable.Of<UIntPtr>(), ref value.Value);
}
else
{
return Unsafe.As<byte, object>(ref value.Value);
handle = typeof(UIntPtr).TypeHandle;
}

return RuntimeHelpers.Box(ref value.Value, handle);
}

public static void SetTypedReference(TypedReference target, object? value) { throw new NotSupportedException(); }
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13149,6 +13149,7 @@ public static partial class RuntimeHelpers
[System.ObsoleteAttribute("OffsetToStringData has been deprecated. Use string.GetPinnableReference() instead.")]
public static int OffsetToStringData { get { throw null; } }
public static System.IntPtr AllocateTypeAssociatedMemory(System.Type type, int size) { throw null; }
public static object? Box(ref byte target, System.RuntimeTypeHandle type) { throw null; }
public static System.ReadOnlySpan<T> CreateSpan<T>(System.RuntimeFieldHandle fldHandle) { throw null; }
public static void EnsureSufficientExecutionStack() { }
public static new bool Equals(object? o1, object? o2) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,173 @@ public static void FixedAddressValueTypeTest()

Assert.Equal(fixedPtr1, fixedPtr2);
}

[Fact]
public static void BoxPrimitive()
{
int value = 4;
object result = RuntimeHelpers.Box(ref Unsafe.As<int, byte>(ref value), typeof(int).TypeHandle);
Assert.Equal(value, Assert.IsType<int>(result));
}

[Fact]
public static void BoxPointer()
{
Assert.Throws<ArgumentException>(() =>
{
nint value = 3;
object result = RuntimeHelpers.Box(ref Unsafe.As<nint, byte>(ref value), typeof(void*).TypeHandle);
});
}

[StructLayout(LayoutKind.Sequential)]
private ref struct ByRefLikeType
{
public int i;
}

[Fact]
public static void BoxByRefLike()
{
Assert.Throws<NotSupportedException>(() =>
{
int value = 3;
object result = RuntimeHelpers.Box(ref Unsafe.As<int, byte>(ref value), typeof(ByRefLikeType).TypeHandle);
});
}

[Fact]
public static void BoxStruct()
{
Span<int> buffer = [0, 42, int.MaxValue];
StructWithoutReferences expected = new()
{
a = buffer[0],
b = buffer[1],
c = buffer[2]
};
object result = RuntimeHelpers.Box(ref MemoryMarshal.AsBytes(buffer)[0], typeof(StructWithoutReferences).TypeHandle);

Assert.Equal(expected, Assert.IsType<StructWithoutReferences>(result));
}

[StructLayout(LayoutKind.Sequential)]
private struct GenericStruct<T>
{
public T data;
}

[Fact]
public static void BoxUnmanagedGenericStruct()
{
int value = 3;
object result = RuntimeHelpers.Box(ref Unsafe.As<int, byte>(ref value), typeof(GenericStruct<int>).TypeHandle);

Assert.Equal(value, Assert.IsType<GenericStruct<int>>(result).data);
}

[Fact]
public static void BoxManagedGenericStruct()
{
object value = new();
object result = RuntimeHelpers.Box(ref Unsafe.As<object, byte>(ref value), typeof(GenericStruct<object>).TypeHandle);

Assert.Same(value, Assert.IsType<GenericStruct<object>>(result).data);
}

[Fact]
public static void BoxNullable()
{
float? value = 3.14f;
object result = RuntimeHelpers.Box(ref Unsafe.As<float?, byte>(ref value), typeof(float?).TypeHandle);
Assert.Equal(value, Assert.IsType<float>(result));
}

[Fact]
public static void BoxNullNullable()
{
float? value = null;
object? result = RuntimeHelpers.Box(ref Unsafe.As<float?, byte>(ref value), typeof(float?).TypeHandle);
Assert.Null(result);
}

[Fact]
public static void NullBox()
{
Assert.Throws<NullReferenceException>(() => RuntimeHelpers.Box(ref Unsafe.NullRef<byte>(), typeof(byte).TypeHandle));
}

[Fact]
public static void BoxNullTypeHandle()
{
Assert.Throws<ArgumentNullException>(() =>
{
byte value = 3;
RuntimeHelpers.Box(ref value, default(RuntimeTypeHandle));
});
}

[Fact]
public static void BoxReferenceType()
{
string str = "ABC";
Assert.Same(str, RuntimeHelpers.Box(ref Unsafe.As<string, byte>(ref str), typeof(string).TypeHandle));
}

[Fact]
public static void BoxArrayType()
{
string[] arr = ["a", "b", "c"];
Assert.Same(arr, RuntimeHelpers.Box(ref Unsafe.As<string[], byte>(ref arr), typeof(string[]).TypeHandle));
}

// We can't even get a RuntimeTypeHandle for a generic parameter type on NativeAOT,
// so we don't even get to the method we're testing.
// So, let's not even waste time running this test on NativeAOT
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotNativeAot))]
public static void BoxGenericParameterType()
{
Type t = typeof(List<>).GetGenericArguments()[0];
Assert.Throws<ArgumentException>(() =>
{
byte value = 3;
RuntimeHelpers.Box(ref value, t.TypeHandle);
});
}

// We can't even get a RuntimeTypeHandle for a partially instantiated generic type on NativeAOT,
// so we don't even get to the method we're testing.
// So, let's not even waste time running this test on NativeAOT
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotNativeAot))]
public static void BoxPartiallyOpenGeneric()
{
Type t = typeof(Dictionary<,>).MakeGenericType(typeof(object), typeof(Dictionary<,>).GetGenericArguments()[1]);
Assert.Throws<ArgumentException>(() =>
{
byte value = 3;
RuntimeHelpers.Box(ref value, t.TypeHandle);
});
}

[Fact]
public static void BoxGenericTypeDefinition()
{
Assert.Throws<ArgumentException>(() =>
{
byte value = 3;
RuntimeHelpers.Box(ref value, typeof(List<>).TypeHandle);
});
}

[Fact]
public static void BoxVoid()
{
Assert.Throws<ArgumentException>(() =>
{
byte value = 3;
RuntimeHelpers.Box(ref value, typeof(void).TypeHandle);
});
}
}

public struct Age
Expand Down
Loading
Loading