// 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."); } }