Skip to content

Commit 15edbed

Browse files
author
msftbot[bot]
authored
New [ReadOnly]Memory<T>.Cast<TFrom, TTo> extensions (#3520)
## PR Type What kind of change does this PR introduce? <!-- Please uncomment one or more that apply to this PR. --> - Feature ## What is the current behavior? <!-- Please describe the current behavior that you are modifying, or link to a relevant issue. --> Right now there is no (easy) way to cast a `Memory<TFrom>` instance to a `Memory<TTo>` instance. There are APIs to to do that for `Span<T>` instances, but not for `Memory<T>`. The reason for that is that with a `Span<T>` it's just a matter of retrieving the wrapped reference, reinterpreting it and then adjusting the size, then creating a new `Span<T>` instance. But a `Memory<T>` instance is completely different: it wraps an object which could be either a `T[]` array, a `MemoryManager<T>` instance, etc. The result is that currently there are no APIs in the BCL nor in the toolkit to just "cast" a `Memory<T>`. This feature has been requested by a number of developers, including in a well known library such as `ImageSharp`: > Yes, that's exactly what I would need. But I'm wondering how would you implement it. > It's certainly non trivial to cast a `Memory<byte>` to a `Memory<TPixel>` and if there's an API for that I would gladly want to know... > So I pressume `ImageSharp` would need to do some work under the hood. (_`ImageSharp` issue, [here](SixLabors/ImageSharp#1097 (comment)) To solve that, I created a very simplified version of the code included in this PR, into a PR [here](SixLabors/ImageSharp#1314). Having this available right out of the box in the `HighPerformance` package would be helpful in a number of similar situations, especially with `Memory<T>` APIs becoming more and more common across libraries now (as they've been out for a while). ## What is the new behavior? <!-- Describe how was this issue resolved or changed? --> This PR includes 4 new extensions for the `Memory<T>` and `ReadOnlyMemory<T>` types that enable the following: ```csharp // Cast between two Memory<T> instances... Memory<byte> memoryOfBytes = new byte[128].AsMemory(); Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>(); // ...any number of times is needed Memory<int> memoryOfInts = memoryOfFloats.Cast<float, int>(); Memory<byte> backToBytesMemory = memoryOfInts.Cast<int, byte>(); // Or just convert into bytes directly Memory<int> sourceAsInts = new int[128].AsMemory(); Memory<byte> sourceAsBytes = sourceAsInts.AsBytes(); // Want to get a stream from a string? Why not! 😄 using (Stream stream = "Hello world".AsMemory().AsBytes().AsStream()) { // Use the stream here, which reads *directly* from the string data! } ``` Here is the full list of the new APIs introduced in this PR: ```csharp namespace Microsoft.Toolkit.HighPerformance.Extensions { public static class MemoryExtensions { public static Memory<byte> AsBytes<T>(this Memory<T> memory) where T : unmanaged; public static Memory<TTo> Cast<TFrom, TTo>(this Memory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } public static class ReadOnlyMemoryExtensions { public static ReadOnlyMemory<byte> AsBytes<T>(this ReadOnlyMemory<T> memory) where T : unmanaged; public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(this ReadOnlyMemory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } } ``` ## Notes Marking as draft as this is still being worked on, but feedbacks and reviews are welcome! 😄 ## PR Checklist Please check if your PR fulfills the following requirements: - [X] Tested code with current [supported SDKs](../readme.md#supported) - [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link -->~~ - [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~ - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~ - [X] Tests for the changes have been added (for bug fixes / features) (if applicable) - [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*) - [X] Contains **NO** breaking changes
2 parents 72d6847 + 5aa217f commit 15edbed

File tree

10 files changed

+1160
-22
lines changed

10 files changed

+1160
-22
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Buffers;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
9+
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
10+
using Microsoft.Toolkit.HighPerformance.Extensions;
11+
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
12+
13+
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
14+
{
15+
/// <summary>
16+
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <typeparamref name="TFrom"/> array, to <typeparamref name="TTo"/> values.
17+
/// </summary>
18+
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
19+
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
20+
internal sealed class ArrayMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
21+
where TFrom : unmanaged
22+
where TTo : unmanaged
23+
{
24+
/// <summary>
25+
/// The source <typeparamref name="TFrom"/> array to read data from.
26+
/// </summary>
27+
private readonly TFrom[] array;
28+
29+
/// <summary>
30+
/// The starting offset within <see name="array"/>.
31+
/// </summary>
32+
private readonly int offset;
33+
34+
/// <summary>
35+
/// The original used length for <see name="array"/>.
36+
/// </summary>
37+
private readonly int length;
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="ArrayMemoryManager{TFrom, TTo}"/> class.
41+
/// </summary>
42+
/// <param name="array">The source <typeparamref name="TFrom"/> array to read data from.</param>
43+
/// <param name="offset">The starting offset within <paramref name="array"/>.</param>
44+
/// <param name="length">The original used length for <paramref name="array"/>.</param>
45+
public ArrayMemoryManager(TFrom[] array, int offset, int length)
46+
{
47+
this.array = array;
48+
this.offset = offset;
49+
this.length = length;
50+
}
51+
52+
/// <inheritdoc/>
53+
public override Span<TTo> GetSpan()
54+
{
55+
#if SPAN_RUNTIME_SUPPORT
56+
ref TFrom r0 = ref this.array.DangerousGetReferenceAt(this.offset);
57+
ref TTo r1 = ref Unsafe.As<TFrom, TTo>(ref r0);
58+
int length = RuntimeHelpers.ConvertLength<TFrom, TTo>(this.length);
59+
60+
return MemoryMarshal.CreateSpan(ref r1, length);
61+
#else
62+
Span<TFrom> span = this.array.AsSpan(this.offset, this.length);
63+
64+
// We rely on MemoryMarshal.Cast here to deal with calculating the effective
65+
// size of the new span to return. This will also make the behavior consistent
66+
// for users that are both using this type as well as casting spans directly.
67+
return MemoryMarshal.Cast<TFrom, TTo>(span);
68+
#endif
69+
}
70+
71+
/// <inheritdoc/>
72+
public override unsafe MemoryHandle Pin(int elementIndex = 0)
73+
{
74+
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
75+
{
76+
ThrowArgumentOutOfRangeExceptionForInvalidIndex();
77+
}
78+
79+
int
80+
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
81+
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
82+
byteOffset = bytePrefix + byteSuffix;
83+
84+
GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);
85+
86+
ref TFrom r0 = ref this.array.DangerousGetReference();
87+
ref byte r1 = ref Unsafe.As<TFrom, byte>(ref r0);
88+
ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
89+
void* pi = Unsafe.AsPointer(ref r2);
90+
91+
return new MemoryHandle(pi, handle);
92+
}
93+
94+
/// <inheritdoc/>
95+
public override void Unpin()
96+
{
97+
}
98+
99+
/// <inheritdoc/>
100+
protected override void Dispose(bool disposing)
101+
{
102+
}
103+
104+
/// <inheritdoc/>
105+
public Memory<T> GetMemory<T>(int offset, int length)
106+
where T : unmanaged
107+
{
108+
// We need to calculate the right offset and length of the new Memory<T>. The local offset
109+
// is the original offset into the wrapped TFrom[] array, while the input offset is the one
110+
// with respect to TTo items in the Memory<TTo> instance that is currently being cast.
111+
int
112+
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, TFrom>(offset),
113+
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(length);
114+
115+
// We have a special handling in cases where the user is circling back to the original type
116+
// of the wrapped array. In this case we can just return a memory wrapping that array directly,
117+
// with offset and length being adjusted, without the memory manager indirection.
118+
if (typeof(T) == typeof(TFrom))
119+
{
120+
return (Memory<T>)(object)this.array.AsMemory(absoluteOffset, absoluteLength);
121+
}
122+
123+
return new ArrayMemoryManager<TFrom, T>(this.array, absoluteOffset, absoluteLength).Memory;
124+
}
125+
126+
/// <summary>
127+
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
128+
/// </summary>
129+
private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
130+
{
131+
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
132+
}
133+
}
134+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Buffers;
7+
8+
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces
9+
{
10+
/// <summary>
11+
/// An interface for a <see cref="MemoryManager{T}"/> instance that can reinterpret its underlying data.
12+
/// </summary>
13+
internal interface IMemoryManager
14+
{
15+
/// <summary>
16+
/// Creates a new <see cref="Memory{T}"/> that reinterprets the underlying data for the current instance.
17+
/// </summary>
18+
/// <typeparam name="T">The target type to cast the items to.</typeparam>
19+
/// <param name="offset">The starting offset within the data store.</param>
20+
/// <param name="length">The original used length for the data store.</param>
21+
/// <returns>A new <see cref="Memory{T}"/> instance of the specified type, reinterpreting the current items.</returns>
22+
Memory<T> GetMemory<T>(int offset, int length)
23+
where T : unmanaged;
24+
}
25+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Buffers;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
9+
using Microsoft.Toolkit.HighPerformance.Buffers.Internals.Interfaces;
10+
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
11+
12+
namespace Microsoft.Toolkit.HighPerformance.Buffers.Internals
13+
{
14+
/// <summary>
15+
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <see cref="MemoryManager{T}"/> of <typeparamref name="TFrom"/>, to <typeparamref name="TTo"/> values.
16+
/// </summary>
17+
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
18+
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
19+
internal sealed class ProxyMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
20+
where TFrom : unmanaged
21+
where TTo : unmanaged
22+
{
23+
/// <summary>
24+
/// The source <see cref="MemoryManager{T}"/> to read data from.
25+
/// </summary>
26+
private readonly MemoryManager<TFrom> memoryManager;
27+
28+
/// <summary>
29+
/// The starting offset within <see name="memoryManager"/>.
30+
/// </summary>
31+
private readonly int offset;
32+
33+
/// <summary>
34+
/// The original used length for <see name="memoryManager"/>.
35+
/// </summary>
36+
private readonly int length;
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="ProxyMemoryManager{TFrom, TTo}"/> class.
40+
/// </summary>
41+
/// <param name="memoryManager">The source <see cref="MemoryManager{T}"/> to read data from.</param>
42+
/// <param name="offset">The starting offset within <paramref name="memoryManager"/>.</param>
43+
/// <param name="length">The original used length for <paramref name="memoryManager"/>.</param>
44+
public ProxyMemoryManager(MemoryManager<TFrom> memoryManager, int offset, int length)
45+
{
46+
this.memoryManager = memoryManager;
47+
this.offset = offset;
48+
this.length = length;
49+
}
50+
51+
/// <inheritdoc/>
52+
public override Span<TTo> GetSpan()
53+
{
54+
Span<TFrom> span = this.memoryManager.GetSpan().Slice(this.offset, this.length);
55+
56+
return MemoryMarshal.Cast<TFrom, TTo>(span);
57+
}
58+
59+
/// <inheritdoc/>
60+
public override MemoryHandle Pin(int elementIndex = 0)
61+
{
62+
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
63+
{
64+
ThrowArgumentExceptionForInvalidIndex();
65+
}
66+
67+
int
68+
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
69+
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
70+
byteOffset = bytePrefix + byteSuffix;
71+
72+
#if NETSTANDARD1_4
73+
int
74+
shiftedOffset = byteOffset / Unsafe.SizeOf<TFrom>(),
75+
remainder = byteOffset - (shiftedOffset * Unsafe.SizeOf<TFrom>());
76+
#else
77+
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf<TFrom>(), out int remainder);
78+
#endif
79+
80+
if (remainder != 0)
81+
{
82+
ThrowArgumentExceptionForInvalidAlignment();
83+
}
84+
85+
return this.memoryManager.Pin(shiftedOffset);
86+
}
87+
88+
/// <inheritdoc/>
89+
public override void Unpin()
90+
{
91+
this.memoryManager.Unpin();
92+
}
93+
94+
/// <inheritdoc/>
95+
protected override void Dispose(bool disposing)
96+
{
97+
((IDisposable)this.memoryManager).Dispose();
98+
}
99+
100+
/// <inheritdoc/>
101+
public Memory<T> GetMemory<T>(int offset, int length)
102+
where T : unmanaged
103+
{
104+
// Like in the other memory manager, calculate the absolute offset and length
105+
int
106+
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, TFrom>(offset),
107+
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(length);
108+
109+
// Skip one indirection level and slice the original memory manager, if possible
110+
if (typeof(T) == typeof(TFrom))
111+
{
112+
return (Memory<T>)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength);
113+
}
114+
115+
return new ProxyMemoryManager<TFrom, T>(this.memoryManager, absoluteOffset, absoluteLength).Memory;
116+
}
117+
118+
/// <summary>
119+
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
120+
/// </summary>
121+
private static void ThrowArgumentExceptionForInvalidIndex()
122+
{
123+
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
124+
}
125+
126+
/// <summary>
127+
/// Throws an <see cref="ArgumentOutOfRangeException"/> when <see cref="Pin"/> receives an invalid target index.
128+
/// </summary>
129+
private static void ThrowArgumentExceptionForInvalidAlignment()
130+
{
131+
throw new ArgumentOutOfRangeException("elementIndex", "The input index doesn't result in an aligned item access");
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)