Skip to content

Commit

Permalink
Rewrite Enum and add {ISpanFormattable}.TryFormat (#78580)
Browse files Browse the repository at this point in the history
* Add generic Enum.TryFormat method

* Rewrite Enum using generic specialization per underlying type

Also revises the shape of the newly added Enum.TryFormat method and fixes tests accordingly.

* Use Enum.TryFormat in interpolated string handlers

* Implement ISpanFormattable on Enum

* Use generic Enum methods in more places

* Replace Unsafe.As with unmanaged pointers where possible

Reduces cost of the generic methods

* Simplify Enum_GetValuesAndNames QCall, fix handling of floats and doubles

This change avoids allocation of the temporary ulong[] array during EnumInfo initialization

Fixes #29266

* Fix mono's GetEnumNamesAndValues, and make ToObject handle float/double as well

* Fix perf gap and polish

* Undo update of Enum.GetTypeCode to include float/double

* Address PR feedback

* Accomodate net8.0 TFM update

* Fix InvalidCastException from GetEnumValues on mono when dealing with bool-based enums

Co-authored-by: Heath Baron-Morgan <heathbm@outlook.com>
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
  • Loading branch information
3 people committed Dec 6, 2022
1 parent 6e0c046 commit 62f3eb2
Show file tree
Hide file tree
Showing 38 changed files with 2,361 additions and 1,355 deletions.
30 changes: 22 additions & 8 deletions src/coreclr/System.Private.CoreLib/src/System/Enum.CoreCLR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand All @@ -20,6 +20,15 @@ public abstract partial class Enum
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern unsafe CorElementType InternalGetCorElementType(MethodTable* pMT);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe CorElementType InternalGetCorElementType(RuntimeType rt)
{
Debug.Assert(rt.IsActualEnum);
CorElementType elementType = InternalGetCorElementType((MethodTable*)rt.GetUnderlyingNativeHandle());
GC.KeepAlive(rt);
return elementType;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe CorElementType InternalGetCorElementType()
{
Expand Down Expand Up @@ -73,26 +82,31 @@ internal static unsafe RuntimeType InternalGetUnderlyingType(RuntimeType enumTyp
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static EnumInfo GetEnumInfo(RuntimeType enumType, bool getNames = true)
private static EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(RuntimeType enumType, bool getNames = true)
where TUnderlyingValue : struct, INumber<TUnderlyingValue>
{
return enumType.GenericCache is EnumInfo info && (!getNames || info.Names is not null) ?
return enumType.GenericCache is EnumInfo<TUnderlyingValue> info && (!getNames || info.Names is not null) ?
info :
InitializeEnumInfo(enumType, getNames);

[MethodImpl(MethodImplOptions.NoInlining)]
static EnumInfo InitializeEnumInfo(RuntimeType enumType, bool getNames)
static EnumInfo<TUnderlyingValue> InitializeEnumInfo(RuntimeType enumType, bool getNames)
{
ulong[]? values = null;
TUnderlyingValue[]? values = null;
string[]? names = null;
RuntimeTypeHandle enumTypeHandle = enumType.TypeHandle;

GetEnumValuesAndNames(
new QCallTypeHandle(ref enumTypeHandle),
new QCallTypeHandle(ref enumType),
ObjectHandleOnStack.Create(ref values),
ObjectHandleOnStack.Create(ref names),
getNames ? Interop.BOOL.TRUE : Interop.BOOL.FALSE);

Debug.Assert(values!.GetType() == typeof(TUnderlyingValue[]));
Debug.Assert(!getNames || names!.GetType() == typeof(string[]));

bool hasFlagsAttribute = enumType.IsDefined(typeof(FlagsAttribute), inherit: false);

var entry = new EnumInfo(hasFlagsAttribute, values!, names!);
var entry = new EnumInfo<TUnderlyingValue>(hasFlagsAttribute, values, names!);
enumType.GenericCache = entry;
return entry;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,10 @@
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:System.Reflection.EnumInfo</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:System.Reflection.EnumInfo`1</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:System.Reflection.Runtime.General.MetadataReaderExtensions</Target>
Expand Down Expand Up @@ -972,4 +976,4 @@
<DiagnosticId>CP0016</DiagnosticId>
<Target>M:System.Runtime.InteropServices.Marshal.GetObjectForNativeVariant``1(System.IntPtr)-&gt;T?:[T:System.Diagnostics.CodeAnalysis.MaybeNullAttribute]</Target>
</Suppression>
</Suppressions>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;

using EETypeElementType = Internal.Runtime.EETypeElementType;

Expand Down Expand Up @@ -164,7 +165,8 @@ public abstract object ActivatorCreateInstance(

public abstract Assembly[] GetLoadedAssemblies();

public abstract EnumInfo GetEnumInfo(Type type);
public abstract EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(Type type)
where TUnderlyingValue : struct, INumber<TUnderlyingValue>;

public abstract DynamicInvokeInfo GetDelegateDynamicInvokeInfo(Type type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection.Runtime.General;
using System.Reflection.Runtime.TypeInfos;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -97,7 +98,9 @@ public abstract class ExecutionEnvironment
// Other
//==============================================================================================
public abstract FieldAccessor CreateLiteralFieldAccessor(object value, RuntimeTypeHandle fieldTypeHandle);
public abstract EnumInfo GetEnumInfo(RuntimeTypeHandle typeHandle);
public abstract EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(RuntimeTypeHandle typeHandle)
where TUnderlyingValue : struct, INumber<TUnderlyingValue>;

public abstract IntPtr GetDynamicInvokeThunk(MethodInvoker invoker);

//==============================================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime;
using System.Runtime.CompilerServices;
using Internal.Runtime.Augments;
using Internal.Runtime.CompilerServices;
using Internal.Reflection.Augments;

Expand All @@ -23,7 +25,29 @@ internal static EnumInfo GetEnumInfo(Type enumType, bool getNames = true)
Debug.Assert(enumType is RuntimeType);
Debug.Assert(enumType.IsEnum);

return ReflectionAugments.ReflectionCoreCallbacks.GetEnumInfo(enumType);
RuntimeType rt = (RuntimeType)enumType;
return Type.GetTypeCode(RuntimeAugments.GetEnumUnderlyingType(rt.TypeHandle)) switch
{
TypeCode.SByte => GetEnumInfo<sbyte>(rt),
TypeCode.Byte => GetEnumInfo<byte>(rt),
TypeCode.Int16 => GetEnumInfo<short>(rt),
TypeCode.UInt16 => GetEnumInfo<ushort>(rt),
TypeCode.Int32 => GetEnumInfo<int>(rt),
TypeCode.UInt32 => GetEnumInfo<uint>(rt),
TypeCode.Int64 => GetEnumInfo<long>(rt),
TypeCode.UInt64 => GetEnumInfo<ulong>(rt),
_ => throw new NotSupportedException(),
};
}

internal static EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(Type enumType, bool getNames = true)
where TUnderlyingValue : struct, INumber<TUnderlyingValue>
{
Debug.Assert(enumType != null);
Debug.Assert(enumType is RuntimeType);
Debug.Assert(enumType.IsEnum);

return ReflectionAugments.ReflectionCoreCallbacks.GetEnumInfo<TUnderlyingValue>(enumType);
}
#pragma warning restore

Expand All @@ -32,6 +56,12 @@ private static object InternalBoxEnum(Type enumType, long value)
return ToObject(enumType.TypeHandle.ToEETypePtr(), value);
}

private static CorElementType InternalGetCorElementType(RuntimeType rt)
{
Debug.Assert(rt.IsActualEnum);
return rt.TypeHandle.ToEETypePtr().CorElementType;
}

private CorElementType InternalGetCorElementType()
{
return this.GetEETypePtr().CorElementType;
Expand Down Expand Up @@ -115,14 +145,6 @@ internal static Type InternalGetUnderlyingType(RuntimeType enumType)
return GetEnumInfo(enumType).UnderlyingType;
}

public static TEnum[] GetValues<TEnum>() where TEnum : struct, Enum
{
Array values = GetEnumInfo(typeof(TEnum)).ValuesAsUnderlyingType;
TEnum[] result = new TEnum[values.Length];
Array.Copy(values, result, values.Length);
return result;
}

//
// Checks if value.GetType() matches enumType exactly.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,84 +4,46 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime;
using System.Runtime.CompilerServices;

namespace System.Reflection
{
[ReflectionBlocked]
public sealed class EnumInfo
public abstract class EnumInfo
{
public EnumInfo(Type underlyingType, object[] rawValues, string[] names, bool isFlags)
private protected EnumInfo(Type underlyingType, string[] names, bool isFlags)
{
Debug.Assert(rawValues.Length == names.Length);

UnderlyingType = underlyingType;

int numValues = rawValues.Length;
ulong[] values = new ulong[numValues];
for (int i = 0; i < numValues; i++)
{
object rawValue = rawValues[i];

ulong rawUnboxedValue;
if (rawValue is ulong)
{
rawUnboxedValue = (ulong)rawValue;
}
else
{
// This conversion is this way for compatibility: do a value-preseving cast to long - then store (and compare) as ulong. This affects
// the order in which the Enum apis return names and values.
rawUnboxedValue = (ulong)(((IConvertible)rawValue).ToInt64(null));
}
values[i] = rawUnboxedValue;
}

// Need to sort the `names` and `rawValues` arrays according to the `values` array
ulong[] valuesCopy = (ulong[])values.Clone();
Array.Sort(keys: valuesCopy, items: rawValues, comparer: Comparer<ulong>.Default);
Array.Sort(keys: values, items: names, comparer: Comparer<ulong>.Default);

Names = names;
Values = values;

// Create the unboxed version of values for the Values property to return. (We didn't do this earlier because
// declaring "rawValues" as "Array" would prevent us from using the generic overload of Array.Sort()).
//
// The array element type is the underlying type, not the enum type. (The enum type could be an open generic.)
ValuesAsUnderlyingType = Type.GetTypeCode(UnderlyingType) switch
{
TypeCode.Byte => new byte[numValues],
TypeCode.SByte => new sbyte[numValues],
TypeCode.UInt16 => new ushort[numValues],
TypeCode.Int16 => new short[numValues],
TypeCode.UInt32 => new uint[numValues],
TypeCode.Int32 => new int[numValues],
TypeCode.UInt64 => new ulong[numValues],
TypeCode.Int64 => new long[numValues],
_ => throw new NotSupportedException(),
};
Array.Copy(rawValues, ValuesAsUnderlyingType, numValues);

HasFlagsAttribute = isFlags;

ValuesAreSequentialFromZero = true;
for (int i = 0; i < values.Length; i++)
{
if (values[i] != (ulong)i)
{
ValuesAreSequentialFromZero = false;
break;
}
}
}

internal Type UnderlyingType { get; }
internal string[] Names { get; }
internal ulong[] Values { get; }
internal Array ValuesAsUnderlyingType { get; }
internal bool HasFlagsAttribute { get; }
}

[ReflectionBlocked]
public sealed class EnumInfo<TUnderlyingValue> : EnumInfo
where TUnderlyingValue : struct, INumber<TUnderlyingValue>
{
public EnumInfo(Type underlyingType, TUnderlyingValue[] values, string[] names, bool isFlags) :
base(underlyingType, names, isFlags)
{
Debug.Assert(values.Length == names.Length);

Array.Sort(keys: values, items: names);

Values = values;
ValuesAreSequentialFromZero = Enum.AreSequentialFromZero(values);
}

internal TUnderlyingValue[] Values { get; }
internal bool ValuesAreSequentialFromZero { get; }

public TUnderlyingValue[] CloneValues() =>
new ReadOnlySpan<TUnderlyingValue>(Values).ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,15 +404,15 @@ public sealed override void MakeTypedReference(object target, FieldInfo[] flds,

public sealed override Assembly[] GetLoadedAssemblies() => RuntimeAssemblyInfo.GetLoadedAssemblies();

public sealed override EnumInfo GetEnumInfo(Type type)
public sealed override EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(Type type)
{
RuntimeTypeInfo runtimeType = type.CastToRuntimeTypeInfo();

EnumInfo? info = runtimeType.GenericCache as EnumInfo;
EnumInfo<TUnderlyingValue>? info = runtimeType.GenericCache as EnumInfo<TUnderlyingValue>;
if (info != null)
return info;

info = ReflectionCoreExecution.ExecutionDomain.ExecutionEnvironment.GetEnumInfo(runtimeType.TypeHandle);
info = ReflectionCoreExecution.ExecutionDomain.ExecutionEnvironment.GetEnumInfo<TUnderlyingValue>(runtimeType.TypeHandle);
runtimeType.GenericCache = info;
return info;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ public abstract class RuntimeType : TypeInfo
// Do a value-preserving cast of both it and the enum values and do a 64-bit compare.

if (!IsActualEnum)
throw new ArgumentException(SR.Arg_MustBeEnum);
throw new ArgumentException(SR.Arg_MustBeEnum, "enumType");

return Enum.GetEnumName(this, rawValue);
return Enum.GetName(this, rawValue);
}

public sealed override string[] GetEnumNames()
{
if (!IsActualEnum)
throw new ArgumentException(SR.Arg_MustBeEnum, "enumType");

string[] ret = Enum.InternalGetNames(this);
string[] ret = Enum.GetNamesNoCopy(this);

// Make a copy since we can't hand out the same array since users can modify them
return new ReadOnlySpan<string>(ret).ToArray();
Expand Down Expand Up @@ -87,7 +87,7 @@ public sealed override bool IsEnumDefined(object value)
throw new ArgumentException(SR.Format(SR.Arg_EnumUnderlyingTypeAndObjectMustBeSameType, value.GetType(), underlyingType));
}

return Enum.GetEnumName(this, rawValue) != null;
return Enum.GetName(this, rawValue) != null;
}
}

Expand All @@ -97,16 +97,16 @@ public sealed override Array GetEnumValues()
if (!IsActualEnum)
throw new ArgumentException(SR.Arg_MustBeEnum, "enumType");

Array values = Enum.GetEnumInfo(this).ValuesAsUnderlyingType;
Array values = Enum.GetValuesAsUnderlyingTypeNoCopy(this);
int count = values.Length;

// Without universal shared generics, chances are slim that we'll have the appropriate
// array type available. Offer an escape hatch that avoids a missing metadata exception
// at the cost of a small appcompat risk.
Array result;
if (AppContext.TryGetSwitch("Switch.System.Enum.RelaxedGetValues", out bool isRelaxed) && isRelaxed)
result = Array.CreateInstance(Enum.InternalGetUnderlyingType(this), count);
else
result = Array.CreateInstance(this, count);
Array result = AppContext.TryGetSwitch("Switch.System.Enum.RelaxedGetValues", out bool isRelaxed) && isRelaxed ?
Array.CreateInstance(Enum.InternalGetUnderlyingType(this), count) :
Array.CreateInstance(this, count);

Array.Copy(values, result, values.Length);
return result;
}
Expand All @@ -116,7 +116,7 @@ public sealed override Array GetEnumValuesAsUnderlyingType()
if (!IsActualEnum)
throw new ArgumentException(SR.Arg_MustBeEnum, "enumType");

return (Array)Enum.GetEnumInfo(this).ValuesAsUnderlyingType.Clone();
return (Array)Enum.GetValuesAsUnderlyingTypeNoCopy(this).Clone();
}

internal bool IsActualEnum
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ namespace Internal.Reflection
{
internal class ReflectionCoreCallbacksImplementation : ReflectionCoreCallbacks
{
public override EnumInfo GetEnumInfo(Type type)
{
return new EnumInfo(
public override EnumInfo<TUnderlyingValue> GetEnumInfo<TUnderlyingValue>(Type type) =>
new EnumInfo<TUnderlyingValue>(
RuntimeAugments.GetEnumUnderlyingType(type.TypeHandle),
rawValues: Array.Empty<object>(),
values: Array.Empty<TUnderlyingValue>(),
names: Array.Empty<string>(),
isFlags: false);
}

public override DynamicInvokeInfo GetDelegateDynamicInvokeInfo(Type type)
=> throw new NotSupportedException(SR.Reflection_Disabled);
Expand Down
Loading

0 comments on commit 62f3eb2

Please sign in to comment.