From 6d4e71bdc6002d6d5b2c5aaf6be286f3c214090c Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 25 Aug 2025 15:52:27 -0500 Subject: [PATCH 01/14] {,ReadOnly}SpanAction --- .../System/Buffers,gte_std_2.1,gte_core_2.1.cs | 3 +++ .../Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs 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 From 5943dc5eb3d3dac09285b05d96e29cad3d15d2e5 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 25 Aug 2025 16:13:10 -0500 Subject: [PATCH 02/14] char.IsBetween and char.IsAsciiX --- src/MonoMod.Backports/System/CharEx.cs | 91 ++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/MonoMod.Backports/System/CharEx.cs 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 From 7dcbf41fd2179af646123d2d676f1c80d35dbb8a Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 25 Aug 2025 18:46:06 -0500 Subject: [PATCH 03/14] CollectionsMarshal --- .../CollectionsMarshal,gte_core_5.0.cs | 4 + ...ectionsMarshal,is_fx,is_std,lt_core_5.0.cs | 18 ++ .../InteropServices/CollectionsMarshalEx.cs | 184 ++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs create mode 100644 src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs create mode 100644 src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs 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 From fbb79027275a67203fe17a13eebebe53771be94b Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 25 Aug 2025 23:05:48 -0500 Subject: [PATCH 04/14] Remove GetValueRef methods --- .../InteropServices/CollectionsMarshalEx.cs | 95 +------------------ 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs index bacec4c..aaa3709 100644 --- a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs @@ -1,17 +1,12 @@ #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 { @@ -40,66 +35,6 @@ static ListFieldHolder() } } #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) { @@ -117,7 +52,7 @@ public static void SetCount(List list, int 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); + versionField.SetValue(list, (int)versionField.GetValue(list)! + 1); } if (count > list.Capacity) @@ -149,34 +84,6 @@ public static void SetCount(List list, int count) } 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 } } From 032569fab3d63f7f3343ba4c387abdcd4969feaf Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 20:27:30 -0600 Subject: [PATCH 05/14] CollectionsExtensions.{AddRange,InsertRange,CopyTo,AsReadOnly} --- .../MonoMod.Backports.Shims.csproj | 6 +- .../MonoMod.Backports.csproj | 2 +- .../Generic/CollectionExtensionsEx.cs | 100 +++++ ...nlyDictionary,gte_fx_4.5,is_core,is_std.cs | 4 + .../ReadOnlyDictionary,lt_fx_4.5.cs | 405 ++++++++++++++++++ ...ectionsMarshal,is_fx,is_std,lt_core_5.0.cs | 3 + .../InteropServices/CollectionsMarshalEx.cs | 7 + src/MonoMod.Backports/System/ThrowHelper.cs | 2 + 8 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs create mode 100644 src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs create mode 100644 src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs diff --git a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj index d9e8431..f0333e8 100644 --- a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj +++ b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj @@ -1,4 +1,4 @@ - + true @@ -18,6 +18,10 @@ + + + + diff --git a/src/MonoMod.Backports/MonoMod.Backports.csproj b/src/MonoMod.Backports/MonoMod.Backports.csproj index 11368ee..2e3cb02 100644 --- a/src/MonoMod.Backports/MonoMod.Backports.csproj +++ b/src/MonoMod.Backports/MonoMod.Backports.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs b/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs new file mode 100644 index 0000000..92e28c8 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs @@ -0,0 +1,100 @@ +#if NET8_0_OR_GREATER +#define HAS_LISTSPANMETHODS +#endif +#if NET7_0_OR_GREATER +#define HAS_ASREADONLY +#endif + +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +#if !HAS_LISTSPANMETHODS +using System.Runtime.InteropServices; +#endif + +namespace System.Collections.Generic +{ + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "Replicating existing APIs")] + public static class CollectionExtensionsEx + { + public static void AddRange( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, params ReadOnlySpan source + ) + { +#if HAS_LISTSPANMETHODS + list.AddRange(source); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + if (source.IsEmpty) + { + return; + } + var currentCount = list.Count; + CollectionsMarshal.SetCount(list, currentCount + source.Length); + source.CopyTo(CollectionsMarshal.AsSpan(list).Slice(currentCount + 1)); +#endif + } + + public static void InsertRange( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, int index, params ReadOnlySpan source + ) + { +#if HAS_LISTSPANMETHODS + list.InsertRange(index, source); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + if ((uint)index > (uint)list.Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index); + } + if (source.IsEmpty) + { + return; + } + var currentCount = list.Count; + CollectionsMarshal.SetCount(list, currentCount + source.Length); + var items = CollectionsMarshal.AsSpan(list); + if (index < currentCount) + { + items.Slice(index, currentCount - index).CopyTo(items.Slice(index + source.Length)); + } + source.CopyTo(items.Slice(index)); +#endif + } + + public static void CopyTo( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, Span destination + ) + { +#if HAS_LISTSPANMETHODS + list.CopyTo(destination); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + CollectionsMarshal.AsSpan(list).CopyTo(destination); +#endif + } + + public static ReadOnlyCollection AsReadOnly( +#if !HAS_ASREADONLY + this +#endif + IList list + ) => new(list); + + public static ReadOnlyDictionary AsReadOnly( +#if !HAS_ASREADONLY + this +#endif + IDictionary list + ) where TKey : notnull => new(list); + } +} diff --git a/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs new file mode 100644 index 0000000..b175366 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs @@ -0,0 +1,4 @@ +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(ReadOnlyDictionary<,>))] diff --git a/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs new file mode 100644 index 0000000..2eb1f90 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs @@ -0,0 +1,405 @@ +using System.Collections.Generic; + +namespace System.Collections.ObjectModel +{ + public class ReadOnlyDictionary : IDictionary, IDictionary where TKey : notnull + { + private readonly IDictionary m_dictionary; // Do not rename (binary serialization) + + private KeyCollection? _keys; + private ValueCollection? _values; + + public ReadOnlyDictionary(IDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + m_dictionary = dictionary; + } + + /// Gets an empty . + /// An empty . + /// The returned instance is immutable and will always be empty. + public static ReadOnlyDictionary Empty { get; } = new ReadOnlyDictionary(new Dictionary()); + + protected IDictionary Dictionary => m_dictionary; + + public KeyCollection Keys => _keys ??= new KeyCollection(m_dictionary.Keys); + + public ValueCollection Values => _values ??= new ValueCollection(m_dictionary.Values); + + public bool ContainsKey(TKey key) => m_dictionary.ContainsKey(key); + + ICollection IDictionary.Keys => Keys; + + public bool TryGetValue(TKey key, out TValue value) + { + return m_dictionary.TryGetValue(key, out value!); + } + + ICollection IDictionary.Values => Values; + + public TValue this[TKey key] => m_dictionary[key]; + + void IDictionary.Add(TKey key, TValue value) + { + throw new NotSupportedException(); + } + + bool IDictionary.Remove(TKey key) + { + throw new NotSupportedException(); + } + + TValue IDictionary.this[TKey key] + { + get => m_dictionary[key]; + set => throw new NotSupportedException(); + } + + public int Count => m_dictionary.Count; + + bool ICollection>.Contains(KeyValuePair item) + { + return m_dictionary.Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + m_dictionary.CopyTo(array, arrayIndex); + } + + bool ICollection>.IsReadOnly => true; + + void ICollection>.Add(KeyValuePair item) + { + throw new NotSupportedException(); + } + + void ICollection>.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotSupportedException(); + } + + public IEnumerator> GetEnumerator() + { + return m_dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)m_dictionary).GetEnumerator(); + } + + private static bool IsCompatibleKey(object key) + { + ArgumentNullException.ThrowIfNull(key); + + return key is TKey; + } + + void IDictionary.Add(object key, object? value) + { + throw new NotSupportedException(); + } + + void IDictionary.Clear() + { + throw new NotSupportedException(); + } + + bool IDictionary.Contains(object key) + { + return IsCompatibleKey(key) && ContainsKey((TKey)key); + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + if (m_dictionary is IDictionary d) + { + return d.GetEnumerator(); + } + return new DictionaryEnumerator(m_dictionary); + } + + bool IDictionary.IsFixedSize => true; + + bool IDictionary.IsReadOnly => true; + + ICollection IDictionary.Keys => Keys; + + void IDictionary.Remove(object key) + { + throw new NotSupportedException(); + } + + ICollection IDictionary.Values => Values; + + object? IDictionary.this[object key] + { + get + { + if (!IsCompatibleKey(key)) + { + return null; + } + + if (m_dictionary.TryGetValue((TKey)key, out TValue? value)) + { + return value; + } + else + { + return null; + } + } + set => throw new NotSupportedException(); + } + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.ValidateCopyToArguments(Count, array, index); + + if (array is KeyValuePair[] pairs) + { + m_dictionary.CopyTo(pairs, index); + } + else + { + if (array is DictionaryEntry[] dictEntryArray) + { + foreach (var item in m_dictionary) + { + dictEntryArray[index++] = new DictionaryEntry(item.Key, item.Value); + } + } + else + { + object[] objects = array as object[] ?? throw new ArgumentException("Incompatible array type", nameof(array)); + try + { + foreach (var item in m_dictionary) + { + objects[index++] = new KeyValuePair(item.Key, item.Value); + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + } + } + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (m_dictionary is ICollection coll) ? coll.SyncRoot : this; + + private readonly struct DictionaryEnumerator : IDictionaryEnumerator + { + private readonly IDictionary _dictionary; + private readonly IEnumerator> _enumerator; + + public DictionaryEnumerator(IDictionary dictionary) + { + _dictionary = dictionary; + _enumerator = _dictionary.GetEnumerator(); + } + + public DictionaryEntry Entry + { + get => new DictionaryEntry(_enumerator.Current.Key, _enumerator.Current.Value); + } + + public object Key => _enumerator.Current.Key; + + public object? Value => _enumerator.Current.Value; + + public object Current => Entry; + + public bool MoveNext() => _enumerator.MoveNext(); + + public void Reset() => _enumerator.Reset(); + } + + public sealed class KeyCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + internal KeyCollection(ICollection collection) + { + ArgumentNullException.ThrowIfNull(collection); + + _collection = collection; + } + + void ICollection.Add(TKey item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(TKey item) + { + return _collection.Contains(item); + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + _collection.CopyTo(array, arrayIndex); + } + + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + bool ICollection.Remove(TKey item) + { + throw new NotSupportedException(); + } + + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.CopyTo(_collection, array, index); + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (_collection is ICollection coll) ? coll.SyncRoot : this; + } + + public sealed class ValueCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + internal ValueCollection(ICollection collection) + { + ArgumentNullException.ThrowIfNull(collection); + + _collection = collection; + } + + void ICollection.Add(TValue item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(TValue item) => _collection.Contains(item); + + public void CopyTo(TValue[] array, int arrayIndex) + { + _collection.CopyTo(array, arrayIndex); + } + + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + bool ICollection.Remove(TValue item) + { + throw new NotSupportedException(); + } + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.CopyTo(_collection, array, index); + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (_collection is ICollection coll) ? coll.SyncRoot : this; + } + } + + internal static class CollectionHelpers + { + internal static void ValidateCopyToArguments(int sourceCount, Array array, int index) + { +#if NET + ArgumentNullException.ThrowIfNull(array); +#else + ArgumentNullException.ThrowIfNull(array); +#endif + + if (array.Rank != 1) + { + throw new ArgumentException("Multidimensional arrays are not supported", nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("Non-zero lower bound", nameof(array)); + } + +#if NET + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, array.Length); +#else + if (index < 0 || index > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } +#endif + + if (array.Length - index < sourceCount) + { + throw new ArgumentException("Array is too small for the index given"); + } + } + + internal static void CopyTo(ICollection collection, Array array, int index) + { + ValidateCopyToArguments(collection.Count, array, index); + + if (collection is ICollection nonGenericCollection) + { + // Easy out if the ICollection implements the non-generic ICollection + nonGenericCollection.CopyTo(array, index); + } + else if (array is T[] items) + { + collection.CopyTo(items, index); + } + else + { + // We can't cast array of value type to object[], so we don't support widening of primitive types here. + if (array is not object?[] objects) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + + try + { + foreach (T item in collection) + { + objects[index++] = item; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + } + } + } +} 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 index e74178a..11d840f 100644 --- 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 @@ -1,8 +1,11 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace System.Runtime.InteropServices { + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "Replicating existing APIs")] public static class CollectionsMarshal { public static Span AsSpan(List? list) diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs index aaa3709..a63136e 100644 --- a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs @@ -6,7 +6,10 @@ #endif using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +#if !HAS_SETCOUNT using System.Reflection; +#endif namespace System.Runtime.InteropServices { @@ -21,6 +24,8 @@ internal static class ListFieldHolder public static FieldInfo CountField; public static FieldInfo? VersionField; + [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline" + , Justification = "This advice is not very important here")] static ListFieldHolder() { var t = typeof(List); @@ -38,6 +43,8 @@ static ListFieldHolder() extension(CollectionsMarshal) { + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "This is replicating an existing API")] public static void SetCount(List list, int count) { #if HAS_SETCOUNT diff --git a/src/MonoMod.Backports/System/ThrowHelper.cs b/src/MonoMod.Backports/System/ThrowHelper.cs index 5d68b66..cb1977c 100644 --- a/src/MonoMod.Backports/System/ThrowHelper.cs +++ b/src/MonoMod.Backports/System/ThrowHelper.cs @@ -283,5 +283,7 @@ internal enum ExceptionArgument threadLocal, delay, millisecondsDelay, + list, + dictionary, } } \ No newline at end of file From 24e3d41a2233ed62a4d79d7642f875b53f7a4ae3 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 20:32:34 -0600 Subject: [PATCH 06/14] ISpanFormattable --- .../System/ISpanFormattable,gte_core_6.0.cs | 4 +++ ...panFormattable,is_fx,is_std,lt_core_6.0.cs | 19 ++++++++++++++ ...dStringHandler,is_std,is_fx,lt_core_6.0.cs | 26 +++++++++---------- 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs create mode 100644 src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs diff --git a/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs b/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs new file mode 100644 index 0000000..9519972 --- /dev/null +++ b/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs @@ -0,0 +1,4 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(ISpanFormattable))] diff --git a/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs b/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs new file mode 100644 index 0000000..35e54b3 --- /dev/null +++ b/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs @@ -0,0 +1,19 @@ +namespace System +{ + /// Provides functionality to format the string representation of an object into a span. + public interface ISpanFormattable : IFormattable + { + /// Tries to format the value of the current instance into the provided span of characters. + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// if the formatting was successful; otherwise, . + /// + /// An implementation of this interface should produce the same string of characters as an implementation of + /// on the same type. + /// TryFormat should return false only if there is not enough space in the destination buffer. Any other failures should throw an exception. + /// + bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider); + } +} diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs index f039ff7..8e0a375 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs @@ -317,17 +317,17 @@ public void AppendFormatted(T value) string? s; if (value is IFormattable) { - // If the value can format itself directly into our buffer, do so. - /*if (value is ISpanFormattable) { - int charsWritten; - while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types - { - Grow(); - } - - _pos += charsWritten; - return; - }*/ + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) { + int charsWritten; + while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types } @@ -376,7 +376,7 @@ public void AppendFormatted(T value, string? format) if (value is IFormattable) { // If the value can format itself directly into our buffer, do so. - /*if (value is ISpanFormattable) { + if (value is ISpanFormattable) { int charsWritten; while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) // constrained call avoiding boxing for value types { @@ -385,7 +385,7 @@ public void AppendFormatted(T value, string? format) _pos += charsWritten; return; - }*/ + } s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types } From a7b4e544ca727a079473adc42433d495c76782d4 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 20:41:58 -0600 Subject: [PATCH 07/14] Enumerable.Reverse(TSource[]) --- src/MonoMod.Backports/System/Linq/EnumerableEx.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/MonoMod.Backports/System/Linq/EnumerableEx.cs diff --git a/src/MonoMod.Backports/System/Linq/EnumerableEx.cs b/src/MonoMod.Backports/System/Linq/EnumerableEx.cs new file mode 100644 index 0000000..d902d94 --- /dev/null +++ b/src/MonoMod.Backports/System/Linq/EnumerableEx.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace System.Linq +{ + public static class EnumerableEx + { + public static IEnumerable Reverse( +#if !NET_10_OR_GREATER + this +#endif + TSource[] source) => Enumerable.Reverse(source); + } +} From 8e1ceb954033a684fc40aabc457ef508691fc6ff Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 21:20:05 -0600 Subject: [PATCH 08/14] RuntimeHelpers.{IsReferenceOrContainsReferences,GetUninitializedObject,TryEnsureSufficientExecutionStack} --- .../Memory.cs | 2 +- .../MemoryExtensions.Portable.cs | 6 +- .../ReadOnlyMemory.cs | 2 +- .../ReadOnlySpan.Portable.cs | 2 +- .../ConditionalWeakTable,lt_fx_4.0.cs | 4 +- .../CompilerServices/RuntimeHelpersEx.cs | 92 +++++++++++++++++++ 6 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs index 2948773..c62676c 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs @@ -279,7 +279,7 @@ public Span Span // and then cast to a Memory. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. - return new Span(s, (nint)RuntimeHelpers.OffsetToStringData, s.Length).Slice(_index, _length); + return new Span(s, (nint)RuntimeHelpersEx.OffsetToStringData, s.Length).Slice(_index, _length); } else if (_object != null) { diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs index c2ceada..f2a7a62 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs @@ -278,7 +278,7 @@ public static ReadOnlySpan AsSpan(this string? text) if (text == null) return default; - return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData, text.Length); + return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData, text.Length); } /// @@ -302,7 +302,7 @@ public static ReadOnlySpan AsSpan(this string? text, int start) if ((uint)start > (uint)text.Length) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData + start * sizeof(char), text.Length - start); + return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData + start * sizeof(char), text.Length - start); } /// @@ -327,7 +327,7 @@ public static ReadOnlySpan AsSpan(this string? text, int start, int length if ((uint)start > (uint)text.Length || (uint)length > (uint)(text.Length - start)) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData + start * sizeof(char), length); + return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData + start * sizeof(char), length); } /// Creates a new over the portion of the target string. diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs index 980e719..5e15d80 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs @@ -196,7 +196,7 @@ public ReadOnlySpan Span else if (typeof(T) == typeof(char) && _object is string s) { Debug.Assert(_length >= 0); - return new ReadOnlySpan(s, (nint)RuntimeHelpers.OffsetToStringData, s.Length).Slice(_index, _length); + return new ReadOnlySpan(s, (nint)RuntimeHelpersEx.OffsetToStringData, s.Length).Slice(_index, _length); } else if (_object != null) { diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs index 940f4b5..b93fda3 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs @@ -199,7 +199,7 @@ public override string ToString() if (typeof(T) == typeof(char)) { // If this wraps a string and represents the full length of the string, just return the wrapped string. - if (_byteOffset == (nint)RuntimeHelpers.OffsetToStringData) + if (_byteOffset == (nint)RuntimeHelpersEx.OffsetToStringData) { object? obj = Unsafe.As(_pinnable); // minimize chances the compilers will optimize away the 'is' check if (obj is string str && _length == str.Length) diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs index 9fffe33..4036cea 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs @@ -514,7 +514,7 @@ internal void CreateEntryNoResize(TKey key, TValue value) VerifyIntegrity(); _invalid = true; - int hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; + int hashCode = RuntimeHelpersEx.GetHashCode(key) & int.MaxValue; int newEntry = _firstFreeEntry++; _entries[newEntry].HashCode = hashCode; @@ -548,7 +548,7 @@ internal int FindEntry(TKey key, out object? value) { Debug.Assert(key != null); // Key already validated as non-null. - int hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; + int hashCode = RuntimeHelpersEx.GetHashCode(key) & int.MaxValue; int bucket = hashCode & (_buckets.Length - 1); for (int entriesIndex = Volatile.Read(ref _buckets[bucket]); entriesIndex != -1; entriesIndex = _entries[entriesIndex].Next) { diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs new file mode 100644 index 0000000..bc68654 --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs @@ -0,0 +1,92 @@ +#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#define HAS_ISREFORCONTAINSREF +#endif +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD1_3_OR_GREATER +#define HAS_ENSUREEXECSTACK +#endif +#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#define HAS_TRYENSUREEXECSTACK +#endif + +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Serialization; + +namespace System.Runtime.CompilerServices +{ + public static class RuntimeHelpersEx + { +#if !HAS_ISREFORCONTAINSREF + private static readonly ConditionalWeakTable> _isReferenceCache = new(); + private static readonly StrongBox _boxFalse = new(false); + private static readonly StrongBox _boxTrue = new(true); + + private static StrongBox GetIsRefOrContainsRef(Type t) + { + if (t.IsPrimitive || t.IsEnum || t.IsPointer) + { + return _boxFalse; + } + + if (!t.IsValueType) + { + return _boxTrue; + } + + foreach (var field in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (_isReferenceCache.GetValue(field.FieldType, GetIsRefOrContainsRef).Value) + { + return _boxTrue; + } + } + + return _boxFalse; + } +#endif + + extension(RuntimeHelpers) + { +#pragma warning disable SYSLIB0050 + public static object GetUninitializedObject( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) => FormatterServices.GetUninitializedObject(type); +#pragma warning restore SYSLIB0050 + + public static bool IsReferenceOrContainsReferences() => +#if HAS_ISREFORCONTAINSREF + RuntimeHelpers.IsReferenceOrContainsReferences(); +#else + _isReferenceCache.GetValue(typeof(T), GetIsRefOrContainsRef).Value; +#endif + + public static void EnsureSufficientExecutionStack() + { +#if HAS_ENSUREEXECSTACK + RuntimeHelpers.EnsureSufficientExecutionStack(); +#endif + // we do nothing + } + + public static bool TryEnsureSufficientExecutionStack() + { +#if HAS_TRYENSUREEXECSTACK + return RuntimeHelpers.TryEnsureSufficientExecutionStack(); +#elif HAS_ENSUREEXECSTACK + try + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + } + catch (InsufficientExecutionStackException) + { + return false; + } + return true; +#else + // just give up + return true; +#endif + } + } + } +} From 7252c9dfa160380c06ef60a2940003d0b3a6c8bb Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 21:52:32 -0600 Subject: [PATCH 09/14] Marshal.InitHandle --- .../System/Runtime/InteropServices/MarshalEx.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs index 4713c60..e4c1f4b 100644 --- a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using InlineIL; +using System.Runtime.CompilerServices; #if !NET6_0_OR_GREATER using System.Reflection; @@ -40,6 +41,14 @@ public static void SetLastPInvokeError(int error) del(error); #endif } + + public static void InitHandle(SafeHandle safeHandle, nint handle) + { + // this method always exists and is protected, we just need to call it + IL.Push(safeHandle); + IL.Push(handle); + IL.Emit.Call(MethodRef.Method(typeof(SafeHandle), "SetHandle", typeof(nint))); + } } } } From 2252c4557a72e4c0c2409505a0872c67c1586fb9 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 21:52:48 -0600 Subject: [PATCH 10/14] Index, Range, RuntimeHelpers.GetSubArray --- .../Index,is_fx,lt_std_2.1,lt_core_3.0.cs | 156 ++++++++++++++++++ .../IndexRange,gte_std_2.1,gte_core_3.0.cs | 5 + .../Range,is_fx,lt_std_2.1,lt_core_3.0.cs | 118 +++++++++++++ .../CompilerServices/RuntimeHelpersEx.cs | 18 ++ 4 files changed, 297 insertions(+) create mode 100644 src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs create mode 100644 src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs create mode 100644 src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs diff --git a/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs new file mode 100644 index 0000000..54c6783 --- /dev/null +++ b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -0,0 +1,156 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + public readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + Debug.Assert(formatted); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} diff --git a/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs b/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs new file mode 100644 index 0000000..14f293a --- /dev/null +++ b/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs @@ -0,0 +1,5 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(Index))] +[assembly: TypeForwardedTo(typeof(Range))] diff --git a/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs new file mode 100644 index 0000000..1ca6dc6 --- /dev/null +++ b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -0,0 +1,118 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + public readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) + { + span[0] = '^'; + pos = 1; + } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; + + if (End.IsFromEnd) + { + span[pos++] = '^'; + } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + Debug.Assert(formatted); + pos += charsWritten; + + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + ThrowArgumentOutOfRangeException(); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); + } + } +} diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs index bc68654..fef3d93 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs @@ -87,6 +87,24 @@ public static bool TryEnsureSufficientExecutionStack() return true; #endif } + + public static T[] GetSubArray(T[] array, Range range) + { + var (offset, length) = range.GetOffsetAndLength(array.Length); + T[] dest; + if (typeof(T[]) == array.GetType()) + { + dest = new T[length]; + } + else + { + dest = Unsafe.As(Array.CreateInstance(array.GetType().GetElementType()!, length)); + } + + Array.Copy(array, offset, dest, 0, length); + + return dest; + } } } } From c4aa7c6b0155eb0c4935e012cf2219ffb7ce18bc Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 22:02:44 -0600 Subject: [PATCH 11/14] Fix Marshal.InitHandle --- .../System/Runtime/InteropServices/MarshalEx.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs index e4c1f4b..0ce2914 100644 --- a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs @@ -44,10 +44,20 @@ public static void SetLastPInvokeError(int error) public static void InitHandle(SafeHandle safeHandle, nint handle) { - // this method always exists and is protected, we just need to call it + SafeHandleHelper.SetHandle(safeHandle, handle); + } + } + + private abstract class SafeHandleHelper : SafeHandle + { + private SafeHandleHelper() : base(default, default) => throw new NotSupportedException(); + + public static void SetHandle(SafeHandle safeHandle, nint handle) + { + // this method always exists and is accessible here, roslyn just wont let us call it since it is protected IL.Push(safeHandle); IL.Push(handle); - IL.Emit.Call(MethodRef.Method(typeof(SafeHandle), "SetHandle", typeof(nint))); + IL.Emit.Callvirt(MethodRef.Method(typeof(SafeHandle), "SetHandle", typeof(nint))); } } } From d2d382383db72f4095eea7810f4b4e4bca61a02a Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 22:12:34 -0600 Subject: [PATCH 12/14] Fix warnings and errors --- .../System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs | 4 ++-- .../System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs | 2 +- .../MemoryExtensions.Portable.cs | 6 +++--- .../Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs | 2 +- .../ReadOnlySpan.Portable.cs | 2 +- .../System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs | 5 ++--- .../CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs | 4 ++-- .../System/Runtime/CompilerServices/RuntimeHelpersEx.cs | 3 +++ 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs index 54c6783..669b55f 100644 --- a/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs +++ b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -12,6 +11,8 @@ namespace System /// int lastElement = someArray[^1]; // lastElement = 5 /// /// + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", + Justification = "Implementation taken straight from BCL")] public readonly struct Index : IEquatable { private readonly int _value; @@ -145,7 +146,6 @@ private string ToStringFromEnd() #if (!NETSTANDARD2_0 && !NETFRAMEWORK) Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); - Debug.Assert(formatted); span[0] = '^'; return new string(span.Slice(0, charsWritten + 1)); #else diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs index c62676c..2948773 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/Memory.cs @@ -279,7 +279,7 @@ public Span Span // and then cast to a Memory. Such a cast can only be done with unsafe or marshaling code, // in which case that's the dangerous operation performed by the dev, and we're just following // suit here to make it work as best as possible. - return new Span(s, (nint)RuntimeHelpersEx.OffsetToStringData, s.Length).Slice(_index, _length); + return new Span(s, (nint)RuntimeHelpers.OffsetToStringData, s.Length).Slice(_index, _length); } else if (_object != null) { diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs index f2a7a62..c2ceada 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/MemoryExtensions.Portable.cs @@ -278,7 +278,7 @@ public static ReadOnlySpan AsSpan(this string? text) if (text == null) return default; - return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData, text.Length); + return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData, text.Length); } /// @@ -302,7 +302,7 @@ public static ReadOnlySpan AsSpan(this string? text, int start) if ((uint)start > (uint)text.Length) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData + start * sizeof(char), text.Length - start); + return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData + start * sizeof(char), text.Length - start); } /// @@ -327,7 +327,7 @@ public static ReadOnlySpan AsSpan(this string? text, int start, int length if ((uint)start > (uint)text.Length || (uint)length > (uint)(text.Length - start)) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.start); - return new ReadOnlySpan(text, (nint)RuntimeHelpersEx.OffsetToStringData + start * sizeof(char), length); + return new ReadOnlySpan(text, (nint)RuntimeHelpers.OffsetToStringData + start * sizeof(char), length); } /// Creates a new over the portion of the target string. diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs index 5e15d80..980e719 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlyMemory.cs @@ -196,7 +196,7 @@ public ReadOnlySpan Span else if (typeof(T) == typeof(char) && _object is string s) { Debug.Assert(_length >= 0); - return new ReadOnlySpan(s, (nint)RuntimeHelpersEx.OffsetToStringData, s.Length).Slice(_index, _length); + return new ReadOnlySpan(s, (nint)RuntimeHelpers.OffsetToStringData, s.Length).Slice(_index, _length); } else if (_object != null) { diff --git a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs index b93fda3..940f4b5 100644 --- a/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs +++ b/src/MonoMod.Backports/System/Memory,is_fx,lt_core_2.1,lt_std_2.1/ReadOnlySpan.Portable.cs @@ -199,7 +199,7 @@ public override string ToString() if (typeof(T) == typeof(char)) { // If this wraps a string and represents the full length of the string, just return the wrapped string. - if (_byteOffset == (nint)RuntimeHelpersEx.OffsetToStringData) + if (_byteOffset == (nint)RuntimeHelpers.OffsetToStringData) { object? obj = Unsafe.As(_pinnable); // minimize chances the compilers will optimize away the 'is' check if (obj is string str && _length == str.Length) diff --git a/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs index 1ca6dc6..546c168 100644 --- a/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs +++ b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -13,6 +12,8 @@ namespace System /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } /// /// + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", + Justification = "Implementation taken straight from BCL")] public readonly struct Range : IEquatable { /// Represent the inclusive start index of the Range. @@ -60,7 +61,6 @@ public override string ToString() pos = 1; } bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); - Debug.Assert(formatted); pos += charsWritten; span[pos++] = '.'; @@ -71,7 +71,6 @@ public override string ToString() span[pos++] = '^'; } formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); - Debug.Assert(formatted); pos += charsWritten; return new string(span.Slice(0, pos)); diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs index 4036cea..9fffe33 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/ConditionalWeakTable,lt_fx_4.0.cs @@ -514,7 +514,7 @@ internal void CreateEntryNoResize(TKey key, TValue value) VerifyIntegrity(); _invalid = true; - int hashCode = RuntimeHelpersEx.GetHashCode(key) & int.MaxValue; + int hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; int newEntry = _firstFreeEntry++; _entries[newEntry].HashCode = hashCode; @@ -548,7 +548,7 @@ internal int FindEntry(TKey key, out object? value) { Debug.Assert(key != null); // Key already validated as non-null. - int hashCode = RuntimeHelpersEx.GetHashCode(key) & int.MaxValue; + int hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; int bucket = hashCode & (_buckets.Length - 1); for (int entriesIndex = Volatile.Read(ref _buckets[bucket]); entriesIndex != -1; entriesIndex = _entries[entriesIndex].Next) { diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs index fef3d93..4fbb938 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs @@ -9,7 +9,9 @@ #endif using System.Diagnostics.CodeAnalysis; +#if !HAS_ISREFORCONTAINSREF using System.Reflection; +#endif using System.Runtime.Serialization; namespace System.Runtime.CompilerServices @@ -90,6 +92,7 @@ public static bool TryEnsureSufficientExecutionStack() public static T[] GetSubArray(T[] array, Range range) { + ThrowHelper.ThrowIfArgumentNull(array, ExceptionArgument.array); var (offset, length) = range.GetOffsetAndLength(array.Length); T[] dest; if (typeof(T[]) == array.GetType()) From aa8628fb883011e2e5b815da9ba1e87012a8fb93 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 22:44:59 -0600 Subject: [PATCH 13/14] MemoryMarshal.{GetArrayDataReference,CreateReadOnlySpanFromNullTerminated,AsRef} --- .../InteropServices/MemoryMarshalEx.cs | 108 ++++++++++++++++++ src/MonoMod.Backports/System/ThrowHelper.cs | 3 + 2 files changed, 111 insertions(+) create mode 100644 src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs new file mode 100644 index 0000000..0b48d29 --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs @@ -0,0 +1,108 @@ +#if !NET6_0_OR_GREATER +using InlineIL; +#endif +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices +{ + public static unsafe class MemoryMarshalEx + { + extension(MemoryMarshal) + { + public static ref byte GetArrayDataReference(Array array) + { +#if NET6_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + IL.DeclareLocals(false, new LocalVar("pinned", typeof(Array)).Pinned()); + IL.Push(array); + IL.Emit.Stloc("pinned"); + return ref *(byte*)Marshal.UnsafeAddrOfPinnedArrayElement(array, 0); +#endif + } + + public static ref T GetArrayDataReference(T[] array) + { +#if NET5_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + return ref Unsafe.As(ref GetArrayDataReference((Array)array)); +#endif + } + + public static ref T AsRef(Span span) where T : struct + { +#if NETCOREAPP3_0_OR_GREATER + return ref MemoryMarshal.AsRef(span); +#else + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentException_TypeContainsReferences(typeof(T)); + } + if (span.Length < Unsafe.SizeOf()) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); +#endif + } + + public static ref readonly T AsRef(ReadOnlySpan span) where T : struct + { +#if NETCOREAPP3_0_OR_GREATER + return ref MemoryMarshal.AsRef(span); +#else + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentException_TypeContainsReferences(typeof(T)); + } + if (span.Length < Unsafe.SizeOf()) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); +#endif + } + + public static ReadOnlySpan CreateReadOnlySpanFromNullTerminated(byte* value) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(value); +#else + nint current = 0; + while (value[current] != 0) + { + current += 1; + } + + if (current > int.MaxValue) + { + ThrowHelper.ThrowArgumentException("Length would exceed int.MaxValue", nameof(value)); + } + + return new ReadOnlySpan(value, (int)current); +#endif + } + + public static ReadOnlySpan CreateReadOnlySpanFromNullTerminated(char* value) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(value); +#else + nint current = 0; + while (value[current] != '\0') + { + current += 1; + } + + if (current > int.MaxValue) + { + ThrowHelper.ThrowArgumentException("Length would exceed int.MaxValue", nameof(value)); + } + + return new ReadOnlySpan(value, (int)current); +#endif + } + } + } +} diff --git a/src/MonoMod.Backports/System/ThrowHelper.cs b/src/MonoMod.Backports/System/ThrowHelper.cs index cb1977c..37c6e32 100644 --- a/src/MonoMod.Backports/System/ThrowHelper.cs +++ b/src/MonoMod.Backports/System/ThrowHelper.cs @@ -224,6 +224,9 @@ private static Exception CreateArgumentValidationException(Array? array, int sta [DoesNotReturn, StackTraceHidden] internal static void ThrowArgumentException_TupleIncorrectType(object other) => throw new ArgumentException($"Value tuple of incorrect type (found {other.GetType()})", nameof(other)); + [DoesNotReturn, StackTraceHidden] + internal static void ThrowArgumentException_TypeContainsReferences(Type targetType) => throw new ArgumentException($"The type '{targetType}' is not supported because it contains references."); + // // ReadOnlySequence Slice validation Throws coalesced to enable inlining of the Slice // From 54a39beb98cc220c59d94d214ff22b0779a5860d Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 13 Jan 2026 23:32:29 -0600 Subject: [PATCH 14/14] string.{Create,CopyTo,TryCopyTo,ReplaceLineEndings,Concat} --- .../System/StringExtensions.cs | 263 ++++++++++++++++++ src/MonoMod.Backports/System/ThrowHelper.cs | 1 + 2 files changed, 264 insertions(+) diff --git a/src/MonoMod.Backports/System/StringExtensions.cs b/src/MonoMod.Backports/System/StringExtensions.cs index c195e00..0c630c1 100644 --- a/src/MonoMod.Backports/System/StringExtensions.cs +++ b/src/MonoMod.Backports/System/StringExtensions.cs @@ -2,7 +2,9 @@ #define HAS_STRING_COMPARISON #endif +using System.Buffers; using System.Runtime.CompilerServices; +using System.Text; namespace System { @@ -84,5 +86,266 @@ public static int IndexOf(this string self, char value, StringComparison compari return self.IndexOf(new string(value, 1), comparison); #endif } + + public static void CopyTo(this string self, Span destination) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + self.AsSpan().CopyTo(destination); + } + + public static bool TryCopyTo(this string self, Span destination) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + return self.AsSpan().TryCopyTo(destination); + } + + private const string NewLineChars = "\n\r\f\u0085\u2028\u2029"; + + public static string ReplaceLineEndings(this string self) => self.ReplaceLineEndings(Environment.NewLine); + + public static string ReplaceLineEndings(this string self, string replacementText) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + int idxOfFirstNewlineChar = IndexOfNewlineChar(self, replacementText, out int stride); + if (idxOfFirstNewlineChar < 0) + { + return self; + } + + var firstSegment = self.AsSpan(0, idxOfFirstNewlineChar); + var remaining = self.AsSpan(idxOfFirstNewlineChar + stride); + + var builder = new StringBuilder(); + while (true) + { + var idx = IndexOfNewlineChar(remaining, replacementText, out stride); + if (idx < 0) { break; } + builder.Append(replacementText); + builder.Append(remaining.Slice(0, idx)); + remaining = remaining.Slice(idx + stride); + } + + return string.Concat(firstSegment, builder.ToString(), replacementText, remaining); + } + + private static int IndexOfNewlineChar(ReadOnlySpan text, string replacementText, out int stride) + { + stride = default; + int offset = 0; + + while (true) + { + int idx = text.IndexOfAny(NewLineChars); + + if ((uint)idx >= (uint)text.Length) + { + return -1; + } + + offset += idx; + stride = 1; // needle found + + // Did we match CR? If so, and if it's followed by LF, then we need + // to consume both chars as a single newline function match. + + if (text[idx] == '\r') + { + int nextCharIdx = idx + 1; + if ((uint)nextCharIdx < (uint)text.Length && text[nextCharIdx] == '\n') + { + stride = 2; + + if (replacementText != "\r\n") + { + return offset; + } + } + else if (replacementText != "\r") + { + return offset; + } + } + else if (replacementText.Length != 1 || replacementText[0] != text[idx]) + { + return offset; + } + + offset += stride; + text = text.Slice(idx + stride); + } + } + + extension(string) + { + /// Creates a new string by using the specified provider to control the formatting of the specified interpolated string. + /// An object that supplies culture-specific formatting information. + /// The interpolated string. + /// The string that results for formatting the interpolated string using the specified format provider. + public static string Create(IFormatProvider? provider, [InterpolatedStringHandlerArgument(nameof(provider))] ref DefaultInterpolatedStringHandler handler) => + handler.ToStringAndClear(); + + /// Creates a new string by using the specified provider to control the formatting of the specified interpolated string. + /// An object that supplies culture-specific formatting information. + /// The initial buffer that may be used as temporary space as part of the formatting operation. The contents of this buffer may be overwritten. + /// The interpolated string. + /// The string that results for formatting the interpolated string using the specified format provider. + public static string Create(IFormatProvider? provider, Span initialBuffer, [InterpolatedStringHandlerArgument(nameof(provider), nameof(initialBuffer))] ref DefaultInterpolatedStringHandler handler) => + handler.ToStringAndClear(); + + public static string Create(int length, TState state, SpanAction action) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return string.Create(length, state, action); +#else + ThrowHelper.ThrowIfArgumentNull(action, ExceptionArgument.action); + if (length <= 0) + { + if (length == 0) + { + return string.Empty; + } + + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + var str = new string('\0', length); + unsafe + { + fixed (char* p = str) + { + action(new Span(p, length), state); + } + } + return str; +#endif + } + + public static unsafe string Concat(params ReadOnlySpan values) + { +#if NET9_0_OR_GREATER + return string.Concat(values); +#else + var sum = 0; + foreach (var s in values) + { + sum += s.Length; + } + + return string.Create(sum, + (nint)(&values), + (span, state) => + { + var values = *(ReadOnlySpan*)state; + var offset = 0; + foreach (var s in values) + { + s.AsSpan().CopyTo(span.Slice(offset)); + offset += s.Length; + } + }); +#endif + } + + public static string Concat(params ReadOnlySpan args) + { +#if NET9_0_OR_GREATER + return string.Concat(args); +#else + if (args.Length <= 1) + { + return args.IsEmpty ? + string.Empty : + args[0]?.ToString() ?? string.Empty; + } + + var strings = new string[args.Length]; + + for (var i = 0; i < args.Length; i++) + { + var value = args[i]; + + strings[i] = value?.ToString() ?? string.Empty; + } + + return string.Concat(strings); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + }; + return string.Create(str0.Length + str1.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + }); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1, ReadOnlySpan str2) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1, str2); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + _3 = str2, + }; + return string.Create(str0.Length + str1.Length + str2.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + holder->_3.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length)); + }); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1, ReadOnlySpan str2, ReadOnlySpan str3) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1, str2, str3); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + _3 = str2, + _4 = str3, + }; + return string.Create(str0.Length + str1.Length + str2.Length + str3.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + holder->_3.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length)); + holder->_4.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length + holder->_3.Length)); + }); +#endif + } + } + + private ref struct ReadOnlySpanHolder + { + public ReadOnlySpan _1; + public ReadOnlySpan _2; + public ReadOnlySpan _3; + public ReadOnlySpan _4; + } } } diff --git a/src/MonoMod.Backports/System/ThrowHelper.cs b/src/MonoMod.Backports/System/ThrowHelper.cs index 37c6e32..91addd0 100644 --- a/src/MonoMod.Backports/System/ThrowHelper.cs +++ b/src/MonoMod.Backports/System/ThrowHelper.cs @@ -288,5 +288,6 @@ internal enum ExceptionArgument millisecondsDelay, list, dictionary, + action, } } \ No newline at end of file