Skip to content

Commit

Permalink
New [ReadOnly]Memory<T>.Cast<TFrom, TTo> extensions (#3520)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
msftbot[bot] authored Nov 12, 2020
2 parents c493cae + 02fdd72 commit ade6c5b
Show file tree
Hide file tree
Showing 10 changed files with 1,160 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <typeparamref name="TFrom"/> array, to <typeparamref name="TTo"/> values.
/// </summary>
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
internal sealed class ArrayMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
where TFrom : unmanaged
where TTo : unmanaged
{
/// <summary>
/// The source <typeparamref name="TFrom"/> array to read data from.
/// </summary>
private readonly TFrom[] array;

/// <summary>
/// The starting offset within <see name="array"/>.
/// </summary>
private readonly int offset;

/// <summary>
/// The original used length for <see name="array"/>.
/// </summary>
private readonly int length;

/// <summary>
/// Initializes a new instance of the <see cref="ArrayMemoryManager{TFrom, TTo}"/> class.
/// </summary>
/// <param name="array">The source <typeparamref name="TFrom"/> array to read data from.</param>
/// <param name="offset">The starting offset within <paramref name="array"/>.</param>
/// <param name="length">The original used length for <paramref name="array"/>.</param>
public ArrayMemoryManager(TFrom[] array, int offset, int length)
{
this.array = array;
this.offset = offset;
this.length = length;
}

/// <inheritdoc/>
public override Span<TTo> GetSpan()
{
#if SPAN_RUNTIME_SUPPORT
ref TFrom r0 = ref this.array.DangerousGetReferenceAt(this.offset);
ref TTo r1 = ref Unsafe.As<TFrom, TTo>(ref r0);
int length = RuntimeHelpers.ConvertLength<TFrom, TTo>(this.length);

return MemoryMarshal.CreateSpan(ref r1, length);
#else
Span<TFrom> 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<TFrom, TTo>(span);
#endif
}

/// <inheritdoc/>
public override unsafe MemoryHandle Pin(int elementIndex = 0)
{
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
{
ThrowArgumentOutOfRangeExceptionForInvalidIndex();
}

int
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
byteOffset = bytePrefix + byteSuffix;

GCHandle handle = GCHandle.Alloc(this.array, GCHandleType.Pinned);

ref TFrom r0 = ref this.array.DangerousGetReference();
ref byte r1 = ref Unsafe.As<TFrom, byte>(ref r0);
ref byte r2 = ref Unsafe.Add(ref r1, byteOffset);
void* pi = Unsafe.AsPointer(ref r2);

return new MemoryHandle(pi, handle);
}

/// <inheritdoc/>
public override void Unpin()
{
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}

/// <inheritdoc/>
public Memory<T> GetMemory<T>(int offset, int length)
where T : unmanaged
{
// We need to calculate the right offset and length of the new Memory<T>. 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<TTo> instance that is currently being cast.
int
absoluteOffset = this.offset + RuntimeHelpers.ConvertLength<TTo, TFrom>(offset),
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(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<T>)(object)this.array.AsMemory(absoluteOffset, absoluteLength);
}

return new ArrayMemoryManager<TFrom, T>(this.array, absoluteOffset, absoluteLength).Memory;
}

/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
/// </summary>
private static void ThrowArgumentOutOfRangeExceptionForInvalidIndex()
{
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// An interface for a <see cref="MemoryManager{T}"/> instance that can reinterpret its underlying data.
/// </summary>
internal interface IMemoryManager
{
/// <summary>
/// Creates a new <see cref="Memory{T}"/> that reinterprets the underlying data for the current instance.
/// </summary>
/// <typeparam name="T">The target type to cast the items to.</typeparam>
/// <param name="offset">The starting offset within the data store.</param>
/// <param name="length">The original used length for the data store.</param>
/// <returns>A new <see cref="Memory{T}"/> instance of the specified type, reinterpreting the current items.</returns>
Memory<T> GetMemory<T>(int offset, int length)
where T : unmanaged;
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A custom <see cref="MemoryManager{T}"/> that casts data from a <see cref="MemoryManager{T}"/> of <typeparamref name="TFrom"/>, to <typeparamref name="TTo"/> values.
/// </summary>
/// <typeparam name="TFrom">The source type of items to read.</typeparam>
/// <typeparam name="TTo">The target type to cast the source items to.</typeparam>
internal sealed class ProxyMemoryManager<TFrom, TTo> : MemoryManager<TTo>, IMemoryManager
where TFrom : unmanaged
where TTo : unmanaged
{
/// <summary>
/// The source <see cref="MemoryManager{T}"/> to read data from.
/// </summary>
private readonly MemoryManager<TFrom> memoryManager;

/// <summary>
/// The starting offset within <see name="memoryManager"/>.
/// </summary>
private readonly int offset;

/// <summary>
/// The original used length for <see name="memoryManager"/>.
/// </summary>
private readonly int length;

/// <summary>
/// Initializes a new instance of the <see cref="ProxyMemoryManager{TFrom, TTo}"/> class.
/// </summary>
/// <param name="memoryManager">The source <see cref="MemoryManager{T}"/> to read data from.</param>
/// <param name="offset">The starting offset within <paramref name="memoryManager"/>.</param>
/// <param name="length">The original used length for <paramref name="memoryManager"/>.</param>
public ProxyMemoryManager(MemoryManager<TFrom> memoryManager, int offset, int length)
{
this.memoryManager = memoryManager;
this.offset = offset;
this.length = length;
}

/// <inheritdoc/>
public override Span<TTo> GetSpan()
{
Span<TFrom> span = this.memoryManager.GetSpan().Slice(this.offset, this.length);

return MemoryMarshal.Cast<TFrom, TTo>(span);
}

/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
{
if ((uint)elementIndex >= (uint)(this.length * Unsafe.SizeOf<TFrom>() / Unsafe.SizeOf<TTo>()))
{
ThrowArgumentExceptionForInvalidIndex();
}

int
bytePrefix = this.offset * Unsafe.SizeOf<TFrom>(),
byteSuffix = elementIndex * Unsafe.SizeOf<TTo>(),
byteOffset = bytePrefix + byteSuffix;

#if NETSTANDARD1_4
int
shiftedOffset = byteOffset / Unsafe.SizeOf<TFrom>(),
remainder = byteOffset - (shiftedOffset * Unsafe.SizeOf<TFrom>());
#else
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf<TFrom>(), out int remainder);
#endif

if (remainder != 0)
{
ThrowArgumentExceptionForInvalidAlignment();
}

return this.memoryManager.Pin(shiftedOffset);
}

/// <inheritdoc/>
public override void Unpin()
{
this.memoryManager.Unpin();
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
((IDisposable)this.memoryManager).Dispose();
}

/// <inheritdoc/>
public Memory<T> GetMemory<T>(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<TTo, TFrom>(offset),
absoluteLength = RuntimeHelpers.ConvertLength<TTo, TFrom>(length);

// Skip one indirection level and slice the original memory manager, if possible
if (typeof(T) == typeof(TFrom))
{
return (Memory<T>)(object)this.memoryManager.Memory.Slice(absoluteOffset, absoluteLength);
}

return new ProxyMemoryManager<TFrom, T>(this.memoryManager, absoluteOffset, absoluteLength).Memory;
}

/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the target index for <see cref="Pin"/> is invalid.
/// </summary>
private static void ThrowArgumentExceptionForInvalidIndex()
{
throw new ArgumentOutOfRangeException("elementIndex", "The input index is not in the valid range");
}

/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when <see cref="Pin"/> receives an invalid target index.
/// </summary>
private static void ThrowArgumentExceptionForInvalidAlignment()
{
throw new ArgumentOutOfRangeException("elementIndex", "The input index doesn't result in an aligned item access");
}
}
}
Loading

0 comments on commit ade6c5b

Please sign in to comment.