| File: Utilities\ValueStringBuilder.cs | Web Access |
| Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Adapted from JeremyKuhne/touki:touki/Touki/Text/ValueStringBuilder.cs // https://github.com/JeremyKuhne/touki/blob/main/touki/Touki/Text/ValueStringBuilder.cs // Original source: .NET Runtime and Windows Forms source code (MIT-licensed). // // Adaptations for MSBuild: // - Moved into the Microsoft.Build.Utilities namespace. // - Removed all [InterpolatedStringHandler]-related members // (the handler attribute, the (literalLength, formattedCount) constructors, // the format-provider fields, AppendLiteral, AppendFormatted, etc.). // - Removed Touki-specific dependencies (StringSegment, CopyTo overloads // that use .NET-only TextWriter / StringBuilder APIs). // - Polyfilled Span<char>.Replace for net472 with a manual loop. using System; using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Microsoft.Build.Utilities; /// <summary> /// String builder struct that can be used to build strings in a memory-efficient way. /// Allows using stack space for small strings. /// </summary> internal ref struct ValueStringBuilder { // We're using a byte array here so the same backing buffer can be passed to APIs that take byte[] without // type-safety juggling. Byte arrays are always allocated on pointer-size boundaries so they're safe for // reinterpreting as Span<char>. private byte[]? _arrayToReturnToPool; private Span<char> _chars; private int _length; /// <summary> /// Initializes a new instance of the <see cref="ValueStringBuilder"/> struct with an initial buffer. /// </summary> /// <param name="initialBuffer">The initial buffer to use for the string builder.</param> public ValueStringBuilder(Span<char> initialBuffer) { _arrayToReturnToPool = null; _chars = initialBuffer; _length = 0; } /// <summary> /// Initializes a new instance of the <see cref="ValueStringBuilder"/> struct with the specified initial capacity. /// </summary> /// <param name="initialCapacity">The initial capacity for the string builder.</param> public ValueStringBuilder(int initialCapacity) { _arrayToReturnToPool = ArrayPool<byte>.Shared.Rent(initialCapacity * sizeof(char)); _chars = MemoryMarshal.Cast<byte, char>((Span<byte>)_arrayToReturnToPool); _length = 0; } /// <summary> /// Gets or sets the current length of the string builder. /// </summary> public int Length { readonly get => _length; set { // Do not free if set to 0. This allows in-place building of strings. ArgumentOutOfRangeException.ThrowIfNegative(value); ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _chars.Length); _length = value; } } /// <summary> /// Gets the maximum number of characters that can be contained in the memory allocated by the current instance. /// </summary> public readonly int Capacity => _chars.Length; /// <summary> /// Clears the contents of the string builder, resetting its length to zero. /// </summary> public void Clear() { // Reset the position to 0, but do not clear the underlying array to allow in-place building. _length = 0; } /// <summary> /// Ensures that the capacity of this builder is at least the specified value. /// </summary> public void EnsureCapacity(int capacity) { ArgumentOutOfRangeException.ThrowIfNegative(capacity); if ((uint)capacity > (uint)_chars.Length) { Grow(capacity - _length); } } /// <summary> /// Get a pinnable reference to the builder. Always ensures that there is a trailing null. /// </summary> /// <remarks> /// This overload is pattern matched in the C# 7.3+ compiler so you can omit /// the explicit method call, and write e.g. <c>fixed (char* c = builder)</c>. /// </remarks> public ref char GetPinnableReference() { EnsureCapacity(_length + 1); _chars[_length] = '\0'; return ref _chars.GetPinnableReference(); } /// <summary> /// Gets a reference to the character at the specified index. /// </summary> public ref char this[int index] { get { ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _length); return ref _chars[index]; } } /// <summary> /// Converts the value of this builder to a <see cref="string"/>. /// </summary> public override readonly string ToString() => _chars[.._length].ToString(); /// <summary> /// Converts the value of this builder to a <see cref="string"/> and disposes the builder. /// </summary> public string ToStringAndDispose() { string s = ToString(); Dispose(); return s; } /// <summary> /// Returns a span around the contents of the builder. /// </summary> /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/>.</param> public ReadOnlySpan<char> AsSpan(bool terminate) { if (terminate) { EnsureCapacity(Length + 1); _chars[Length] = '\0'; } return _chars[.._length]; } /// <summary> /// Returns a read-only span around the contents of the builder. /// </summary> public readonly ReadOnlySpan<char> AsSpan() => _chars[.._length]; /// <summary> /// Gets a slice of the builder using the specified range. /// </summary> public readonly ReadOnlySpan<char> this[Range range] => _chars[.._length][range]; /// <summary> /// Forms a slice out of the buffer starting at a specified index. /// </summary> public readonly ReadOnlySpan<char> Slice(int start) { ArgumentOutOfRangeException.ThrowIfNegative(start); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start, _length); return _chars[start.._length]; } /// <summary> /// Forms a slice out of the buffer starting at a specified index for a specified length. /// </summary> public readonly ReadOnlySpan<char> Slice(int start, int length) { ArgumentOutOfRangeException.ThrowIfNegative(start); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start, _length); ArgumentOutOfRangeException.ThrowIfNegative(length); ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, _length); return _chars.Slice(start, length); } /// <summary> /// Returns a span of the requested length at the current position in the builder that can be /// used to directly append characters. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span<char> AppendSpan(int length) { ArgumentOutOfRangeException.ThrowIfNegative(length); int originalLength = _length; if (originalLength > _chars.Length - length) { Grow(length); } _length = originalLength + length; return _chars.Slice(originalLength, length); } /// <summary> /// Attempts to copy the contents of this builder to the destination span. /// </summary> public readonly bool TryCopyTo(Span<char> destination, out int charsWritten) { if (_chars[.._length].TryCopyTo(destination)) { charsWritten = _length; return true; } charsWritten = 0; return false; } /// <summary> /// Inserts the specified character a specified number of times at the specified index. /// </summary> public void Insert(int index, char value, int count) { ArgumentOutOfRangeException.ThrowIfNegative(index); ArgumentOutOfRangeException.ThrowIfGreaterThan(index, _length); ArgumentOutOfRangeException.ThrowIfNegative(count); if (_length > _chars.Length - count) { Grow(count); } int remaining = _length - index; _chars.Slice(index, remaining).CopyTo(_chars[(index + count)..]); _chars.Slice(index, count).Fill(value); _length += count; } /// <summary> /// Inserts the specified string at the specified index. /// </summary> public void Insert(int index, string? s) { ArgumentOutOfRangeException.ThrowIfNegative(index); ArgumentOutOfRangeException.ThrowIfGreaterThan(index, _length); if (s is null) { return; } int count = s.Length; if (_length > _chars.Length - count) { Grow(count); } int remaining = _length - index; _chars.Slice(index, remaining).CopyTo(_chars[(index + count)..]); s.AsSpan().CopyTo(_chars[index..]); _length += count; } /// <summary> /// Appends the specified character to the end of this builder. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { int pos = _length; if ((uint)pos < (uint)_chars.Length) { _chars[pos] = c; _length = pos + 1; } else { GrowAndAppend(c); } } /// <summary> /// Appends a string to the builder. If the string is <see langword="null"/>, this method does nothing. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string? s) { if (s is null) { return; } int pos = _length; if (s.Length == 1 && (uint)pos < (uint)_chars.Length) { // Very common case, e.g. appending separators or single-char delimiters. _chars[pos] = s[0]; _length = pos + 1; } else { AppendSlow(s); } } private void AppendSlow(string s) { int pos = _length; if (pos > _chars.Length - s.Length) { Grow(s.Length); } s.AsSpan().CopyTo(_chars[pos..]); _length += s.Length; } /// <summary> /// Appends the specified character a specified number of times to the end of this builder. /// </summary> public void Append(char c, int count) { if (_length > _chars.Length - count) { Grow(count); } Span<char> dst = _chars.Slice(_length, count); for (int i = 0; i < dst.Length; i++) { dst[i] = c; } _length += count; } /// <summary> /// Appends characters from a pointer to the end of this builder. /// </summary> public unsafe void Append(char* value, int length) { int pos = _length; if (pos > _chars.Length - length) { Grow(length); } Span<char> dst = _chars.Slice(_length, length); for (int i = 0; i < dst.Length; i++) { dst[i] = *value++; } _length += length; } /// <summary> /// Appends the specified read-only span of characters to the end of this builder. /// </summary> public void Append(scoped ReadOnlySpan<char> value) { int pos = _length; if (pos > _chars.Length - value.Length) { Grow(value.Length); } value.CopyTo(_chars[_length..]); _length += value.Length; } /// <summary> /// Replace all occurrences of a character in the builder with another character. /// </summary> public readonly void Replace(char oldValue, char newValue) { if (_length == 0 || oldValue == newValue) { return; } _chars[.._length].Replace(oldValue, newValue); } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { Grow(1); Append(c); } /// <summary> /// Resize the internal buffer either by doubling current buffer size or by adding /// <paramref name="additionalCapacityBeyondPos"/> to <see cref="_length"/> whichever is greater. /// </summary> [MethodImpl(MethodImplOptions.NoInlining)] [MemberNotNull(nameof(_arrayToReturnToPool))] private void Grow(int additionalCapacityBeyondPos) { Debug.Assert(additionalCapacityBeyondPos > 0); Debug.Assert(_length > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength // Increase to at least the required size, but try to double the size if possible, // bounding the doubling to not go beyond the max array length. int newCapacity = (int)Math.Max( (uint)(_length + additionalCapacityBeyondPos), Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. byte[] poolArray = ArrayPool<byte>.Shared.Rent(newCapacity * sizeof(char)); _chars[.._length].CopyTo(MemoryMarshal.Cast<byte, char>((Span<byte>)poolArray)); byte[]? toReturn = _arrayToReturnToPool; _chars = MemoryMarshal.Cast<byte, char>((Span<byte>)poolArray); _arrayToReturnToPool = poolArray; if (toReturn is not null) { ArrayPool<byte>.Shared.Return(toReturn); } } /// <summary> /// Releases all resources used by the <see cref="ValueStringBuilder"/>. /// </summary> /// <remarks> /// Returns any rented array back to the array pool and resets the builder to its default state. /// After calling this method the builder should not be used again. /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { byte[]? toReturn = _arrayToReturnToPool; // Clear the fields to prevent accidental reuse. _arrayToReturnToPool = null; _chars = default; _length = 0; if (toReturn is not null) { ArrayPool<byte>.Shared.Return(toReturn); } } /// <summary> /// Implicitly converts a <see cref="ValueStringBuilder"/> to a <see cref="ReadOnlySpan{T}"/>. /// </summary> public static implicit operator ReadOnlySpan<char>(ValueStringBuilder builder) => builder._chars[..builder._length]; }