From eaca0eb5ad77594176bcfa8fa29fc83945f7a455 Mon Sep 17 00:00:00 2001 From: Richard Deeming Date: Thu, 19 Oct 2023 16:07:04 +0100 Subject: [PATCH] Add .NET Standard 2.0 target. --- .../ArrayBufferWriter.cs | 256 ++++++++++++++++++ .../Shims.NetStandard2.cs | 91 +++++++ src/Utf8StringInterpolation/Shims.cs | 38 +-- .../Utf8StringInterpolation.csproj | 14 +- .../Utf8StringWriter.cs | 11 +- .../FormatTest.cs | 5 - .../Utf8StringInterpolation.Tests/JoinTest.cs | 48 ++-- tests/Utf8StringInterpolation.Tests/MathF.cs | 13 + tests/Utf8StringInterpolation.Tests/Shims.cs | 13 + .../Utf8StringInterpolation.Tests.csproj | 4 +- 10 files changed, 420 insertions(+), 73 deletions(-) create mode 100644 src/Utf8StringInterpolation/ArrayBufferWriter.cs create mode 100644 src/Utf8StringInterpolation/Shims.NetStandard2.cs create mode 100644 tests/Utf8StringInterpolation.Tests/MathF.cs create mode 100644 tests/Utf8StringInterpolation.Tests/Shims.cs diff --git a/src/Utf8StringInterpolation/ArrayBufferWriter.cs b/src/Utf8StringInterpolation/ArrayBufferWriter.cs new file mode 100644 index 0000000..0dd1db3 --- /dev/null +++ b/src/Utf8StringInterpolation/ArrayBufferWriter.cs @@ -0,0 +1,256 @@ +#if NET6_0_OR_GREATER || NETSTANDARD2_1 + +using System.Runtime.CompilerServices; +[assembly: TypeForwardedTo(typeof(System.Buffers.ArrayBufferWriter<>))] + +#else + +using System; +using System.Diagnostics; + +namespace System.Buffers +{ + /// + /// Represents a heap-based, array-backed output sink into which data can be written. + /// + /// + public sealed class ArrayBufferWriter : IBufferWriter + { + // Copy of Array.MaxLength. + // Used by projects targeting .NET Framework. + private const int ArrayMaxLength = 0x7FFFFFC7; + + private const int DefaultInitialBufferSize = 256; + + private T[] _buffer; + private int _index; + + + /// + /// Creates an instance of an , in which data can be written to, + /// with the default initial capacity. + /// + public ArrayBufferWriter() + { + _buffer = Array.Empty(); + _index = 0; + } + + /// + /// Creates an instance of an , in which data can be written to, + /// with an initial capacity specified. + /// + /// The minimum capacity with which to initialize the underlying buffer. + /// + /// Thrown when is not positive (i.e. less than or equal to 0). + /// + public ArrayBufferWriter(int initialCapacity) + { + if (initialCapacity <= 0) + throw new ArgumentException(null, nameof(initialCapacity)); + + _buffer = new T[initialCapacity]; + _index = 0; + } + + /// + /// Returns the data written to the underlying buffer so far, as a . + /// + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, _index); + + /// + /// Returns the data written to the underlying buffer so far, as a . + /// + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _index); + + /// + /// Returns the amount of data written to the underlying buffer so far. + /// + public int WrittenCount => _index; + + /// + /// Returns the total amount of space within the underlying buffer. + /// + public int Capacity => _buffer.Length; + + /// + /// Returns the amount of space available that can still be written into without forcing the underlying buffer to grow. + /// + public int FreeCapacity => _buffer.Length - _index; + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// + /// You must reset or clear the before trying to re-use it. + /// + /// + /// The method is faster since it only sets to zero the writer's index + /// while the method additionally zeroes the content of the underlying buffer. + /// + /// + /// + public void Clear() + { + Debug.Assert(_buffer.Length >= _index); + _buffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + /// + /// Resets the data written to the underlying buffer without zeroing its content. + /// + /// + /// + /// You must reset or clear the before trying to re-use it. + /// + /// + /// If you reset the writer using the method, the underlying buffer will not be cleared. + /// + /// + /// + public void ResetWrittenCount() => _index = 0; + + /// + /// Notifies that amount of data was written to the output / + /// + /// + /// Thrown when is negative. + /// + /// + /// Thrown when attempting to advance past the end of the underlying buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + public void Advance(int count) + { + if (count < 0) + throw new ArgumentException(null, nameof(count)); + + if (_index > _buffer.Length - count) + ThrowInvalidOperationException_AdvancedTooFar(_buffer.Length); + + _index += count; + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// If no is provided (or it's equal to 0), some non-empty buffer is returned. + /// + /// + /// Thrown when is negative. + /// + /// + /// + /// This will never return an empty . + /// + /// + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + /// + /// If you reset the writer using the method, this method may return a non-cleared . + /// + /// + /// If you clear the writer using the method, this method will return a with its content zeroed. + /// + /// + public Memory GetMemory(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + Debug.Assert(_buffer.Length > _index); + return _buffer.AsMemory(_index); + } + + /// + /// Returns a to write to that is at least the requested length (specified by ). + /// If no is provided (or it's equal to 0), some non-empty buffer is returned. + /// + /// + /// Thrown when is negative. + /// + /// + /// + /// This will never return an empty . + /// + /// + /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer. + /// + /// + /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer. + /// + /// + /// If you reset the writer using the method, this method may return a non-cleared . + /// + /// + /// If you clear the writer using the method, this method will return a with its content zeroed. + /// + /// + public Span GetSpan(int sizeHint = 0) + { + CheckAndResizeBuffer(sizeHint); + Debug.Assert(_buffer.Length > _index); + return _buffer.AsSpan(_index); + } + + private void CheckAndResizeBuffer(int sizeHint) + { + if (sizeHint < 0) + throw new ArgumentException(nameof(sizeHint)); + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > FreeCapacity) + { + int currentLength = _buffer.Length; + + // Attempt to grow by the larger of the sizeHint and double the current size. + int growBy = Math.Max(sizeHint, currentLength); + + if (currentLength == 0) + { + growBy = Math.Max(growBy, DefaultInitialBufferSize); + } + + int newSize = currentLength + growBy; + + if ((uint)newSize > int.MaxValue) + { + // Attempt to grow to ArrayMaxLength. + uint needed = (uint)(currentLength - FreeCapacity + sizeHint); + Debug.Assert(needed > currentLength); + + if (needed > ArrayMaxLength) + { + ThrowOutOfMemoryException(needed); + } + + newSize = ArrayMaxLength; + } + + Array.Resize(ref _buffer, newSize); + } + + Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint); + } + + private static void ThrowInvalidOperationException_AdvancedTooFar(int capacity) + { + throw new InvalidOperationException(); + } + + private static void ThrowOutOfMemoryException(uint capacity) + { + throw new OutOfMemoryException(); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Utf8StringInterpolation/Shims.NetStandard2.cs b/src/Utf8StringInterpolation/Shims.NetStandard2.cs new file mode 100644 index 0000000..bc66ffb --- /dev/null +++ b/src/Utf8StringInterpolation/Shims.NetStandard2.cs @@ -0,0 +1,91 @@ +#if NETSTANDARD2_0 + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Utf8StringInterpolation +{ + internal static partial class Shims + { + public static string GetString(this Encoding encoding, scoped ReadOnlySpan bytes) + { + if (bytes.IsEmpty) return string.Empty; + + unsafe + { + fixed (byte* pB = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetString(pB, bytes.Length); + } + } + } + + public static int GetByteCount(this Encoding encoding, scoped ReadOnlySpan chars) + { + unsafe + { + fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) + { + return encoding.GetByteCount(charsPtr, chars.Length); + } + } + } + + public static int GetBytes(this Encoding encoding, scoped ReadOnlySpan chars, scoped Span bytes) + { + unsafe + { + fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) + fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); + } + } + } + + private static bool TryFormat(this DateTime value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) + { + string s = value.ToString(format, formatProvider); + if (s.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + s.AsSpan().CopyTo(destination); + charsWritten = s.Length; + return true; + } + + private static bool TryFormat(this DateTimeOffset value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) + { + string s = value.ToString(format, formatProvider); + if (s.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + s.AsSpan().CopyTo(destination); + charsWritten = s.Length; + return true; + } + + private static bool TryFormat(this TimeSpan value, scoped Span destination, out int charsWritten, string? format, IFormatProvider? formatProvider) + { + string s = value.ToString(format, formatProvider); + if (s.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + s.AsSpan().CopyTo(destination); + charsWritten = s.Length; + return true; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Utf8StringInterpolation/Shims.cs b/src/Utf8StringInterpolation/Shims.cs index 416e9da..a03b898 100644 --- a/src/Utf8StringInterpolation/Shims.cs +++ b/src/Utf8StringInterpolation/Shims.cs @@ -9,7 +9,7 @@ namespace Utf8StringInterpolation { #if !NET8_0_OR_GREATER - internal static class Shims + internal static partial class Shims { public static bool TryFormat(this DateTime value, Span utf8Destination, out int bytesWritten, string? format, IFormatProvider? formatProvider) { @@ -116,38 +116,4 @@ public static bool TryFormat(this char value, Span utf8Destination, out in } #endif -} - -#if NETSTANDARD2_1 - -namespace System.Runtime.CompilerServices -{ - /// Indicates the attributed type is to be used as an interpolated string handler. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] - public sealed class InterpolatedStringHandlerAttribute : Attribute - { - /// Initializes the . - public InterpolatedStringHandlerAttribute() { } - } - - /// Indicates which arguments to a method involving an interpolated string handler should be passed to that handler. - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute - { - /// Initializes a new instance of the class. - /// The name of the argument that should be passed to the handler. - /// The empty string may be used as the name of the receiver in an instance method. - public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = new string[] { argument }; - - /// Initializes a new instance of the class. - /// The names of the arguments that should be passed to the handler. - /// The empty string may be used as the name of the receiver in an instance method. - public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => Arguments = arguments; - - /// Gets the names of the arguments that should be passed to the handler. - /// The empty string may be used as the name of the receiver in an instance method. - public string[] Arguments { get; } - } -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Utf8StringInterpolation/Utf8StringInterpolation.csproj b/src/Utf8StringInterpolation/Utf8StringInterpolation.csproj index a298f8c..710765a 100644 --- a/src/Utf8StringInterpolation/Utf8StringInterpolation.csproj +++ b/src/Utf8StringInterpolation/Utf8StringInterpolation.csproj @@ -1,7 +1,7 @@  - netstandard2.1;net6.0;net8.0 + netstandard2.0;netstandard2.1;net6.0;net8.0 enable 11 enable @@ -11,6 +11,18 @@ Successor of ZString; UTF8 based zero allocation high-peformance String Interpolation and StringBuilder. + + true + + + + + + + + + + diff --git a/src/Utf8StringInterpolation/Utf8StringWriter.cs b/src/Utf8StringInterpolation/Utf8StringWriter.cs index 53f9990..04f1d39 100644 --- a/src/Utf8StringInterpolation/Utf8StringWriter.cs +++ b/src/Utf8StringInterpolation/Utf8StringWriter.cs @@ -1,11 +1,12 @@ #pragma warning disable CA2014 // Do not use stackalloc in loops using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using Utf8StringInterpolation.Internal; + #if NET6_0_OR_GREATER +using System.Diagnostics; using System.Text.Unicode; #endif @@ -173,10 +174,10 @@ public void AppendFormatted(string value, int alignment = 0, string? format = nu // add left whitespace if (alignment > 0) { - var max = GetStringByteCount(value); + var max = GetStringByteCount(value.AsSpan()); var rentArray = ArrayPool.Shared.Rent(max); var buffer = rentArray.AsSpan(); - var bytesWritten = Encoding.UTF8.GetBytes(value, buffer); + var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), buffer); var space = alignment - bytesWritten; if (space > 0) @@ -194,9 +195,9 @@ public void AppendFormatted(string value, int alignment = 0, string? format = nu else { // add right whitespace - var max = GetStringByteCount(value); + var max = GetStringByteCount(value.AsSpan()); TryGrow(max); - var bytesWritten = Encoding.UTF8.GetBytes(value, destination); + var bytesWritten = Encoding.UTF8.GetBytes(value.AsSpan(), destination); destination = destination.Slice(bytesWritten); currentWritten += bytesWritten; diff --git a/tests/Utf8StringInterpolation.Tests/FormatTest.cs b/tests/Utf8StringInterpolation.Tests/FormatTest.cs index 448eb28..aeec508 100644 --- a/tests/Utf8StringInterpolation.Tests/FormatTest.cs +++ b/tests/Utf8StringInterpolation.Tests/FormatTest.cs @@ -1,9 +1,4 @@ -using FluentAssertions; -using System; -using System.Buffers; using System.Numerics; -using System.Text; -using Xunit; namespace Utf8StringInterpolation.Tests { diff --git a/tests/Utf8StringInterpolation.Tests/JoinTest.cs b/tests/Utf8StringInterpolation.Tests/JoinTest.cs index e829237..e5d2f14 100644 --- a/tests/Utf8StringInterpolation.Tests/JoinTest.cs +++ b/tests/Utf8StringInterpolation.Tests/JoinTest.cs @@ -61,29 +61,29 @@ public void JoinOverloads2() public void JoinOverloads3() { // Utf8String.Join(",", new string[] { }.ToList()).Should().Be(string.Join(',', new string[0])); - Utf8String.Join(",", new[] { 1 }.ToList()).Should().Be(string.Join(',', new[] { 1 })); - Utf8String.Join(",", new[] { 1, 2 }.ToList()).Should().Be(string.Join(',', new[] { 1, 2 })); - Utf8String.Join(",", new[] { 1, 2, 3 }.ToList()).Should().Be(string.Join(',', new[] { 1, 2, 3 })); - - Utf8String.Join(",", new int[] { }).Should().Be(string.Join(',', new string[0])); - Utf8String.Join(",", new[] { 1 }).Should().Be(string.Join(',', new[] { 1 })); - Utf8String.Join(",", new[] { 1, 2 }).Should().Be(string.Join(',', new[] { 1, 2 })); - Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(string.Join(',', new[] { 1, 2, 3 })); - - Utf8String.Join(",", new int[] { }).Should().Be(string.Join(',', new string[0])); - Utf8String.Join(",", new[] { 1 }).Should().Be(string.Join(',', new[] { 1 })); - Utf8String.Join(",", new[] { 1, 2 }).Should().Be(string.Join(',', new[] { 1, 2 })); - Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(string.Join(',', new[] { 1, 2, 3 })); - - Utf8String.Join(",", new int[] { }).Should().Be(string.Join(',', new string[0])); - Utf8String.Join(",", new[] { 1 }).Should().Be(string.Join(',', new[] { 1 })); - Utf8String.Join(",", new[] { 1, 2 }).Should().Be(string.Join(',', new[] { 1, 2 })); - Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(string.Join(',', new[] { 1, 2, 3 })); - - Utf8String.Join(",", new int[] { }).Should().Be(string.Join(',', new string[0])); - Utf8String.Join(",", new[] { 1 }).Should().Be(string.Join(',', new[] { 1 })); - Utf8String.Join(",", new[] { 1, 2 }).Should().Be(string.Join(',', new[] { 1, 2 })); - Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(string.Join(',', new[] { 1, 2, 3 })); + Utf8String.Join(",", new[] { 1 }.ToList()).Should().Be(Shims.Join(',', new[] { 1 })); + Utf8String.Join(",", new[] { 1, 2 }.ToList()).Should().Be(Shims.Join(',', new[] { 1, 2 })); + Utf8String.Join(",", new[] { 1, 2, 3 }.ToList()).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); + + Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); + Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); + Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); + Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); + + Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); + Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); + Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); + Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); + + Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); + Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); + Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); + Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); + + Utf8String.Join(",", new int[] { }).Should().Be(Shims.Join(',', new string[0])); + Utf8String.Join(",", new[] { 1 }).Should().Be(Shims.Join(',', new[] { 1 })); + Utf8String.Join(",", new[] { 1, 2 }).Should().Be(Shims.Join(',', new[] { 1, 2 })); + Utf8String.Join(",", new[] { 1, 2, 3 }).Should().Be(Shims.Join(',', new[] { 1, 2, 3 })); } [Fact] @@ -100,7 +100,7 @@ public void ConcatHugeString() var b = new string('b', 1000000); var actrual = Utf8String.Join(",", new string[] { a, b }); - var expected = string.Join(',', new string[] { a, b }); + var expected = Shims.Join(',', new string[] { a, b }); actrual.Should().Be(expected); } diff --git a/tests/Utf8StringInterpolation.Tests/MathF.cs b/tests/Utf8StringInterpolation.Tests/MathF.cs new file mode 100644 index 0000000..c6cc39f --- /dev/null +++ b/tests/Utf8StringInterpolation.Tests/MathF.cs @@ -0,0 +1,13 @@ +#if NETFRAMEWORK + +using System; + +namespace Utf8StringInterpolation.Tests +{ + internal static class MathF + { + public const float PI = 3.14159265f; + } +} + +#endif \ No newline at end of file diff --git a/tests/Utf8StringInterpolation.Tests/Shims.cs b/tests/Utf8StringInterpolation.Tests/Shims.cs new file mode 100644 index 0000000..fcc37b2 --- /dev/null +++ b/tests/Utf8StringInterpolation.Tests/Shims.cs @@ -0,0 +1,13 @@ +using System; + +namespace Utf8StringInterpolation.Tests +{ + internal static class Shims + { +#if NETFRAMEWORK + public static string Join(char separator, IEnumerable values) => string.Join(separator.ToString(), values); +#else + public static string Join(char separator, IEnumerable values) => string.Join(separator, values); +#endif + } +} \ No newline at end of file diff --git a/tests/Utf8StringInterpolation.Tests/Utf8StringInterpolation.Tests.csproj b/tests/Utf8StringInterpolation.Tests/Utf8StringInterpolation.Tests.csproj index 607d57a..1324c5a 100644 --- a/tests/Utf8StringInterpolation.Tests/Utf8StringInterpolation.Tests.csproj +++ b/tests/Utf8StringInterpolation.Tests/Utf8StringInterpolation.Tests.csproj @@ -1,7 +1,7 @@ - + - net5.0;net6.0;net8.0 + net48;net5.0;net6.0;net8.0 enable enable 11.0