BufferTextWriter.cs 8.58 KB
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.

using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft;

namespace SoapCore.MessageEncoder
{
	/// <summary>
	/// A <see cref="TextWriter"/> that writes to a reassignable instance of <see cref="IBufferWriter{T}"/>.
	/// </summary>
	/// <remarks>
	/// Using this is much more memory efficient than a <see cref="StreamWriter"/> when writing to many different
	/// <see cref="IBufferWriter{T}"/> because the same writer, with all its buffers, can be reused.
	/// </remarks>
	public class BufferTextWriter : TextWriter
	{
		/// <summary>
		/// A buffer of written characters that have not yet been encoded.
		/// The <see cref="_charBufferPosition"/> field tracks how many characters are represented in this buffer.
		/// </summary>
		private readonly char[] _charBuffer = new char[512];

		/// <summary>
		/// The internal buffer writer to use for writing encoded characters.
		/// </summary>
		private IBufferWriter<byte> _bufferWriter;

		/// <summary>
		/// The last buffer received from <see cref="_bufferWriter"/>.
		/// </summary>
		private Memory<byte> _memory;

		/// <summary>
		/// The number of characters written to the <see cref="_memory"/> buffer.
		/// </summary>
		private int _memoryPosition;

		/// <summary>
		/// The number of characters written to the <see cref="_charBuffer"/>.
		/// </summary>
		private int _charBufferPosition;

		/// <summary>
		/// Whether the encoding preamble has been written since the last call to <see cref="Initialize(IBufferWriter{byte}, Encoding)"/>.
		/// </summary>
		private bool _preambleWritten;

		/// <summary>
		/// The encoding currently in use.
		/// </summary>
		private Encoding _encoding;

		/// <summary>
		/// The preamble for the current <see cref="_encoding"/>.
		/// </summary>
		/// <remarks>
		/// We store this as a field to avoid calling <see cref="Encoding.GetPreamble"/> repeatedly,
		/// since the typical implementation allocates a new array for each call.
		/// </remarks>
		private ReadOnlyMemory<byte> _encodingPreamble;

		/// <summary>
		/// An encoder obtained from the current <see cref="_encoding"/> used for incrementally encoding written characters.
		/// </summary>
		private Encoder _encoder;

		/// <summary>
		/// Initializes a new instance of the <see cref="BufferTextWriter"/> class.
		/// </summary>
		/// <param name="bufferWriter">The buffer writer to write to.</param>
		/// <param name="encoding">The encoding to use.</param>
		public BufferTextWriter(IBufferWriter<byte> bufferWriter, Encoding encoding)
		{
			Initialize(bufferWriter, encoding);
		}

		/// <inheritdoc />
		public override Encoding Encoding => _encoding;

		/// <summary>
		/// Gets the number of uninitialized characters remaining in <see cref="_charBuffer"/>.
		/// </summary>
		private int CharBufferSlack => _charBuffer.Length - _charBufferPosition;

		/// <summary>
		/// Prepares for writing to the specified buffer.
		/// </summary>
		/// <param name="bufferWriter">The buffer writer to write to.</param>
		/// <param name="encoding">The encoding to use.</param>
		public void Initialize(IBufferWriter<byte> bufferWriter, Encoding encoding)
		{
			Requires.NotNull(bufferWriter, nameof(bufferWriter));
			Requires.NotNull(encoding, nameof(encoding));

			Verify.Operation(_memoryPosition == 0 && _charBufferPosition == 0, "This instance must be flushed before being reinitialized.");

			_preambleWritten = false;
			_bufferWriter = bufferWriter;
			if (encoding != _encoding)
			{
				_encoding = encoding;
				_encoder = _encoding.GetEncoder();
				_encodingPreamble = _encoding.GetPreamble();
			}
			else
			{
				// encoder != null because if it were, encoding == null too, so we would have been in the first branch above.
				_encoder.Reset();
			}
		}

		/// <summary>
		/// Clears references to the <see cref="IBufferWriter{T}"/> set by a prior call to <see cref="Initialize(IBufferWriter{byte}, Encoding)"/>.
		/// </summary>
		public void Reset()
		{
			_bufferWriter = null;
		}

		/// <inheritdoc />
		public override void Flush()
		{
			ThrowIfNotInitialized();
			EncodeCharacters(flushEncoder: true);
			CommitBytes();
		}

		/// <inheritdoc />
		public override Task FlushAsync()
		{
			try
			{
				Flush();
				return Task.CompletedTask;
			}
			catch (Exception ex)
			{
				return Task.FromException(ex);
			}
		}

		/// <inheritdoc />
		public override void Write(char value)
		{
			ThrowIfNotInitialized();
			_charBuffer[_charBufferPosition++] = value;
			EncodeCharactersIfBufferFull();
		}

