diff --git a/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index 4bdfcc8d..04ce65dc 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; +using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -45,21 +46,17 @@ public static Stream AsStream(this IBufferWriter writer) /// The target instance to write to. /// The input value to write to . /// Thrown if reaches the end. + /// + /// The sizeHint passed to is a hint, not a demand: a writer is + /// free to return less. The write is therefore delegated to , which loops over + /// whatever spans it is given, rather than requiring to fit in a single span. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void Write(this IBufferWriter writer, T value) where T : unmanaged { - Span span = writer.GetSpan(sizeof(T)); - - if (span.Length < sizeof(T)) - { - ThrowArgumentExceptionForEndOfBuffer(); - } - - ref byte r0 = ref MemoryMarshal.GetReference(span); - - Unsafe.WriteUnaligned(ref r0, value); - - writer.Advance(sizeof(T)); + // the address of a parameter is stack-based, so this needs no pinning + BuffersExtensions.Write(writer, new ReadOnlySpan(&value, sizeof(T))); } /// @@ -91,16 +88,16 @@ public static void Write(this IBufferWriter writer, T value) /// The target instance to write to. /// The input to write to . /// Thrown if reaches the end. + /// + /// The sizeHint passed to is a hint, not a demand: a writer is + /// free to return less. The write is therefore delegated to , which loops over + /// whatever spans it is given, rather than requiring to fit in a single span. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Write(this IBufferWriter writer, ReadOnlySpan span) where T : unmanaged { - ReadOnlySpan source = MemoryMarshal.AsBytes(span); - Span destination = writer.GetSpan(source.Length); - - source.CopyTo(destination); - - writer.Advance(source.Length); + BuffersExtensions.Write(writer, MemoryMarshal.AsBytes(span)); } #if !NETSTANDARD2_1_OR_GREATER @@ -111,14 +108,17 @@ public static void Write(this IBufferWriter writer, ReadOnlySpan spa /// The target instance to write to. /// The input to write to . /// Thrown if reaches the end. + /// + /// This is not an extension method: has the same signature and is always + /// available (the System.Memory package is a dependency of this package on netstandard2.0), so an extension + /// method here would only be an ambiguity. It remains for binary compatibility. + /// + [Obsolete("Use System.Buffers.BuffersExtensions.Write instead; this overload exists only for binary compatibility.")] + [EditorBrowsable(EditorBrowsableState.Never)] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Write(this IBufferWriter writer, ReadOnlySpan span) + public static void Write(IBufferWriter writer, ReadOnlySpan span) { - Span destination = writer.GetSpan(span.Length); - - span.CopyTo(destination); - - writer.Advance(span.Length); + BuffersExtensions.Write(writer, span); } #endif diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_IBufferWriterExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_IBufferWriterExtensions.cs index 696faee7..06e921ec 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_IBufferWriterExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_IBufferWriterExtensions.cs @@ -3,10 +3,13 @@ // See the LICENSE file in the project root for more information. using System; -#if NET6_0_OR_GREATER + +// this was previously #if NET6_0_OR_GREATER, to dodge the CS0121 ambiguity between +// BuffersExtensions.Write and IBufferWriterExtensions.Write on the netstandard2.0 build using System.Buffers; -#endif +using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using CommunityToolkit.HighPerformance.Buffers; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -127,7 +130,7 @@ public void Test_IBufferWriterExtensions_WriteExceedingFreeCapacity() // Leave only one byte of free capacity int count = writer.Capacity - 1; - + for (int i = 0; i < count; i++) { writer.Write(0); @@ -136,4 +139,182 @@ public void Test_IBufferWriterExtensions_WriteExceedingFreeCapacity() // Write 4 bytes writer.Write(1); } + + // See https://github.com/CommunityToolkit/dotnet/issues/1208 + [TestMethod] + public void Test_IBufferWriterExtensions_WriteBytes_SegmentedWriter() + { + byte[] payload = CreateBytes(64); + + SegmentedWriter writer = new(); + + writer.Write(payload); + + CollectionAssert.AreEqual(payload, writer.ToArray()); + + // the payload is 8x the segment size; a single request could not have satisfied it + Assert.IsGreaterThan(1, writer.Requests); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1208 + [TestMethod] + public void Test_IBufferWriterExtensions_WriteBytes_SegmentedWriter_ExtensionSyntax() + { + byte[] payload = CreateBytes(64); + + SegmentedWriter writer = new(); + + // this binds to IBufferWriterExtensions.Write(IBufferWriter, ReadOnlySpan), which + // out-competes BuffersExtensions.Write(IBufferWriter, ReadOnlySpan) on all TFMs; it must + // therefore behave the same as the method it hides + writer.Write(payload.AsSpan()); + + CollectionAssert.AreEqual(payload, writer.ToArray()); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1208 + [TestMethod] + public void Test_IBufferWriterExtensions_WriteItems_SegmentedWriter() + { + int[] payload = CreateInts(64); + + SegmentedWriter writer = new(); + + writer.Write(payload.AsSpan()); + + CollectionAssert.AreEqual(payload, writer.ToArray()); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1208 + [TestMethod] + public void Test_IBufferWriterExtensions_WriteBlittedItems_SegmentedWriter() + { + int[] payload = CreateInts(16); + + SegmentedWriter writer = new(); + + writer.Write(payload); + + byte[] written = writer.ToArray(); + + Assert.HasCount(sizeof(int) * payload.Length, written); + Assert.IsTrue(written.AsSpan().SequenceEqual(MemoryMarshal.AsBytes(payload.AsSpan()))); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1208 + [TestMethod] + public void Test_IBufferWriterExtensions_WriteValue_StraddlingSegmentBoundary() + { + const long Value = 0x0102030405060708; + + SegmentedWriter writer = new(); + + // five bytes of padding leaves three bytes in the current segment, so the value cannot fit + writer.Write(new byte[5]); + writer.Write(Value); + + byte[] written = writer.ToArray(); + + Assert.HasCount(5 + sizeof(long), written); + Assert.AreEqual(Value, MemoryMarshal.Read(written.AsSpan(5))); + } + +#if NETFRAMEWORK + // See https://github.com/CommunityToolkit/dotnet/issues/1208; the T-to-T overload is retained on + // netstandard2.0 (which is what net472 resolves) for binary compatibility, but is no longer an + // extension method + [TestMethod] + public void Test_IBufferWriterExtensions_WriteItems_ObsoleteCompatShim() + { + int[] payload = CreateInts(64); + + SegmentedWriter writer = new(); + +#pragma warning disable CS0618 // obsolete + IBufferWriterExtensions.Write(writer, payload); +#pragma warning restore CS0618 + + CollectionAssert.AreEqual(payload, writer.ToArray()); + } +#endif + + private static byte[] CreateBytes(int count) + { + byte[] payload = new byte[count]; + + new Random(42).NextBytes(payload); + + return payload; + } + + private static int[] CreateInts(int count) + { + int[] payload = new int[count]; + Random random = new(42); + + for (int i = 0; i < payload.Length; i++) + { + payload[i] = random.Next(int.MinValue, int.MaxValue); + } + + return payload; + } + + /// + /// An that never hands out more than elements at a + /// time, whatever sizeHint asks for; the hint is a hint, not a demand. + /// + private sealed class SegmentedWriter : IBufferWriter + { + private const int SegmentSize = 8; + + private readonly List segments = new(); + private readonly List counts = new(); + private int used; + + /// + /// Gets the number of calls received. + /// + public int Requests { get; private set; } + + public Span GetSpan(int sizeHint = 0) + { + Requests++; + + if (this.segments.Count == 0 || this.used == SegmentSize) + { + this.segments.Add(new T[SegmentSize]); + this.counts.Add(0); + this.used = 0; + } + + return this.segments[this.segments.Count - 1].AsSpan(this.used); + } + + public Memory GetMemory(int sizeHint = 0) + { + throw new NotSupportedException(); + } + + public void Advance(int count) + { + this.used += count; + this.counts[this.counts.Count - 1] = this.used; + } + + public T[] ToArray() + { + List result = new(); + + for (int i = 0; i < this.segments.Count; i++) + { + for (int j = 0; j < this.counts[i]; j++) + { + result.Add(this.segments[i][j]); + } + } + + return result.ToArray(); + } + } }