// 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.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using CommunityToolkit.HighPerformance; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.HighPerformance.UnitTests.Extensions; [TestClass] public class Test_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); } [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); } [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 cast 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]))); } [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]))); } [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]))); } [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. byte[]? 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 ArraySegment 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); } [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); } [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]))); } [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]))); } [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]))); } [TestMethod] public void Test_MemoryExtensions_FromArray_CastFromByteAndBack_WithSlice() { // Just like the equivalent test above, but with a slice thrown in too byte[]? 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 ArraySegment 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); } [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]))); } [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]))); } [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]))); } [TestMethod] public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack() { // Equivalent to the one with an array, but with a memory manager ArrayMemoryManager? data = new(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 ArrayMemoryManager? manager, out int start, out int 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); } [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]))); } [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]))); } [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 spanOfShorts = memoryOfShorts.Span; Assert.AreEqual(memoryOfShorts.Length, spanOfShorts.Length); Assert.IsTrue(Unsafe.AreSame( ref spanOfFloats[0], ref Unsafe.As(ref spanOfShorts[0]))); } [TestMethod] public void Test_MemoryExtensions_FromMemoryManager_CastFromByteAndBack_WithSlice() { // Just like the one above, but with the slice ArrayMemoryManager? data = new(512); Memory memoryOfBytes = data.Memory.Slice(128, 128); Memory memoryOfFloats = memoryOfBytes.Cast(); Memory memoryBack = memoryOfFloats.Cast(); Assert.AreEqual(memoryOfBytes.Length, memoryBack.Length); // Here we also need to validate that the offset was maintained Assert.IsTrue(MemoryMarshal.TryGetMemoryManager>(memoryBack, out ArrayMemoryManager? manager, out int start, out int length)); Assert.AreSame(manager!, data); Assert.AreEqual(start, 128); Assert.AreEqual(length, 128); Assert.IsTrue(memoryOfBytes.Equals(memoryBack)); Span span1 = memoryOfBytes.Span; Span span2 = memoryBack.Span; Assert.IsTrue(span1 == span2); } [TestMethod] [DataRow(64, 0, 0)] [DataRow(64, 4, 0)] [DataRow(64, 0, 4)] [DataRow(64, 4, 4)] [DataRow(64, 4, 0)] [DataRow(256, 16, 0)] [DataRow(256, 4, 16)] [DataRow(256, 64, 0)] [DataRow(256, 64, 8)] public unsafe void Test_MemoryExtensions_FromArray_CastFromByte_Pin(int size, int preOffset, int postOffset) { // Here we need to validate that pinning works correctly in a number of cases. First we allocate // an array of the requested size, then get a memory after slicing to a target position, then cast // and then slice again. We do so to ensure that pinning correctly tracks the correct index with // respect to the original array through a number of internal offsets. As in, when pinning the // final memory, our internal custom memory manager should be able to pin the item in the original // array at offset preOffset + (postOffset * sizeof(float)), accounting for the cast as well. byte[]? data = new byte[size]; Memory memoryOfBytes = data.AsMemory(preOffset); Memory memoryOfFloats = memoryOfBytes.Cast().Slice(postOffset); using MemoryHandle handle = memoryOfFloats.Pin(); void* p1 = handle.Pointer; void* p2 = Unsafe.AsPointer(ref data[preOffset + (postOffset * sizeof(float))]); Assert.IsTrue(p1 == p2); } [TestMethod] [DataRow(64, 0, 0)] [DataRow(64, 4, 0)] [DataRow(64, 0, 4)] [DataRow(64, 4, 4)] [DataRow(64, 4, 0)] [DataRow(256, 16, 0)] [DataRow(256, 4, 16)] [DataRow(256, 64, 0)] [DataRow(256, 64, 8)] public unsafe void Test_MemoryExtensions_FromMemoryManager_CastFromByte_Pin(int size, int preOffset, int postOffset) { // Just like the test above, but this type the initial memory wraps a memory manager ArrayMemoryManager? data = new(size); Memory memoryOfBytes = data.Memory.Slice(preOffset); Memory memoryOfFloats = memoryOfBytes.Cast().Slice(postOffset); using MemoryHandle handle = memoryOfFloats.Pin(); void* p1 = handle.Pointer; void* p2 = Unsafe.AsPointer(ref data.GetSpan()[preOffset + (postOffset * sizeof(float))]); Assert.IsTrue(p1 == p2); } [TestMethod] public void Test_MemoryExtensions_FromString_CastFromByteAndBack() { // This is the same as the tests above, but here we're testing the // other remaining case, that is when a memory is wrapping a string. string? data = new('a', 128); Memory memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()); Memory memoryOfFloats = memoryOfChars.Cast(); Memory memoryBack = memoryOfFloats.Cast(); Assert.AreEqual(memoryOfChars.Length, memoryBack.Length); // Get the original string back (to validate the optimization too) and check the params Assert.IsTrue(MemoryMarshal.TryGetString(memoryOfChars, out string? text, out int start, out int length)); Assert.AreSame(text!, data); Assert.AreEqual(start, 0); Assert.AreEqual(length, data.Length); Assert.IsTrue(memoryOfChars.Equals(memoryBack)); Span span1 = memoryOfChars.Span; Span span2 = memoryBack.Span; Assert.IsTrue(span1 == span2); } [TestMethod] [DataRow(64, 0, 0)] [DataRow(64, 4, 0)] [DataRow(64, 0, 4)] [DataRow(64, 4, 4)] [DataRow(64, 4, 0)] [DataRow(256, 16, 0)] [DataRow(256, 4, 16)] [DataRow(256, 64, 0)] [DataRow(256, 64, 8)] public unsafe void Test_MemoryExtensions_FromString_CastAndPin(int size, int preOffset, int postOffset) { // Same test as before to validate pinning, but starting from a string string? data = new('a', size); Memory memoryOfChars = MemoryMarshal.AsMemory(data.AsMemory()).Slice(preOffset); Memory memoryOfBytes = memoryOfChars.Cast().Slice(postOffset); using (MemoryHandle handle1 = memoryOfBytes.Pin()) { void* p1 = handle1.Pointer; void* p2 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(byte) / sizeof(char)))); Assert.IsTrue(p1 == p2); } // Here we also add an extra test just like the one above, but casting to a type // that is bigger in byte size than char. Just to double check the casting logic. Memory memoryOfInts = memoryOfChars.Cast().Slice(postOffset); using (MemoryHandle handle2 = memoryOfInts.Pin()) { void* p3 = handle2.Pointer; void* p4 = Unsafe.AsPointer(ref data.DangerousGetReferenceAt(preOffset + (postOffset * sizeof(int) / sizeof(char)))); Assert.IsTrue(p3 == p4); } } [TestMethod] public void Test_MemoryExtensions_EmptyMemoryStream() { Memory memory = default; // Creating a stream from a default memory is valid, it's just empty Stream stream = memory.AsStream(); Assert.IsNotNull(stream); Assert.AreEqual(stream.Length, memory.Length); Assert.IsTrue(stream.CanWrite); } [TestMethod] public void Test_MemoryExtensions_MemoryStream() { Memory memory = new byte[1024]; Stream stream = memory.AsStream(); Assert.IsNotNull(stream); Assert.AreEqual(stream.Length, memory.Length); Assert.IsTrue(stream.CanWrite); } private sealed class ArrayMemoryManager : MemoryManager where T : unmanaged { private readonly T[] array; public ArrayMemoryManager(int size) { this.array = new T[size]; } public override Span GetSpan() { return this.array; } public override unsafe MemoryHandle Pin(int elementIndex = 0) { GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned); ref T r0 = ref this.array[elementIndex]; void* p = Unsafe.AsPointer(ref r0); return new(p, handle); } public override void Unpin() { } protected override void Dispose(bool disposing) { } public static implicit operator Memory(ArrayMemoryManager memoryManager) { return memoryManager.Memory; } } }