using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Cysharp.Text { public partial struct Utf8ValueStringBuilder : IDisposable, IBufferWriter, IResettableBufferWriter { public delegate bool TryFormat(T value, Span destination, out int written, StandardFormat format); const int ThreadStaticBufferSize = 64444; const int DefaultBufferSize = 65536; // use 64K default buffer. static Encoding UTF8NoBom = new UTF8Encoding(false); static byte newLine1; static byte newLine2; static bool crlf; static Utf8ValueStringBuilder() { var newLine = UTF8NoBom.GetBytes(Environment.NewLine); if (newLine.Length == 1) { // cr or lf newLine1 = newLine[0]; crlf = false; } else { // crlf(windows) newLine1 = newLine[0]; newLine2 = newLine[1]; crlf = true; } } [ThreadStatic] static byte[]? scratchBuffer; [ThreadStatic] internal static bool scratchBufferUsed; byte[]? buffer; int index; bool disposeImmediately; /// Length of written buffer. public int Length => index; /// Get the written buffer data. public ReadOnlySpan AsSpan() => buffer.AsSpan(0, index); /// Get the written buffer data. public ReadOnlyMemory AsMemory() => buffer.AsMemory(0, index); /// Get the written buffer data. public ArraySegment AsArraySegment() => new ArraySegment(buffer, 0, index); /// /// Initializes a new instance /// /// /// If true uses thread-static buffer that is faster but must return immediately. /// /// /// This exception is thrown when new StringBuilder(disposeImmediately: true) or ZString.CreateUtf8StringBuilder(notNested: true) is nested. /// See the README.md /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Utf8ValueStringBuilder(bool disposeImmediately) { if (disposeImmediately && scratchBufferUsed) { ThrowNestedException(); } byte[]? buf; if (disposeImmediately) { buf = scratchBuffer; if (buf == null) { buf = scratchBuffer = new byte[ThreadStaticBufferSize]; } scratchBufferUsed = true; } else { buf = ArrayPool.Shared.Rent(DefaultBufferSize); } buffer = buf; index = 0; this.disposeImmediately = disposeImmediately; } /// /// Return the inner buffer to pool. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { if (buffer != null) { if (buffer.Length != ThreadStaticBufferSize) { ArrayPool.Shared.Return(buffer); } buffer = null; index = 0; if (disposeImmediately) { scratchBufferUsed = false; } } } public void Clear() { index = 0; } public void TryGrow(int sizeHint) { if (buffer!.Length < index + sizeHint) { Grow(sizeHint); } } public void Grow(int sizeHint) { var nextSize = buffer!.Length * 2; if (sizeHint != 0) { nextSize = Math.Max(nextSize, index + sizeHint); } var newBuffer = ArrayPool.Shared.Rent(nextSize); buffer.CopyTo(newBuffer, 0); if (buffer.Length != ThreadStaticBufferSize) { ArrayPool.Shared.Return(buffer); } buffer = newBuffer; } /// Appends the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine() { if (crlf) { if (buffer!.Length - index < 2) Grow(2); buffer[index] = newLine1; buffer[index + 1] = newLine2; index += 2; } else { if (buffer!.Length - index < 1) Grow(1); buffer[index] = newLine1; index += 1; } } /// Appends the string representation of a specified value to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Append(char value) { var maxLen = UTF8NoBom.GetMaxByteCount(1); if (buffer!.Length - index < maxLen) { Grow(maxLen); } fixed (byte* bp = &buffer[index]) { index += UTF8NoBom.GetBytes(&value, 1, bp, maxLen); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char value, int repeatCount) { if (repeatCount < 0) { throw new ArgumentOutOfRangeException(nameof(repeatCount)); } if (value <= 0x7F) // ASCII { GetSpan(repeatCount).Fill((byte)value); Advance(repeatCount); } else { var maxLen = UTF8NoBom.GetMaxByteCount(1); Span utf8Bytes = stackalloc byte[maxLen]; ReadOnlySpan chars = stackalloc char[1] { value }; int len = UTF8NoBom.GetBytes(chars, utf8Bytes); TryGrow(len * repeatCount); for (int i = 0; i < repeatCount; i++) { utf8Bytes.CopyTo(GetSpan(len)); Advance(len); } } } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(char value) { Append(value); AppendLine(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string value, int startIndex, int count) { if (value == null) { if (startIndex == 0 && count == 0) { return; } else { throw new ArgumentNullException(nameof(value)); } } Append(value.AsSpan(startIndex, count)); } /// Appends the string representation of a specified value to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string value) { Append(value.AsSpan()); } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(string value) { Append(value); AppendLine(); } /// Appends a contiguous region of arbitrary memory to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(ReadOnlySpan value) { var maxLen = UTF8NoBom.GetMaxByteCount(value.Length); if (buffer!.Length - index < maxLen) { Grow(maxLen); } index += UTF8NoBom.GetBytes(value, buffer.AsSpan(index)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(ReadOnlySpan value) { Append(value); AppendLine(); } public void AppendLiteral(ReadOnlySpan value) { if ((buffer!.Length - index) < value.Length) { Grow(value.Length); } value.CopyTo(buffer.AsSpan(index)); index += value.Length; } /// Appends the string representation of a specified value to this instance. public void Append(T value) { if (!FormatterCache.TryFormatDelegate(value, buffer.AsSpan(index), out var written, default)) { Grow(written); if (!FormatterCache.TryFormatDelegate(value, buffer.AsSpan(index), out written, default)) { ThrowArgumentException(nameof(value)); } } index += written; } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. public void AppendLine(T value) { Append(value); AppendLine(); } // Output /// Copy inner buffer to the bufferWriter. public void CopyTo(IBufferWriter bufferWriter) { var destination = bufferWriter.GetSpan(index); TryCopyTo(destination, out var written); bufferWriter.Advance(written); } /// Copy inner buffer to the destination span. public bool TryCopyTo(Span destination, out int bytesWritten) { if (destination.Length < index) { bytesWritten = 0; return false; } bytesWritten = index; buffer.AsSpan(0, index).CopyTo(destination); return true; } /// Write inner buffer to stream. public Task WriteToAsync(Stream stream) { return stream.WriteAsync(buffer, 0, index); } /// Write inner buffer to stream. public Task WriteToAsync(Stream stream, CancellationToken cancellationToken) { return stream.WriteAsync(buffer, 0, index, cancellationToken); } /// Encode the innner utf8 buffer to a System.String. public override string ToString() { if (index == 0) return string.Empty; return UTF8NoBom.GetString(buffer, 0, index); } // IBufferWriter /// IBufferWriter.GetMemory. public Memory GetMemory(int sizeHint) { if ((buffer!.Length - index) < sizeHint) { Grow(sizeHint); } return buffer.AsMemory(index); } /// IBufferWriter.GetSpan. public Span GetSpan(int sizeHint) { if ((buffer!.Length - index) < sizeHint) { Grow(sizeHint); } return buffer.AsSpan(index); } /// IBufferWriter.Advance. public void Advance(int count) { index += count; } void IResettableBufferWriter.Reset() { index = 0; } void ThrowArgumentException(string paramName) { throw new ArgumentException("Can't format argument.", paramName); } void ThrowFormatException() { throw new FormatException("Index (zero based) must be greater than or equal to zero and less than the size of the argument list."); } static void ThrowNestedException() { throw new NestedStringBuilderCreationException(nameof(Utf8ValueStringBuilder)); } private void AppendFormatInternal(T arg, int width, StandardFormat format, string argName) { if (width <= 0) // leftJustify { width *= -1; if (!FormatterCache.TryFormatDelegate(arg, buffer.AsSpan(index), out var charsWritten, format)) { Grow(charsWritten); if (!FormatterCache.TryFormatDelegate(arg, buffer.AsSpan(index), out charsWritten, format)) { ThrowArgumentException(argName); } } index += charsWritten; int padding = width - charsWritten; if (width > 0 && padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } } else // rightJustify { if (typeof(T) == typeof(string)) { var s = Unsafe.As(arg); int padding = width - s.Length; if (padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } Append(s); } else { Span s = stackalloc byte[typeof(T).IsValueType ? Unsafe.SizeOf() * 8 : 1024]; if (!FormatterCache.TryFormatDelegate(arg, s, out var charsWritten, format)) { s = stackalloc byte[s.Length * 2]; if (!FormatterCache.TryFormatDelegate(arg, s, out charsWritten, format)) { ThrowArgumentException(argName); } } int padding = width - charsWritten; if (padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } s.CopyTo(GetSpan(charsWritten)); Advance(charsWritten); } } } /// /// Register custom formatter /// public static void RegisterTryFormat(TryFormat formatMethod) { FormatterCache.TryFormatDelegate = formatMethod; } static TryFormat CreateNullableFormatter() where T : struct { return new TryFormat((T? x, Span destination, out int written, StandardFormat format) => { if (x == null) { written = 0; return true; } return FormatterCache.TryFormatDelegate(x.Value, destination, out written, format); }); } /// /// Supports the Nullable type for a given struct type. /// public static void EnableNullableFormat() where T : struct { RegisterTryFormat(CreateNullableFormatter()); } public static class FormatterCache { public static TryFormat TryFormatDelegate; static FormatterCache() { var formatter = (TryFormat?)CreateFormatter(typeof(T)); if (formatter == null) { if (typeof(T).IsEnum) { formatter = new TryFormat(EnumUtil.TryFormatUtf8); } else { formatter = new TryFormat(TryFormatDefault); } } TryFormatDelegate = formatter; } static bool TryFormatDefault(T value, Span dest, out int written, StandardFormat format) { if (value == null) { written = 0; return true; } var s = typeof(T) == typeof(string) ? Unsafe.As(value) : (value is IFormattable formattable && format != default) ? formattable.ToString(format.ToString(), null) : value.ToString(); // also use this length when result is false. written = UTF8NoBom.GetMaxByteCount(s.Length); if (dest.Length < written) { return false; } written = UTF8NoBom.GetBytes(s.AsSpan(), dest); return true; } } } }