diff --git a/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs b/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs index 6cf0d81..162d2cb 100644 --- a/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs +++ b/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs @@ -24,3 +24,6 @@ [assembly: TypeForwardedTo(typeof(Utf8Parser))] [assembly: TypeForwardedTo(typeof(ArrayPool<>))] + +[assembly: TypeForwardedTo(typeof(SpanAction<,>))] +[assembly: TypeForwardedTo(typeof(ReadOnlySpanAction<,>))] diff --git a/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs b/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs new file mode 100644 index 0000000..8951448 --- /dev/null +++ b/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs @@ -0,0 +1,5 @@ +namespace System.Buffers +{ + public delegate void SpanAction(Span span, TArg arg); + public delegate void ReadOnlySpanAction(ReadOnlySpan span, TArg arg); +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/CharEx.cs b/src/MonoMod.Backports/System/CharEx.cs new file mode 100644 index 0000000..cb40e40 --- /dev/null +++ b/src/MonoMod.Backports/System/CharEx.cs @@ -0,0 +1,91 @@ +namespace System +{ + public static class CharEx + { + // there is no real reason to forward these, they are all very simple + extension(char) + { + /// Indicates whether a character is categorized as an ASCII letter. + /// The character to evaluate. + /// true if is an ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive, + /// or 'a' through 'z', inclusive. + /// + public static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a'; + + /// Indicates whether a character is categorized as a lowercase ASCII letter. + /// The character to evaluate. + /// true if is a lowercase ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'a' through 'z', inclusive. + /// + public static bool IsAsciiLetterLower(char c) => IsBetween(c, 'a', 'z'); + + /// Indicates whether a character is categorized as an uppercase ASCII letter. + /// The character to evaluate. + /// true if is an uppercase ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive. + /// + public static bool IsAsciiLetterUpper(char c) => IsBetween(c, 'A', 'Z'); + + /// Indicates whether a character is categorized as an ASCII digit. + /// The character to evaluate. + /// true if is an ASCII digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive. + /// + public static bool IsAsciiDigit(char c) => IsBetween(c, '0', '9'); + + /// Indicates whether a character is categorized as an ASCII letter or digit. + /// The character to evaluate. + /// true if is an ASCII letter or digit; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive, + /// 'a' through 'z', inclusive, or '0' through '9', inclusive. + /// + public static bool IsAsciiLetterOrDigit(char c) => IsAsciiLetter(c) | IsBetween(c, '0', '9'); + + /// Indicates whether a character is categorized as an ASCII hexadecimal digit. + /// The character to evaluate. + /// true if is a hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// 'A' through 'F', inclusive, or 'a' through 'f', inclusive. + /// + public static bool IsAsciiHexDigit(char c) => IsAsciiDigit(c) || IsBetween(c, 'a', 'f') || IsBetween(c, 'A', 'F'); + + /// Indicates whether a character is categorized as an ASCII upper-case hexadecimal digit. + /// The character to evaluate. + /// true if is a hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// or 'A' through 'F', inclusive. + /// + public static bool IsAsciiHexDigitUpper(char c) => IsAsciiDigit(c) || IsBetween(c, 'A', 'F'); + + /// Indicates whether a character is categorized as an ASCII lower-case hexadecimal digit. + /// The character to evaluate. + /// true if is a lower-case hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// or 'a' through 'f', inclusive. + /// + public static bool IsAsciiHexDigitLower(char c) => IsAsciiDigit(c) || IsBetween(c, 'a', 'f'); + + /// Indicates whether a character is within the specified inclusive range. + /// The character to evaluate. + /// The lower bound, inclusive. + /// The upper bound, inclusive. + /// true if is within the specified range; otherwise, false. + /// + /// The method does not validate that is greater than or equal + /// to . If is less than + /// , the behavior is undefined. + /// + public static bool IsBetween(char c, char minInclusive, char maxInclusive) => + (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + } + } +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs new file mode 100644 index 0000000..2a31eff --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: TypeForwardedTo(typeof(CollectionsMarshal))] diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs new file mode 100644 index 0000000..e74178a --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices +{ + public static class CollectionsMarshal + { + public static Span AsSpan(List? list) + { + if (list is null) + { + return Span.Empty; + } + + return Unsafe.As(CollectionsMarshalEx.ListFieldHolder.ItemsField.GetValue(list)); + } + } +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs new file mode 100644 index 0000000..bacec4c --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs @@ -0,0 +1,184 @@ +#if NET5_0_OR_GREATER +#define HAS_ASSPAN +#endif +#if NET6_0_OR_GREATER +#define HAS_GETVALUEREF +#endif +#if NET8_0_OR_GREATER +#define HAS_SETCOUNT +#endif + +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices +{ + public static unsafe class CollectionsMarshalEx + { +#if !HAS_SETCOUNT + internal static class ListFieldHolder + { +#if !HAS_ASSPAN + public static FieldInfo ItemsField; +#endif + public static FieldInfo CountField; + public static FieldInfo? VersionField; + + static ListFieldHolder() + { + var t = typeof(List); + +#if !HAS_ASSPAN + ItemsField = t.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get List items field"); +#endif + CountField = t.GetField("_count", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get List count field"); + VersionField = t.GetField("_version", BindingFlags.Instance | BindingFlags.NonPublic); + } + } +#endif + +#if !HAS_GETVALUEREF + private static class DictRelfectionHolder where TKey : notnull + { + public delegate ref TValue? EntryValueFieldRefGetter(Dictionary dict, TKey key); + public static EntryValueFieldRefGetter GetEntryValueFieldRef; + + static DictRelfectionHolder() + { + var dictType = typeof(Dictionary); + + var findValueMethod = dictType.GetMethod("FindValue", BindingFlags.Instance | BindingFlags.NonPublic, + null, [typeof(TKey)], null); + + if (findValueMethod is not null) + { + GetEntryValueFieldRef = (EntryValueFieldRefGetter)Delegate.CreateDelegate(typeof(EntryValueFieldRefGetter), findValueMethod); + return; + } + + var entriesField = dictType.GetField("_entries", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get dictionary entries array field"); + + var entryType = entriesField.FieldType.GetElementType()!; + var entryValueField = entryType.GetField("value", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!; + + var findEntryMethod = dictType.GetMethod("FindEntry", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get dictionary find entry method"); + + var dm = new DynamicMethod("GetEntryValueFieldRef", typeof(TValue).MakeByRefType(), [dictType, typeof(TKey)], typeof(CollectionExtensionsEx), true); + var il = dm.GetILGenerator(); + + var entryIndex = il.DeclareLocal(typeof(int)); + var successLabel = il.DefineLabel(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, findEntryMethod); + il.Emit(OpCodes.Stloc, entryIndex); + il.Emit(OpCodes.Ldloc, entryIndex); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Bge, successLabel); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + // im not taking any risks here + il.Emit(OpCodes.Call, typeof(Unsafe).GetMethod("AsRef", [typeof(void*)])!.MakeGenericMethod(typeof(TValue))); + il.Emit(OpCodes.Ret); + il.MarkLabel(successLabel); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, entriesField); + il.Emit(OpCodes.Ldloc, entryIndex); + il.Emit(OpCodes.Ldelema, entriesField.FieldType.GetElementType()!); + il.Emit(OpCodes.Ldflda, entryValueField); + il.Emit(OpCodes.Ret); + + GetEntryValueFieldRef = (EntryValueFieldRefGetter)dm.CreateDelegate(typeof(EntryValueFieldRefGetter)); + } + } +#endif + + extension(CollectionsMarshal) + { + public static void SetCount(List list, int count) + { +#if HAS_SETCOUNT + CollectionsMarshal.SetCount(list, count); +#else + ArgumentNullException.ThrowIfNull(list); + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + // setting the version field only really needs to be best effort + if (ListFieldHolder.VersionField is { } versionField) + { + versionField.SetValue(list, (int)versionField.GetValue(list) + 1); + } + + if (count > list.Capacity) + { + // taken from List.EnsureCapacity + var newCapacity = list.Capacity == 0 ? 4 : 2 * list.Capacity; + + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > Array.MaxLength) + { + newCapacity = Array.MaxLength; + } + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newCapacity < count) + { + newCapacity = count; + } + + list.Capacity = newCapacity; + } + + // TODO: IsReferenceOrContainsReferences + if (count < list.Count) + { + CollectionsMarshal.AsSpan(list).Slice(count + 1).Clear(); + } + + ListFieldHolder.CountField.SetValue(list, count); +#endif + } + + public static ref TValue GetValueRefOrNullRef(Dictionary dict, TKey key) + where TKey : notnull + { +#if HAS_GETVALUEREF + return ref CollectionsMarshal.GetValueRefOrNullRef(dict, key); +#else + // they don't validate for null so neither will we + return ref DictRelfectionHolder.GetEntryValueFieldRef(dict, key)!; +#endif + } + + public static ref TValue? GetValueRefOrAddDefault(Dictionary dict, TKey key) + where TKey : notnull + { +#if HAS_GETVALUEREF + return ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key); +#else + // they don't validate for null so neither will we + if (dict.ContainsKey(key)) + { + return ref DictRelfectionHolder.GetEntryValueFieldRef(dict, key); + } + + dict.Add(key, default!); + return ref DictRelfectionHolder.GetEntryValueFieldRef(dict, key); +#endif + } + } + } +} \ No newline at end of file