diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs
new file mode 100644
index 00000000000..7a09d1f9007
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ArrayMemoryManager{TFrom,TTo}.cs
@@ -0,0 +1,134 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
+using Microsoft.Toolkit.HighPerformance.Extensions;
+using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
+
+namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
+{
+ ///
+ /// A custom that casts data from a array, to values.
+ ///
+ /// The source type of items to read.
+ /// The target type to cast the source items to.
+ internal sealed class ArrayMemoryManager : MemoryManager, IMemoryManager
+ where TFrom : unmanaged
+ where TTo : unmanaged
+ {
+ ///
+ /// The source array to read data from.
+ ///
+ private readonly TFrom[] array;
+
+ ///
+ /// The starting offset within .
+ ///
+ private readonly int offset;
+
+ ///
+ /// The original used length for .
+ ///
+ private readonly int length;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source array to read data from.
+ /// The starting offset within .
+ /// The original used length for .
+ public ArrayMemoryManager(TFrom[] array, int offset, int length)
+ {
+ this.array = array;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ ///
+ public override Span GetSpan()
+ {
+#if SPAN_RUNTIME_SUPPORT
+ ref TFrom r0 = ref this.array.DangerousGetReferenceAt(this.offset);
+ ref TTo r1 = ref Unsafe.As(ref r0);
+ int length = RuntimeHelpers.ConvertLength(this.length);
+
+ return MemoryMarshal.CreateSpan(ref r1, length);
+#else
+ Span span = this.array.AsSpan(this.offset, this.length);
+
+ // We rely on MemoryMarshal.Cast here to deal with calculating the effective
+ // size of the new span to return. This will also make the behavior consistent
+ // for users that are both using this type as well as casting spans directly.
+ return MemoryMarshal.Cast(span);
+#endif
+ }
+
+ ///
+ public override unsafe MemoryHandle Pin(int elementIndex = 0)
+ {
+ if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf()))
+ {
+ ThrowArgumentOutOfRangeExceptionForInvalidIndex();
+ }
+
+ int
+ bytePrefix = this.offset * Unsafe.SizeOf(),
+ byteSuffix = elementIndex * Unsafe.SizeOf(),
+ byteOffset = bytePrefix + byteSuffix;
+
+ GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
+
+ ref TFrom r0 = ref this.array.DangerousGetReference();
+ ref byte r1 = ref Unsafe.As(ref r0);
+ ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
+ void* pi = Unsafe.AsPointer(ref r2);
+
+ return new MemoryHandle(pi, handle);
+ }
+
+ ///
+ public override void Unpin()
+ {
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+
+ ///
+ public Memory GetMemory(int offset, int length)
+ where T : unmanaged
+ {
+ // We need to calculate the right offset and length of the new Memory. The local offset
+ // is the original offset into the wrapped TFrom[] array, while the input offset is the one
+ // with respect to TTo items in the Memory instance that is currently being cast.
+ int
+ absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset),
+ absoluteLength = RuntimeHelpers.ConvertLength(length);
+
+ // We have a special handling in cases where the user is circling back to the original type
+ // of the wrapped array. In this case we can just return a memory wrapping that array directly,
+ // with offset and length being adjusted, without the memory manager indirection.
+ if (typeof(T) == typeof(TFrom))
+ {
+ return (Memory)(object)this.array.AsMemory(absoluteOffset, absoluteLength);
+ }
+
+ return new ArrayMemoryManager(this.array, absoluteOffset, absoluteLength).Memory;
+ }
+
+ ///
+ /// Throws an when the target index for is invalid.
+ ///
+ private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
+ {
+ throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs
new file mode 100644
index 00000000000..20a56c54b2b
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/Interfaces/IMemoryManager.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+
+namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces
+{
+ ///
+ /// An interface for a instance that can reinterpret its underlying data.
+ ///
+ internal interface IMemoryManager
+ {
+ ///
+ /// Creates a new that reinterprets the underlying data for the current instance.
+ ///
+ /// The target type to cast the items to.
+ /// The starting offset within the data store.
+ /// The original used length for the data store.
+ /// A new instance of the specified type, reinterpreting the current items.
+ Memory GetMemory(int offset, int length)
+ where T : unmanaged;
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs
new file mode 100644
index 00000000000..688450d6bd1
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/ProxyMemoryManager{TFrom,TTo}.cs
@@ -0,0 +1,134 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
+using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
+
+namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
+{
+ ///
+ /// A custom that casts data from a of , to values.
+ ///
+ /// The source type of items to read.
+ /// The target type to cast the source items to.
+ internal sealed class ProxyMemoryManager : MemoryManager, IMemoryManager
+ where TFrom : unmanaged
+ where TTo : unmanaged
+ {
+ ///
+ /// The source to read data from.
+ ///
+ private readonly MemoryManager memoryManager;
+
+ ///
+ /// The starting offset within .
+ ///
+ private readonly int offset;
+
+ ///
+ /// The original used length for .
+ ///
+ private readonly int length;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source to read data from.
+ /// The starting offset within .
+ /// The original used length for .
+ public ProxyMemoryManager(MemoryManager memoryManager, int offset, int length)
+ {
+ this.memoryManager = memoryManager;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ ///
+ public override Span GetSpan()
+ {
+ Span span = this.memoryManager.GetSpan().Slice(this.offset, this.length);
+
+ return MemoryMarshal.Cast(span);
+ }
+
+ ///
+ public override MemoryHandle Pin(int elementIndex = 0)
+ {
+ if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf()))
+ {
+ ThrowArgumentExceptionForInvalidIndex();
+ }
+
+ int
+ bytePrefix = this.offset * Unsafe.SizeOf(),
+ byteSuffix = elementIndex * Unsafe.SizeOf(),
+ byteOffset = bytePrefix + byteSuffix;
+
+#if NETSTANDARD1_4
+ int
+ shiftedOffset = byteOffset / Unsafe.SizeOf(),
+ remainder = byteOffset - (shiftedOffset * Unsafe.SizeOf());
+#else
+ int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf(), out int remainder);
+#endif
+
+ if (remainder != 0)
+ {
+ ThrowArgumentExceptionForInvalidAlignment();
+ }
+
+ return this.memoryManager.Pin(shiftedOffset);
+ }
+
+ ///
+ public override void Unpin()
+ {
+ this.memoryManager.Unpin();
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ ((IDisposable)this.memoryManager).Dispose();
+ }
+
+ ///
+ public Memory GetMemory(int offset, int length)
+ where T : unmanaged
+ {
+ // Like in the other memory manager, calculate the absolute offset and length
+ int
+ absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset),
+ absoluteLength = RuntimeHelpers.ConvertLength(length);
+
+ // Skip one indirection level and slice the original memory manager, if possible
+ if (typeof(T) == typeof(TFrom))
+ {
+ return (Memory)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength);
+ }
+
+ return new ProxyMemoryManager(this.memoryManager, absoluteOffset, absoluteLength).Memory;
+ }
+
+ ///
+ /// Throws an when the target index for is invalid.
+ ///
+ private static void ThrowArgumentExceptionForInvalidIndex()
+ {
+ throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
+ }
+
+ ///
+ /// Throws an when receives an invalid target index.
+ ///
+ private static void ThrowArgumentExceptionForInvalidAlignment()
+ {
+ throw new ArgumentOutOfRangeException("elementIndex", "The input index doesn't result in an aligned item access");
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs
new file mode 100644
index 00000000000..3a16c0b4794
--- /dev/null
+++ b/Microsoft.Toolkit.HighPerformance/Buffers/Internals/StringMemoryManager{TTo}.cs
@@ -0,0 +1,126 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
+using Microsoft.Toolkit.HighPerformance.Extensions;
+using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
+
+namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
+{
+ ///
+ /// A custom that casts data from a to values.
+ ///
+ /// The target type to cast the source characters to.
+ internal sealed class StringMemoryManager : MemoryManager, IMemoryManager
+ where TTo : unmanaged
+ {
+ ///
+ /// The source to read data from.
+ ///
+ private readonly string text;
+
+ ///
+ /// The starting offset within .
+ ///
+ private readonly int offset;
+
+ ///
+ /// The original used length for .
+ ///
+ private readonly int length;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source to read data from.
+ /// The starting offset within .
+ /// The original used length for .
+ public StringMemoryManager(string text, int offset, int length)
+ {
+ this.text = text;
+ this.offset = offset;
+ this.length = length;
+ }
+
+ ///
+ public override Span GetSpan()
+ {
+#if SPAN_RUNTIME_SUPPORT
+ ref char r0 = ref this.text.DangerousGetReferenceAt(this.offset);
+ ref TTo r1 = ref Unsafe.As(ref r0);
+ int length = RuntimeHelpers.ConvertLength(this.length);
+
+ return MemoryMarshal.CreateSpan(ref r1, length);
+#else
+ ReadOnlyMemory memory = this.text.AsMemory(this.offset, this.length);
+ Span span = MemoryMarshal.AsMemory(memory).Span;
+
+ return MemoryMarshal.Cast(span);
+#endif
+ }
+
+ ///
+ public override unsafe MemoryHandle Pin(int elementIndex = 0)
+ {
+ if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf() / Unsafe.SizeOf()))
+ {
+ ThrowArgumentOutOfRangeExceptionForInvalidIndex();
+ }
+
+ int
+ bytePrefix = this.offset * Unsafe.SizeOf(),
+ byteSuffix = elementIndex * Unsafe.SizeOf(),
+ byteOffset = bytePrefix + byteSuffix;
+
+ GCHandle handle = GCHandle.Alloc(this.text, GCHandleType.Pinned);
+
+ ref char r0 = ref this.text.DangerousGetReference();
+ ref byte r1 = ref Unsafe.As(ref r0);
+ ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
+ void* pi = Unsafe.AsPointer(ref r2);
+
+ return new MemoryHandle(pi, handle);
+ }
+
+ ///
+ public override void Unpin()
+ {
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+
+ ///
+ public Memory GetMemory(int offset, int length)
+ where T : unmanaged
+ {
+ int
+ absoluteOffset = this.offset + RuntimeHelpers.ConvertLength(offset),
+ absoluteLength = RuntimeHelpers.ConvertLength(length);
+
+ if (typeof(T) == typeof(char))
+ {
+ ReadOnlyMemory memory = this.text.AsMemory(absoluteOffset, absoluteLength);
+
+ return (Memory)(object)MemoryMarshal.AsMemory(memory);
+ }
+
+ return new StringMemoryManager(this.text, absoluteOffset, absoluteLength).Memory;
+ }
+
+ ///
+ /// Throws an when the target index for is invalid.
+ ///
+ private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
+ {
+ throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs
index e2b9d434cad..a175a02b577 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/MemoryExtensions.cs
@@ -6,6 +6,7 @@
using System.Diagnostics.Contracts;
using System.IO;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
#if SPAN_RUNTIME_SUPPORT
using Microsoft.Toolkit.HighPerformance.Memory;
#endif
@@ -64,6 +65,41 @@ public static Memory2D AsMemory2D(this Memory memory, int offset, int h
}
#endif
+ ///
+ /// Casts a of one primitive type to of bytes.
+ ///
+ /// The type if items in the source .
+ /// The source , of type .
+ /// A of bytes.
+ ///
+ /// Thrown if the property of the new would exceed .
+ ///
+ /// Thrown when the data store of is not supported.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Memory AsBytes(this Memory memory)
+ where T : unmanaged
+ {
+ return MemoryMarshal.AsMemory(((ReadOnlyMemory)memory).Cast());
+ }
+
+ ///
+ /// Casts a of one primitive type to another primitive type .
+ ///
+ /// The type of items in the source .
+ /// The type of items in the destination .
+ /// The source , of type .
+ /// A of type
+ /// Thrown when the data store of is not supported.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Memory Cast(this Memory memory)
+ where TFrom : unmanaged
+ where TTo : unmanaged
+ {
+ return MemoryMarshal.AsMemory(((ReadOnlyMemory)memory).Cast());
+ }
+
///
/// Returns a wrapping the contents of the given of instance.
///
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs
index 0c1c6eb821f..d9d996717b0 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs
@@ -3,9 +3,13 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Buffers;
using System.Diagnostics.Contracts;
using System.IO;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Toolkit.HighPerformance.Buffers.Internals;
+using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
#if SPAN_RUNTIME_SUPPORT
using Microsoft.Toolkit.HighPerformance.Memory;
#endif
@@ -64,6 +68,77 @@ public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, i
}
#endif
+ ///
+ /// Casts a of one primitive type to of bytes.
+ ///
+ /// The type if items in the source .
+ /// The source , of type .
+ /// A of bytes.
+ ///
+ /// Thrown if the property of the new would exceed .
+ ///
+ /// Thrown when the data store of is not supported.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlyMemory AsBytes(this ReadOnlyMemory memory)
+ where T : unmanaged
+ {
+ return Cast(memory);
+ }
+
+ ///
+ /// Casts a of one primitive type to another primitive type .
+ ///
+ /// The type of items in the source .
+ /// The type of items in the destination .
+ /// The source , of type .
+ /// A of type
+ /// Thrown when the data store of is not supported.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlyMemory Cast(this ReadOnlyMemory memory)
+ where TFrom : unmanaged
+ where TTo : unmanaged
+ {
+ if (memory.IsEmpty)
+ {
+ return default;
+ }
+
+ if (typeof(TFrom) == typeof(char) &&
+ MemoryMarshal.TryGetString((ReadOnlyMemory)(object)memory, out string? text, out int start, out int length))
+ {
+ return new StringMemoryManager(text!, start, length).Memory;
+ }
+
+ if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment))
+ {
+ return new ArrayMemoryManager(segment.Array!, segment.Offset, segment.Count).Memory;
+ }
+
+ if (MemoryMarshal.TryGetMemoryManager>(memory, out var memoryManager, out start, out length))
+ {
+ // If the memory manager is the one resulting from a previous cast, we can use it directly to retrieve
+ // a new manager for the target type that wraps the original data store, instead of creating one that
+ // wraps the current manager. This ensures that doing repeated casts always results in only up to one
+ // indirection level in the chain of memory managers needed to access the target data buffer to use.
+ if (memoryManager is IMemoryManager wrappingManager)
+ {
+ return wrappingManager.GetMemory(start, length);
+ }
+
+ return new ProxyMemoryManager(memoryManager, start, length).Memory;
+ }
+
+ // Throws when the memory instance has an unsupported backing store
+ static ReadOnlyMemory ThrowArgumentExceptionForUnsupportedMemory()
+ {
+ throw new ArgumentException("The input instance doesn't have a supported underlying data store.");
+ }
+
+ return ThrowArgumentExceptionForUnsupportedMemory();
+ }
+
///
/// Returns a wrapping the contents of the given of instance.
///
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs
index b4264395133..e0aef51a3d4 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs
@@ -259,14 +259,10 @@ public static int Count(this ReadOnlySpan span, T value)
///
/// Casts a of one primitive type to of bytes.
- /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety.
///
/// The type if items in the source .
/// The source slice, of type .
/// A of bytes.
- ///
- /// Thrown when contains pointers.
- ///
///
/// Thrown if the property of the new would exceed .
///
@@ -280,7 +276,6 @@ public static ReadOnlySpan AsBytes(this ReadOnlySpan span)
///
/// Casts a of one primitive type to another primitive type .
- /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety.
///
/// The type of items in the source .
/// The type of items in the destination .
@@ -289,14 +284,11 @@ public static ReadOnlySpan AsBytes(this ReadOnlySpan span)
///
/// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means.
///
- ///
- /// Thrown when or contains pointers.
- ///
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan Cast(this ReadOnlySpan span)
- where TFrom : struct
- where TTo : struct
+ where TFrom : unmanaged
+ where TTo : unmanaged
{
return MemoryMarshal.Cast(span);
}
diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs
index eecf0519360..d70c743f56b 100644
--- a/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs
+++ b/Microsoft.Toolkit.HighPerformance/Extensions/SpanExtensions.cs
@@ -117,14 +117,10 @@ public static Span2D AsSpan2D(this Span span, int offset, int height, i
///
/// Casts a of one primitive type to of bytes.
- /// That type may not contain pointers or references. This is checked at runtime in order to preserve type safety.
///
/// The type if items in the source .
/// The source slice, of type .
/// A of bytes.
- ///
- /// Thrown when contains pointers.
- ///
///
/// Thrown if the property of the new would exceed .
///
@@ -138,7 +134,6 @@ public static Span AsBytes(this Span span)
///
/// Casts a of one primitive type to another primitive type .
- /// These types may not contain pointers or references. This is checked at runtime in order to preserve type safety.
///
/// The type of items in the source .
/// The type of items in the destination .
@@ -147,14 +142,11 @@ public static Span AsBytes(this Span span)
///
/// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means.
///
- ///
- /// Thrown when or contains pointers.
- ///
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span Cast(this Span span)
- where TFrom : struct
- where TTo : struct
+ where TFrom : unmanaged
+ where TTo : unmanaged
{
return MemoryMarshal.Cast(span);
}
diff --git a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs
index d14bc211193..5a22ef8aff8 100644
--- a/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs
+++ b/Microsoft.Toolkit.HighPerformance/Helpers/Internals/RuntimeHelpers.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -18,10 +18,40 @@
namespace Microsoft.Toolkit.HighPerformance.Helpers.Internals
{
///
- /// A helper class that act as polyfill for .NET Standard 2.0 and below.
+ /// A helper class that with utility methods for dealing with references, and other low-level details.
+ /// It also contains some APIs that act as polyfills for .NET Standard 2.0 and below.
///
internal static class RuntimeHelpers
{
+ ///
+ /// Converts a length of items from one size to another (rounding towards zero).
+ ///
+ /// The source type of items.
+ /// The target type of items.
+ /// The input length to convert.
+ /// The converted length for the specified argument and types.
+ [Pure]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe int ConvertLength(int length)
+ where TFrom : unmanaged
+ where TTo : unmanaged
+ {
+ if (sizeof(TFrom) == sizeof(TTo))
+ {
+ return length;
+ }
+ else if (sizeof(TFrom) == 1)
+ {
+ return length / sizeof(TTo);
+ }
+ else
+ {
+ ulong targetLength = (ulong)(uint)length * (uint)sizeof(TFrom) / (uint)sizeof(TTo);
+
+ return checked((int)targetLength);
+ }
+ }
+
///
/// Gets the length of a given array as a native integer.
///
diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs
index 705a335735d..edbf5d0e961 100644
--- a/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs
+++ b/UnitTests/UnitTests.HighPerformance.Shared/Extensions/Test_MemoryExtensions.cs
@@ -3,7 +3,10 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Buffers;
using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using Microsoft.Toolkit.HighPerformance.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -12,12 +15,565 @@ namespace UnitTests.HighPerformance.Extensions
[TestClass]
public class Test_MemoryExtensions
{
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_Cast_Empty()
+ {
+ // Casting an empty memory of any size should always be valid
+ // and result in another empty memory, regardless of types.
+ Memory m1 = default;
+ Memory mc1 = m1.Cast();
+
+ Assert.IsTrue(mc1.IsEmpty);
+
+ Memory m2 = default;
+ Memory mc2 = m2.Cast();
+
+ Assert.IsTrue(mc2.IsEmpty);
+
+ Memory m3 = default;
+ Memory mc3 = m3.Cast();
+
+ Assert.IsTrue(mc3.IsEmpty);
+
+ // Same as above, but with a sliced memory (length 12, slide from 12, so length of 0)
+ Memory m4 = new byte[12].AsMemory(12);
+ Memory mc4 = m4.Cast();
+
+ Assert.IsTrue(mc4.IsEmpty);
+
+ // Same as above, but slicing to 12 in two steps
+ Memory m5 = new byte[12].AsMemory().Slice(4).Slice(8);
+ Memory mc5 = m5.Cast();
+
+ Assert.IsTrue(mc5.IsEmpty);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_Cast_TooShort()
+ {
+ // One int is 4 bytes, so casting from 3 rounds down to 0
+ Memory m1 = new byte[3];
+ Memory mc1 = m1.Cast();
+
+ Assert.IsTrue(mc1.IsEmpty);
+
+ // Same as above, 13 / sizeof(int) == 3
+ Memory m2 = new byte[13];
+ Memory mc2 = m2.Cast();
+
+ Assert.AreEqual(mc2.Length, 3);
+
+ // 16 - 5 = 11 ---> 11 / sizeof(int) = 2
+ Memory m3 = new byte[16].AsMemory(5);
+ Memory mc3 = m3.Cast();
+
+ Assert.AreEqual(mc3.Length, 2);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastFromByte()
+ {
+ // Test for a standard cast from bytes with an evenly divisible length
+ Memory memoryOfBytes = new byte[128];
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+
+ Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
+
+ Span spanOfBytes = memoryOfBytes.Span;
+ Span spanOfFloats = memoryOfFloats.Span;
+
+ // We also need to check that the Span returned from the caast memory
+ // actually has the initial reference pointing to the same location as
+ // the one to the same item in the span from the original memory.
+ Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfBytes[0],
+ ref Unsafe.As(ref spanOfFloats[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastToByte()
+ {
+ // Cast from float to bytes to verify casting works when the target type
+ // as a smaller byte size as well (so the resulting length will be larger).
+ Memory memoryOfFloats = new float[128];
+ Memory memoryOfBytes = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfBytes = memoryOfBytes.Span;
+
+ // Same as above, we need to verify that the resulting span has matching
+ // starting references with the one produced by the original memory.
+ Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfBytes[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastToShort()
+ {
+ // Similar test as above, just with different types to double check
+ Memory memoryOfFloats = new float[128];
+ Memory memoryOfShorts = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfShorts = memoryOfShorts.Span;
+
+ Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfShorts[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack()
+ {
+ // Here we start from a byte array, get a memory, then cast to float and then
+ // back to byte. We want to verify that the final memory is both valid and
+ // consistent, as well that our internal optimization works and that the final
+ // memory correctly skipped the indirect memory managed and just wrapped the original
+ // array instead. This is documented in the custom array memory manager in the package.
+ var data = new byte[128];
+ Memory memoryOfBytes = data;
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+ Memory memoryBack = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
+
+ // Here we get the array from the final memory and check that it does exist and
+ // the associated parameters match the ones we'd expect here (same length, offset of 0).
+ Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out var segment));
+ Assert.AreSame(segment.Array!, data);
+ Assert.AreEqual(segment.Offset, 0);
+ Assert.AreEqual(segment.Count, data.Length);
+
+ Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
+
+ Span span1 = memoryOfBytes.Span;
+ Span span2 = memoryBack.Span;
+
+ // Also validate the initial and final spans for reference equality
+ Assert.IsTrue(span1 == span2);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_Cast_TooShort_WithSlice()
+ {
+ // Like we did above, we have some more tests where we slice an initial memory and
+ // validate the length of the resulting, accounting for the expected rounding down.
+ Memory m1 = new byte[8].AsMemory().Slice(4, 3);
+ Memory mc1 = m1.Cast();
+
+ Assert.IsTrue(mc1.IsEmpty);
+
+ Memory m2 = new byte[20].AsMemory().Slice(4, 13);
+ Memory mc2 = m2.Cast();
+
+ Assert.AreEqual(mc2.Length, 3);
+
+ Memory m3 = new byte[16].AsMemory().Slice(5);
+ Memory mc3 = m3.Cast();
+
+ Assert.AreEqual(mc3.Length, 2);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastFromByte_WithSlice()
+ {
+ // Same exact test as the cast from byte to float did above, but with a slice. This is done
+ // to ensure the cast still works correctly when the memory is internally storing an offset.
+ Memory memoryOfBytes = new byte[512].AsMemory().Slice(128, 128);
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+
+ Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
+
+ Span spanOfBytes = memoryOfBytes.Span;
+ Span spanOfFloats = memoryOfFloats.Span;
+
+ Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfBytes[0],
+ ref Unsafe.As(ref spanOfFloats[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastToByte_WithSlice()
+ {
+ // Same as above, just with inverted source and destination types
+ Memory memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
+ Memory memoryOfBytes = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfBytes = memoryOfBytes.Span;
+
+ Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfBytes[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastToShort_WithSlice()
+ {
+ // Once again the same test but with types both different in size than 1. We're mostly
+ // just testing the rounding logic in a number of different case to ensure it's correct.
+ Memory memoryOfFloats = new float[512].AsMemory().Slice(128, 128);
+ Memory memoryOfShorts = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfShorts = memoryOfShorts.Span;
+
+ Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfShorts[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice()
+ {
+ // Just like the equivalent test above, but with a slice thrown in too
+ var data = new byte[512];
+ Memory memoryOfBytes = data.AsMemory().Slice(128, 128);
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+ Memory memoryBack = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
+
+ // Here we now also have to validate the starting offset from the extracted array
+ Assert.IsTrue(MemoryMarshal.TryGetArray(memoryBack, out var segment));
+ Assert.AreSame(segment.Array!, data);
+ Assert.AreEqual(segment.Offset, 128);
+ Assert.AreEqual(segment.Count, 128);
+
+ Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
+
+ Span span1 = memoryOfBytes.Span;
+ Span span2 = memoryBack.Span;
+
+ Assert.IsTrue(span1 == span2);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastFromByte()
+ {
+ // This test is just like the ones above, but this time we're casting a memory
+ // that wraps a custom memory manager and not an array. This is done to ensure
+ // the casting logic works correctly in all cases, as it'll use a different
+ // memory manager internally (a memory can wrap a string, an array or a manager).
+ Memory memoryOfBytes = new ArrayMemoryManager(128);
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+
+ Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
+
+ Span spanOfBytes = memoryOfBytes.Span;
+ Span spanOfFloats = memoryOfFloats.Span;
+
+ Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfBytes[0],
+ ref Unsafe.As(ref spanOfFloats[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastToByte()
+ {
+ // Same as above, but with inverted types
+ Memory memoryOfFloats = new ArrayMemoryManager(128);
+ Memory memoryOfBytes = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfBytes = memoryOfBytes.Span;
+
+ Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfBytes[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastToShort()
+ {
+ // Same as above, but with types different in size than 1, just in case
+ Memory memoryOfFloats = new ArrayMemoryManager(128);
+ Memory memoryOfShorts = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfShorts = memoryOfShorts.Span;
+
+ Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfShorts[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack()
+ {
+ // Equivalent to the one with an array, but with a memory manager
+ var data = new ArrayMemoryManager(128);
+ Memory memoryOfBytes = data;
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+ Memory memoryBack = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length);
+
+ // Here we expect to get back the original memory manager, due to the same optimization we
+ // checked for when using an array. We need to check they're the same, and the other parameters.
+ Assert.IsTrue(MemoryMarshal.TryGetMemoryManager>(memoryBack, out var manager, out var start, out var length));
+ Assert.AreSame(manager!, data);
+ Assert.AreEqual(start, 0);
+ Assert.AreEqual(length, 128);
+
+ Assert.IsTrue(memoryOfBytes.Equals(memoryBack));
+
+ Span span1 = memoryOfBytes.Span;
+ Span span2 = memoryBack.Span;
+
+ Assert.IsTrue(span1 == span2);
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastFromByte_WithSlice()
+ {
+ // Same as the ones with an array, but with an extra slice
+ Memory memoryOfBytes = new ArrayMemoryManager(512).Memory.Slice(128, 128);
+ Memory memoryOfFloats = memoryOfBytes.Cast();
+
+ Assert.AreEqual(memoryOfFloats.Length, 128 / sizeof(float));
+
+ Span spanOfBytes = memoryOfBytes.Span;
+ Span spanOfFloats = memoryOfFloats.Span;
+
+ Assert.AreEqual(memoryOfFloats.Length, spanOfFloats.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfBytes[0],
+ ref Unsafe.As(ref spanOfFloats[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastToByte_WithSlice()
+ {
+ // Same as above, but with inverted types
+ Memory memoryOfFloats = new ArrayMemoryManager(512).Memory.Slice(128, 128);
+ Memory memoryOfBytes = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfBytes.Length, 128 * sizeof(float));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span spanOfBytes = memoryOfBytes.Span;
+
+ Assert.AreEqual(memoryOfBytes.Length, spanOfBytes.Length);
+ Assert.IsTrue(Unsafe.AreSame(
+ ref spanOfFloats[0],
+ ref Unsafe.As(ref spanOfBytes[0])));
+ }
+
+ [TestCategory("MemoryExtensions")]
+ [TestMethod]
+ public void Test_MemoryExtensions_FromMemoryManager_CastToShort_WithSlice()
+ {
+ // Same as above but with different types
+ Memory memoryOfFloats = new ArrayMemoryManager(512).Memory.Slice(128, 128);
+ Memory memoryOfShorts = memoryOfFloats.Cast();
+
+ Assert.AreEqual(memoryOfShorts.Length, 128 * sizeof(float) / sizeof(short));
+
+ Span spanOfFloats = memoryOfFloats.Span;
+ Span