		/// <inheritdoc />
		public override void Write(string value)
		{
			if (value == null)
			{
				return;
			}

			Write(value.AsSpan());
		}

		/// <inheritdoc />
		public override void Write(char[] buffer, int index, int count) => Write(Requires.NotNull(buffer, nameof(buffer)).AsSpan(index, count));

#if SPAN_BUILTIN
		/// <inheritdoc />
		public override void Write(ReadOnlySpan<char> buffer)
#else
        /// <summary>
        /// Copies a given span of characters into the writer.
        /// </summary>
        /// <param name="buffer">The characters to write.</param>
		public virtual void Write(ReadOnlySpan<char> buffer)
#endif
		{
			ThrowIfNotInitialized();

			// Try for fast path
			if (buffer.Length <= CharBufferSlack)
			{
				buffer.CopyTo(_charBuffer.AsSpan(_charBufferPosition));
				_charBufferPosition += buffer.Length;
				EncodeCharactersIfBufferFull();
			}
			else
			{
				int charsCopied = 0;
				while (charsCopied < buffer.Length)
				{
					int charsToCopy = Math.Min(buffer.Length - charsCopied, CharBufferSlack);
					buffer.Slice(charsCopied, charsToCopy).CopyTo(_charBuffer.AsSpan(_charBufferPosition));
					charsCopied += charsToCopy;
					_charBufferPosition += charsToCopy;
					EncodeCharactersIfBufferFull();
				}
			}
		}

#if SPAN_BUILTIN
		/// <inheritdoc />
		public override void WriteLine(ReadOnlySpan<char> buffer)
#else
        /// <summary>
        /// Writes a span of characters followed by a <see cref="TextWriter.NewLine"/>.
        /// </summary>
        /// <param name="buffer">The characters to write.</param>
		public virtual void WriteLine(ReadOnlySpan<char> buffer)
#endif
		{
			Write(buffer);
			WriteLine();
		}

		/// <summary>
		/// Encodes the written characters if the character buffer is full.
		/// </summary>
		private void EncodeCharactersIfBufferFull()
		{
			if (_charBufferPosition == _charBuffer.Length)
			{
				EncodeCharacters(flushEncoder: false);
			}
		}

		/// <summary>
		/// Encodes characters written so far to a buffer provided by the underyling <see cref="_bufferWriter"/>.
		/// </summary>
		/// <param name="flushEncoder"><c>true</c> to flush the characters in the encoder; useful when finalizing the output.</param>
		private void EncodeCharacters(bool flushEncoder)
		{
			if (_charBufferPosition > 0)
			{
				int maxBytesLength = Encoding.GetMaxByteCount(_charBufferPosition);
				if (!_preambleWritten)
				{
					maxBytesLength += _encodingPreamble.Length;
				}

				if (_memory.Length - _memoryPosition < maxBytesLength)
				{
					CommitBytes();
					_memory = _bufferWriter.GetMemory(maxBytesLength);
				}

				if (!_preambleWritten)
				{
					_encodingPreamble.Span.CopyTo(_memory.Span.Slice(_memoryPosition));
					_memoryPosition += _encodingPreamble.Length;
					_preambleWritten = true;
				}

				if (MemoryMarshal.TryGetArray(_memory, out ArraySegment<byte> segment))
				{
					_memoryPosition += _encoder.GetBytes(_charBuffer, 0, _charBufferPosition, segment.Array, segment.Offset + _memoryPosition, flush: flushEncoder);
				}
				else
				{
					byte[] rentedByteBuffer = ArrayPool<byte>.Shared.Rent(maxBytesLength);
					try
					{
						int bytesWritten = _encoder.GetBytes(_charBuffer, 0, _charBufferPosition, rentedByteBuffer, 0, flush: flushEncoder);
						rentedByteBuffer.CopyTo(_memory.Span.Slice(_memoryPosition));
						_memoryPosition += bytesWritten;
					}
					finally
					{
						ArrayPool<byte>.Shared.Return(rentedByteBuffer);
					}
				}

				_charBufferPosition = 0;

				if (_memoryPosition == _memory.Length)
				{
					Flush();
				}
			}
		}

		/// <summary>
		/// Commits any written bytes to the underlying <see cref="_bufferWriter"/>.
		/// </summary>
		private void CommitBytes()
		{
			if (_memoryPosition > 0)
			{
				_bufferWriter.Advance(_memoryPosition);
				_memoryPosition = 0;
				_memory = default;
			}
		}

		private void ThrowIfNotInitialized()
		{
			if (_bufferWriter == null)
			{
				throw new InvalidOperationException("Call " + nameof(Initialize) + " first.");
			}
		}
	}
}