// 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.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CommunityToolkit.HighPerformance.Buffers.Views;
#if NET6_0_OR_GREATER
using BitOperations = System.Numerics.BitOperations;
#else
using BitOperations = CommunityToolkit.HighPerformance.Helpers.Internals.BitOperations;
#endif
namespace CommunityToolkit.HighPerformance.Buffers;
///
/// Represents a heap-based, array-backed output sink into which data can be written.
///
/// The type of items to write to the current instance.
///
/// This is a custom implementation that replicates the
/// functionality and API surface of the array-based buffer writer available in
/// .NET Standard 2.1, with the main difference being the fact that in this case
/// the arrays in use are rented from the shared instance,
/// and that is also available on .NET Standard 2.0.
///
[DebuggerTypeProxy(typeof(MemoryDebugView<>))]
[DebuggerDisplay("{ToString(),raw}")]
public sealed class ArrayPoolBufferWriter : IBuffer, IMemoryOwner
{
///
/// The default buffer size to use to expand empty arrays.
///
private const int DefaultInitialBufferSize = 256;
///
/// The instance used to rent .
///
private readonly ArrayPool pool;
///
/// The underlying array.
///
private T[]? array;
#pragma warning disable IDE0032 // Use field over auto-property (clearer and faster)
///
/// The starting offset within .
///
private int index;
#pragma warning restore IDE0032
///
/// Initializes a new instance of the class.
///
public ArrayPoolBufferWriter()
: this(ArrayPool.Shared, DefaultInitialBufferSize)
{
}
///
/// Initializes a new instance of the class.
///
/// The instance to use.
public ArrayPoolBufferWriter(ArrayPool pool)
: this(pool, DefaultInitialBufferSize)
{
}
///
/// Initializes a new instance of the class.
///
/// The minimum capacity with which to initialize the underlying buffer.
/// Thrown when is not valid.
public ArrayPoolBufferWriter(int initialCapacity)
: this(ArrayPool.Shared, initialCapacity)
{
}
///
/// Initializes a new instance of the class.
///
/// The instance to use.
/// The minimum capacity with which to initialize the underlying buffer.
/// Thrown when is not valid.
public ArrayPoolBufferWriter(ArrayPool pool, int initialCapacity)
{
// Since we're using pooled arrays, we can rent the buffer with the
// default size immediately, we don't need to use lazy initialization
// to save unnecessary memory allocations in this case.
// Additionally, we don't need to manually throw the exception if
// the requested size is not valid, as that'll be thrown automatically
// by the array pool in use when we try to rent an array with that size.
this.pool = pool;
this.array = pool.Rent(initialCapacity);
this.index = 0;
}
///
/// Finalizes an instance of the class.
///
~ArrayPoolBufferWriter() => Dispose();
///
Memory IMemoryOwner.Memory
{
// This property is explicitly implemented so that it's hidden
// under normal usage, as the name could be confusing when
// displayed besides WrittenMemory and GetMemory().
// The IMemoryOwner interface is implemented primarily
// so that the AsStream() extension can be used on this type,
// allowing users to first create a ArrayPoolBufferWriter
// instance to write data to, then get a stream through the
// extension and let it take care of returning the underlying
// buffer to the shared pool when it's no longer necessary.
// Inlining is not needed here since this will always be a callvirt.
get => MemoryMarshal.AsMemory(WrittenMemory);
}
///
public ReadOnlyMemory WrittenMemory
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
return array!.AsMemory(0, this.index);
}
}
///
public ReadOnlySpan WrittenSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
return array!.AsSpan(0, this.index);
}
}
///
public int WrittenCount
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.index;
}
///
public int Capacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
return array!.Length;
}
}
///
public int FreeCapacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
return array!.Length - this.index;
}
}
///
public void Clear()
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
array.AsSpan(0, this.index).Clear();
this.index = 0;
}
///
public void Advance(int count)
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
if (count < 0)
{
ThrowArgumentOutOfRangeExceptionForNegativeCount();
}
if (this.index > array!.Length - count)
{
ThrowArgumentExceptionForAdvancedTooFar();
}
this.index += count;
}
///
public Memory GetMemory(int sizeHint = 0)
{
CheckBufferAndEnsureCapacity(sizeHint);
return this.array.AsMemory(this.index);
}
///
public Span GetSpan(int sizeHint = 0)
{
CheckBufferAndEnsureCapacity(sizeHint);
return this.array.AsSpan(this.index);
}
///
/// Ensures that has enough free space to contain a given number of new items.
///
/// The minimum number of items to ensure space for in .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CheckBufferAndEnsureCapacity(int sizeHint)
{
T[]? array = this.array;
if (array is null)
{
ThrowObjectDisposedException();
}
if (sizeHint < 0)
{
ThrowArgumentOutOfRangeExceptionForNegativeSizeHint();
}
if (sizeHint == 0)
{
sizeHint = 1;
}
if (sizeHint > array!.Length - this.index)
{
ResizeBuffer(sizeHint);
}
}
///
/// Resizes to ensure it can fit the specified number of new items.
///
/// The minimum number of items to ensure space for in .
[MethodImpl(MethodImplOptions.NoInlining)]
private void ResizeBuffer(int sizeHint)
{
uint minimumSize = (uint)this.index + (uint)sizeHint;
// The ArrayPool class has a maximum threshold of 1024 * 1024 for the maximum length of
// pooled arrays, and once this is exceeded it will just allocate a new array every time
// of exactly the requested size. In that case, we manually round up the requested size to
// the nearest power of two, to ensure that repeated consecutive writes when the array in
// use is bigger than that threshold don't end up causing a resize every single time.
if (minimumSize > 1024 * 1024)
{
minimumSize = BitOperations.RoundUpToPowerOf2(minimumSize);
}
this.pool.Resize(ref this.array, (int)minimumSize);
}
///
public void Dispose()
{
T[]? array = this.array;
if (array is null)
{
return;
}
GC.SuppressFinalize(this);
this.array = null;
this.pool.Return(array);
}
///
[Pure]
public override string ToString()
{
// See comments in MemoryOwner about this
if (typeof(T) == typeof(char) &&
this.array is char[] chars)
{
return new string(chars, 0, this.index);
}
// Same representation used in Span
return $"CommunityToolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<{typeof(T)}>[{this.index}]";
}
///
/// Throws an when the requested count is negative.
///
private static void ThrowArgumentOutOfRangeExceptionForNegativeCount()
{
throw new ArgumentOutOfRangeException("count", "The count can't be a negative value.");
}
///
/// Throws an when the size hint is negative.
///
private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint()
{
throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value.");
}
///
/// Throws an when the requested count is negative.
///
private static void ThrowArgumentExceptionForAdvancedTooFar()
{
throw new ArgumentException("The buffer writer has advanced too far.");
}
///
/// Throws an when is .
///
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException("The current buffer has already been disposed.");
}
}