From 79f5bad83cf82b717528f4e57377055954924696 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 24 Jun 2020 16:02:13 +1200 Subject: [PATCH 1/2] Change ByteString to use memory and support unsafe create without copy --- Makefile.am | 5 +- .../ByteStringBenchmark.cs | 72 +++++++++ .../Google.Protobuf.Test/ByteStringTest.cs | 64 ++++++++ csharp/src/Google.Protobuf/ByteString.cs | 142 +++++++++--------- csharp/src/Google.Protobuf/ByteStringAsync.cs | 64 ++++++++ .../Google.Protobuf/UnsafeByteOperations.cs | 81 ++++++++++ 6 files changed, 353 insertions(+), 75 deletions(-) create mode 100644 csharp/src/Google.Protobuf.Benchmarks/ByteStringBenchmark.cs create mode 100644 csharp/src/Google.Protobuf/ByteStringAsync.cs create mode 100644 csharp/src/Google.Protobuf/UnsafeByteOperations.cs diff --git a/Makefile.am b/Makefile.am index 4fc706bc9cee..4e0452af4c49 100644 --- a/Makefile.am +++ b/Makefile.am @@ -89,6 +89,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf.Benchmarks/BenchmarkDatasetConfig.cs \ csharp/src/Google.Protobuf.Benchmarks/BenchmarkMessage1Proto3.cs \ csharp/src/Google.Protobuf.Benchmarks/Benchmarks.cs \ + csharp/src/Google.Protobuf.Benchmarks/ByteStringBenchmark.cs \ csharp/src/Google.Protobuf.Benchmarks/Google.Protobuf.Benchmarks.csproj \ csharp/src/Google.Protobuf.Benchmarks/GoogleMessageBenchmark.cs \ csharp/src/Google.Protobuf.Benchmarks/ParseMessagesBenchmark.cs \ @@ -171,6 +172,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf.sln \ csharp/src/Google.Protobuf/ByteArray.cs \ csharp/src/Google.Protobuf/ByteString.cs \ + csharp/src/Google.Protobuf/ByteStringAsync.cs \ csharp/src/Google.Protobuf/CodedInputStream.cs \ csharp/src/Google.Protobuf/CodedOutputStream.ComputeSize.cs \ csharp/src/Google.Protobuf/CodedOutputStream.cs \ @@ -268,7 +270,8 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf/WriteContext.cs \ csharp/src/Google.Protobuf/WriteBufferHelper.cs \ csharp/src/Google.Protobuf/UnknownField.cs \ - csharp/src/Google.Protobuf/UnknownFieldSet.cs + csharp/src/Google.Protobuf/UnknownFieldSet.cs \ + csharp/src/Google.Protobuf/UnsafeByteOperations.cs java_EXTRA_DIST= \ java/README.md \ diff --git a/csharp/src/Google.Protobuf.Benchmarks/ByteStringBenchmark.cs b/csharp/src/Google.Protobuf.Benchmarks/ByteStringBenchmark.cs new file mode 100644 index 000000000000..a755850a66e1 --- /dev/null +++ b/csharp/src/Google.Protobuf.Benchmarks/ByteStringBenchmark.cs @@ -0,0 +1,72 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2019 Google Inc. All rights reserved. +// https://github.com/protocolbuffers/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using BenchmarkDotNet.Attributes; + +namespace Google.Protobuf.Benchmarks +{ + /// + /// Benchmarks using ByteString. + /// + [MemoryDiagnoser] + public class ByteStringBenchmark + { + private const int Zero = 0; + private const int Kilobyte = 1024; + private const int _128Kilobytes = 1024 * 128; + private const int Megabyte = 1024 * 1024; + private const int _10Megabytes = 1024 * 1024 * 10; + + byte[] byteBuffer; + + [GlobalSetup] + public void GlobalSetup() + { + byteBuffer = new byte[PayloadSize]; + } + + [Params(Zero, Kilobyte, _128Kilobytes, Megabyte, _10Megabytes)] + public int PayloadSize { get; set; } + + [Benchmark] + public ByteString CopyFrom() + { + return ByteString.CopyFrom(byteBuffer); + } + + [Benchmark] + public ByteString UnsafeWrap() + { + return UnsafeByteOperations.UnsafeWrap(byteBuffer); + } + } +} diff --git a/csharp/src/Google.Protobuf.Test/ByteStringTest.cs b/csharp/src/Google.Protobuf.Test/ByteStringTest.cs index adbbc3ef5470..bed8c8b8aa0e 100644 --- a/csharp/src/Google.Protobuf.Test/ByteStringTest.cs +++ b/csharp/src/Google.Protobuf.Test/ByteStringTest.cs @@ -34,6 +34,9 @@ using System.Text; using NUnit.Framework; using System.IO; +using System.Collections.Generic; +using System.Collections; +using System.Linq; #if !NET35 using System.Threading.Tasks; #endif @@ -54,6 +57,7 @@ public void Equality() EqualityTester.AssertInequality(b1, b3); EqualityTester.AssertInequality(b1, b4); EqualityTester.AssertInequality(b1, null); + EqualityTester.AssertEquality(ByteString.Empty, ByteString.Empty); #pragma warning disable 1718 // Deliberately calling ==(b1, b1) and !=(b1, b1) Assert.IsTrue(b1 == b1); Assert.IsTrue(b1 == b2); @@ -63,6 +67,7 @@ public void Equality() Assert.IsTrue((ByteString) null == null); Assert.IsFalse(b1 != b1); Assert.IsFalse(b1 != b2); + Assert.IsTrue(ByteString.Empty == ByteString.Empty); #pragma warning disable 1718 Assert.IsTrue(b1 != b3); Assert.IsTrue(b1 != b4); @@ -154,6 +159,65 @@ public void CopyFromPortion() Assert.AreEqual(3, bs[1]); } + [Test] + public void CopyTo() + { + byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; + ByteString bs = ByteString.CopyFrom(data); + + byte[] dest = new byte[data.Length]; + bs.CopyTo(dest, 0); + + CollectionAssert.AreEqual(data, dest); + } + + [Test] + public void GetEnumerator() + { + byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; + ByteString bs = ByteString.CopyFrom(data); + + IEnumerator genericEnumerator = bs.GetEnumerator(); + Assert.IsTrue(genericEnumerator.MoveNext()); + Assert.AreEqual(0, genericEnumerator.Current); + + IEnumerator enumerator = ((IEnumerable)bs).GetEnumerator(); + Assert.IsTrue(enumerator.MoveNext()); + Assert.AreEqual(0, enumerator.Current); + + // Call via LINQ + CollectionAssert.AreEqual(bs.Span.ToArray(), bs.ToArray()); + } + + [Test] + public void UnsafeWrap() + { + byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; + ByteString bs = UnsafeByteOperations.UnsafeWrap(data.AsMemory(2, 3)); + ReadOnlySpan s = bs.Span; + + Assert.AreEqual(3, s.Length); + Assert.AreEqual(2, s[0]); + Assert.AreEqual(3, s[1]); + Assert.AreEqual(4, s[2]); + + // Check that the value is not a copy + data[2] = byte.MaxValue; + Assert.AreEqual(byte.MaxValue, s[0]); + } + + [Test] + public void WriteToStream() + { + byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6 }; + ByteString bs = ByteString.CopyFrom(data); + + MemoryStream ms = new MemoryStream(); + bs.WriteTo(ms); + + CollectionAssert.AreEqual(data, ms.ToArray()); + } + [Test] public void ToStringUtf8() { diff --git a/csharp/src/Google.Protobuf/ByteString.cs b/csharp/src/Google.Protobuf/ByteString.cs index 45b3885a5cbe..2619be5bef2d 100644 --- a/csharp/src/Google.Protobuf/ByteString.cs +++ b/csharp/src/Google.Protobuf/ByteString.cs @@ -34,6 +34,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Security; using System.Text; #if !NET35 @@ -49,40 +50,26 @@ namespace Google.Protobuf /// /// Immutable array of bytes. /// + [SecuritySafeCritical] public sealed class ByteString : IEnumerable, IEquatable { private static readonly ByteString empty = new ByteString(new byte[0]); - private readonly byte[] bytes; + private readonly ReadOnlyMemory bytes; /// - /// Unsafe operations that can cause IO Failure and/or other catastrophic side-effects. + /// Internal use only. Ensure that the provided memory is not mutated and belongs to this instance. /// - internal static class Unsafe - { - /// - /// Constructs a new ByteString from the given byte array. The array is - /// *not* copied, and must not be modified after this constructor is called. - /// - internal static ByteString FromBytes(byte[] bytes) - { - return new ByteString(bytes); - } - } - - /// - /// Internal use only. Ensure that the provided array is not mutated and belongs to this instance. - /// - internal static ByteString AttachBytes(byte[] bytes) + internal static ByteString AttachBytes(ReadOnlyMemory bytes) { return new ByteString(bytes); } /// - /// Constructs a new ByteString from the given byte array. The array is + /// Constructs a new ByteString from the given memory. The memory is /// *not* copied, and must not be modified after this constructor is called. /// - private ByteString(byte[] bytes) + private ByteString(ReadOnlyMemory bytes) { this.bytes = bytes; } @@ -117,11 +104,7 @@ public bool IsEmpty /// public ReadOnlySpan Span { - [SecuritySafeCritical] - get - { - return new ReadOnlySpan(bytes); - } + get { return bytes.Span; } } /// @@ -130,11 +113,7 @@ public ReadOnlySpan Span /// public ReadOnlyMemory Memory { - [SecuritySafeCritical] - get - { - return new ReadOnlyMemory(bytes); - } + get { return bytes; } } /// @@ -144,7 +123,7 @@ public ReadOnlyMemory Memory /// A byte array with the same data as this ByteString. public byte[] ToByteArray() { - return (byte[]) bytes.Clone(); + return bytes.ToArray(); } /// @@ -153,7 +132,16 @@ public byte[] ToByteArray() /// A base64 representation of this ByteString. public string ToBase64() { - return Convert.ToBase64String(bytes); + if (MemoryMarshal.TryGetArray(bytes, out ArraySegment segment)) + { + // Fast path. ByteString was created with an array, so pass the underlying array. + return Convert.ToBase64String(segment.Array, segment.Offset, segment.Count); + } + else + { + // Slow path. BytesString is not an array. Convert memory and pass result to ToBase64String. + return Convert.ToBase64String(bytes.ToArray()); + } } /// @@ -197,21 +185,10 @@ public static ByteString FromStream(Stream stream) /// The stream to copy into a ByteString. /// The cancellation token to use when reading from the stream, if any. /// A ByteString with content read from the given stream. - public async static Task FromStreamAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken)) + public static Task FromStreamAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken)) { ProtoPreconditions.CheckNotNull(stream, nameof(stream)); - int capacity = stream.CanSeek ? checked((int) (stream.Length - stream.Position)) : 0; - var memoryStream = new MemoryStream(capacity); - // We have to specify the buffer size here, as there's no overload accepting the cancellation token - // alone. But it's documented to use 81920 by default if not specified. - await stream.CopyToAsync(memoryStream, 81920, cancellationToken); -#if NETSTANDARD1_1 || NETSTANDARD2_0 - byte[] bytes = memoryStream.ToArray(); -#else - // Avoid an extra copy if we can. - byte[] bytes = memoryStream.Length == memoryStream.Capacity ? memoryStream.GetBuffer() : memoryStream.ToArray(); -#endif - return AttachBytes(bytes); + return ByteStringAsync.FromStreamAsyncCore(stream, cancellationToken); } #endif @@ -242,7 +219,6 @@ public static ByteString CopyFrom(byte[] bytes, int offset, int count) /// are copied, so further modifications to the span will not /// be reflected in the returned . /// - [SecuritySafeCritical] public static ByteString CopyFrom(ReadOnlySpan bytes) { return new ByteString(bytes.ToArray()); @@ -270,7 +246,7 @@ public static ByteString CopyFromUtf8(string text) /// public byte this[int index] { - get { return bytes[index]; } + get { return bytes.Span[index]; } } /// @@ -284,7 +260,18 @@ public static ByteString CopyFromUtf8(string text) /// The result of decoding the binary data with the given decoding. public string ToString(Encoding encoding) { - return encoding.GetString(bytes, 0, bytes.Length); + if (MemoryMarshal.TryGetArray(bytes, out ArraySegment segment)) + { + // Fast path. ByteString was created with an array. + return encoding.GetString(segment.Array, segment.Offset, segment.Count); + } + else + { + // Slow path. BytesString is not an array. Convert memory and pass result to GetString. + // TODO: Consider using GetString overload that takes a pointer. + byte[] array = bytes.ToArray(); + return encoding.GetString(array, 0, array.Length); + } } /// @@ -304,9 +291,10 @@ public string ToStringUtf8() /// Returns an iterator over the bytes in this . /// /// An iterator over the bytes in this object. + [SecuritySafeCritical] public IEnumerator GetEnumerator() { - return ((IEnumerable) bytes).GetEnumerator(); + return MemoryMarshal.ToEnumerable(bytes).GetEnumerator(); } /// @@ -324,7 +312,17 @@ IEnumerator IEnumerable.GetEnumerator() public CodedInputStream CreateCodedInput() { // We trust CodedInputStream not to reveal the provided byte array or modify it - return new CodedInputStream(bytes); + if (MemoryMarshal.TryGetArray(bytes, out ArraySegment segment) && segment.Count == bytes.Length) + { + // Fast path. ByteString was created with a complete array. + return new CodedInputStream(segment.Array); + } + else + { + // Slow path. BytesString is not an array, or is a slice of an array. + // Convert memory and pass result to WriteRawBytes. + return new CodedInputStream(bytes.ToArray()); + } } /// @@ -343,18 +341,8 @@ public CodedInputStream CreateCodedInput() { return false; } - if (lhs.bytes.Length != rhs.bytes.Length) - { - return false; - } - for (int i = 0; i < lhs.Length; i++) - { - if (rhs.bytes[i] != lhs.bytes[i]) - { - return false; - } - } - return true; + + return lhs.bytes.Span.SequenceEqual(rhs.bytes.Span); } /// @@ -373,6 +361,7 @@ public CodedInputStream CreateCodedInput() /// /// The object to compare this with. /// true if refers to an equal ; false otherwise. + [SecuritySafeCritical] public override bool Equals(object obj) { return this == (obj as ByteString); @@ -383,12 +372,15 @@ public override bool Equals(object obj) /// will return the same hash code. /// /// A hash code for this object. + [SecuritySafeCritical] public override int GetHashCode() { + ReadOnlySpan b = bytes.Span; + int ret = 23; - foreach (byte b in bytes) + for (int i = 0; i < b.Length; i++) { - ret = (ret * 31) + b; + ret = (ret * 31) + b[i]; } return ret; } @@ -403,20 +395,12 @@ public bool Equals(ByteString other) return this == other; } - /// - /// Used internally by CodedOutputStream to avoid creating a copy for the write - /// - internal void WriteRawBytesTo(CodedOutputStream outputStream) - { - outputStream.WriteRawBytes(bytes, 0, bytes.Length); - } - /// /// Copies the entire byte array to the destination array provided at the offset specified. /// public void CopyTo(byte[] array, int position) { - ByteArray.Copy(bytes, 0, array, position, bytes.Length); + bytes.CopyTo(array.AsMemory(position)); } /// @@ -424,7 +408,17 @@ public void CopyTo(byte[] array, int position) /// public void WriteTo(Stream outputStream) { - outputStream.Write(bytes, 0, bytes.Length); + if (MemoryMarshal.TryGetArray(bytes, out ArraySegment segment)) + { + // Fast path. ByteString was created with an array, so pass the underlying array. + outputStream.Write(segment.Array, segment.Offset, segment.Count); + } + else + { + // Slow path. BytesString is not an array. Convert memory and pass result to WriteRawBytes. + var array = bytes.ToArray(); + outputStream.Write(array, 0, array.Length); + } } } } \ No newline at end of file diff --git a/csharp/src/Google.Protobuf/ByteStringAsync.cs b/csharp/src/Google.Protobuf/ByteStringAsync.cs new file mode 100644 index 000000000000..8bf8add4b8df --- /dev/null +++ b/csharp/src/Google.Protobuf/ByteStringAsync.cs @@ -0,0 +1,64 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Google.Protobuf +{ + /// + /// SecuritySafeCritical attribute can not be placed on types with async methods. + /// This class has ByteString's async methods so it can be marked with SecuritySafeCritical. + /// + internal static class ByteStringAsync + { +#if !NET35 + internal static async Task FromStreamAsyncCore(Stream stream, CancellationToken cancellationToken) + { + int capacity = stream.CanSeek ? checked((int)(stream.Length - stream.Position)) : 0; + var memoryStream = new MemoryStream(capacity); + // We have to specify the buffer size here, as there's no overload accepting the cancellation token + // alone. But it's documented to use 81920 by default if not specified. + await stream.CopyToAsync(memoryStream, 81920, cancellationToken); +#if NETSTANDARD1_1 || NETSTANDARD2_0 + byte[] bytes = memoryStream.ToArray(); +#else + // Avoid an extra copy if we can. + byte[] bytes = memoryStream.Length == memoryStream.Capacity ? memoryStream.GetBuffer() : memoryStream.ToArray(); +#endif + return ByteString.AttachBytes(bytes); + } +#endif + } +} \ No newline at end of file diff --git a/csharp/src/Google.Protobuf/UnsafeByteOperations.cs b/csharp/src/Google.Protobuf/UnsafeByteOperations.cs new file mode 100644 index 000000000000..5ef25d4a18d0 --- /dev/null +++ b/csharp/src/Google.Protobuf/UnsafeByteOperations.cs @@ -0,0 +1,81 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using System; +using System.Security; + +namespace Google.Protobuf +{ + /// + /// Provides a number of unsafe byte operations to be used by advanced applications with high performance + /// requirements. These methods are referred to as "unsafe" due to the fact that they potentially expose + /// the backing buffer of a to the application. + /// + /// + /// + /// The methods in this class should only be called if it is guaranteed that the buffer backing the + /// will never change! Mutation of a can lead to unexpected + /// and undesirable consequences in your application, and will likely be difficult to debug. Proceed with caution! + /// + /// + /// This can have a number of significant side affects that have spooky-action-at-a-distance-like behavior. In + /// particular, if the bytes value changes out from under a Protocol Buffer: + /// + /// + /// + /// serialization may throw + /// + /// + /// serialization may succeed but the wrong bytes may be written out + /// + /// + /// messages are no longer threadsafe + /// + /// + /// hashCode may be incorrect + /// + /// + /// + [SecuritySafeCritical] + public static class UnsafeByteOperations + { + /// + /// Constructs a new from the given bytes. The bytes are not copied, + /// and must not be modified while the is in use. + /// This API is experimental and subject to change. + /// + public static ByteString UnsafeWrap(ReadOnlyMemory bytes) + { + return ByteString.AttachBytes(bytes); + } + } +} From e794919f6b566d425dc6fec30d3f7e8ae61bb7ec Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 18 Nov 2020 22:54:06 +1300 Subject: [PATCH 2/2] PR feedback --- .../Google.Protobuf.Test/ByteStringTest.cs | 94 +++++++++++++++++++ .../Google.Protobuf.Test.csproj | 1 + csharp/src/Google.Protobuf/ByteString.cs | 10 ++ csharp/src/Google.Protobuf/ByteStringAsync.cs | 2 +- .../Google.Protobuf/UnsafeByteOperations.cs | 2 +- 5 files changed, 107 insertions(+), 2 deletions(-) diff --git a/csharp/src/Google.Protobuf.Test/ByteStringTest.cs b/csharp/src/Google.Protobuf.Test/ByteStringTest.cs index bed8c8b8aa0e..d9f607448d9c 100644 --- a/csharp/src/Google.Protobuf.Test/ByteStringTest.cs +++ b/csharp/src/Google.Protobuf.Test/ByteStringTest.cs @@ -37,6 +37,10 @@ using System.Collections.Generic; using System.Collections; using System.Linq; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Threading; +using System.Runtime.CompilerServices; #if !NET35 using System.Threading.Tasks; #endif @@ -218,6 +222,25 @@ public void WriteToStream() CollectionAssert.AreEqual(data, ms.ToArray()); } + [Test] + public void WriteToStream_Stackalloc() + { + byte[] data = Encoding.UTF8.GetBytes("Hello world"); + Span s = stackalloc byte[data.Length]; + data.CopyTo(s); + + MemoryStream ms = new MemoryStream(); + + using (UnmanagedMemoryManager manager = new UnmanagedMemoryManager(s)) + { + ByteString bs = ByteString.AttachBytes(manager.Memory); + + bs.WriteTo(ms); + } + + CollectionAssert.AreEqual(data, ms.ToArray()); + } + [Test] public void ToStringUtf8() { @@ -232,6 +255,21 @@ public void ToStringWithExplicitEncoding() Assert.AreEqual("\u20ac", bs.ToString(Encoding.Unicode)); } + [Test] + public void ToString_Stackalloc() + { + byte[] data = Encoding.UTF8.GetBytes("Hello world"); + Span s = stackalloc byte[data.Length]; + data.CopyTo(s); + + using (UnmanagedMemoryManager manager = new UnmanagedMemoryManager(s)) + { + ByteString bs = ByteString.AttachBytes(manager.Memory); + + Assert.AreEqual("Hello world", bs.ToString(Encoding.UTF8)); + } + } + [Test] public void FromBase64_WithText() { @@ -248,6 +286,29 @@ public void FromBase64_Empty() Assert.AreSame(ByteString.Empty, ByteString.FromBase64("")); } + [Test] + public void ToBase64_Array() + { + ByteString bs = ByteString.CopyFrom(Encoding.UTF8.GetBytes("Hello world")); + + Assert.AreEqual("SGVsbG8gd29ybGQ=", bs.ToBase64()); + } + + [Test] + public void ToBase64_Stackalloc() + { + byte[] data = Encoding.UTF8.GetBytes("Hello world"); + Span s = stackalloc byte[data.Length]; + data.CopyTo(s); + + using (UnmanagedMemoryManager manager = new UnmanagedMemoryManager(s)) + { + ByteString bs = ByteString.AttachBytes(manager.Memory); + + Assert.AreEqual("SGVsbG8gd29ybGQ=", bs.ToBase64()); + } + } + [Test] public void FromStream_Seekable() { @@ -325,5 +386,38 @@ public void GetContentsAsReadOnlyMemory() var copied = byteString.Memory.ToArray(); CollectionAssert.AreEqual(byteString, copied); } + + // Create Memory from non-array source. + // Use by ByteString tests that have optimized path for array backed Memory. + private sealed unsafe class UnmanagedMemoryManager : MemoryManager where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + public UnmanagedMemoryManager(Span span) + { + fixed (T* ptr = &MemoryMarshal.GetReference(span)) + { + _pointer = ptr; + _length = span.Length; + } + } + + public override Span GetSpan() => new Span(_pointer, _length); + + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0 || elementIndex >= _length) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + return new MemoryHandle(_pointer + elementIndex); + } + + public override void Unpin() { } + + protected override void Dispose(bool disposing) { } + } } } \ No newline at end of file diff --git a/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj b/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj index 1a7953d6b786..89fe5d4febbc 100644 --- a/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj +++ b/csharp/src/Google.Protobuf.Test/Google.Protobuf.Test.csproj @@ -6,6 +6,7 @@ true true False + True diff --git a/csharp/src/Google.Protobuf/ByteString.cs b/csharp/src/Google.Protobuf/ByteString.cs index 2619be5bef2d..7828658672fe 100644 --- a/csharp/src/Google.Protobuf/ByteString.cs +++ b/csharp/src/Google.Protobuf/ByteString.cs @@ -65,6 +65,16 @@ internal static ByteString AttachBytes(ReadOnlyMemory bytes) return new ByteString(bytes); } + /// + /// Internal use only. Ensure that the provided memory is not mutated and belongs to this instance. + /// This method encapsulates converting array to memory. Reduces need for SecuritySafeCritical + /// in .NET Framework. + /// + internal static ByteString AttachBytes(byte[] bytes) + { + return AttachBytes(bytes.AsMemory()); + } + /// /// Constructs a new ByteString from the given memory. The memory is /// *not* copied, and must not be modified after this constructor is called. diff --git a/csharp/src/Google.Protobuf/ByteStringAsync.cs b/csharp/src/Google.Protobuf/ByteStringAsync.cs index 8bf8add4b8df..3465cc67b40f 100644 --- a/csharp/src/Google.Protobuf/ByteStringAsync.cs +++ b/csharp/src/Google.Protobuf/ByteStringAsync.cs @@ -51,7 +51,7 @@ internal static async Task FromStreamAsyncCore(Stream stream, Cancel // We have to specify the buffer size here, as there's no overload accepting the cancellation token // alone. But it's documented to use 81920 by default if not specified. await stream.CopyToAsync(memoryStream, 81920, cancellationToken); -#if NETSTANDARD1_1 || NETSTANDARD2_0 +#if NETSTANDARD1_1 byte[] bytes = memoryStream.ToArray(); #else // Avoid an extra copy if we can. diff --git a/csharp/src/Google.Protobuf/UnsafeByteOperations.cs b/csharp/src/Google.Protobuf/UnsafeByteOperations.cs index 5ef25d4a18d0..865ea067b8c8 100644 --- a/csharp/src/Google.Protobuf/UnsafeByteOperations.cs +++ b/csharp/src/Google.Protobuf/UnsafeByteOperations.cs @@ -58,7 +58,7 @@ namespace Google.Protobuf /// serialization may succeed but the wrong bytes may be written out /// /// - /// messages are no longer threadsafe + /// objects that are normally immutable (such as ByteString) are no longer immutable /// /// /// hashCode may be incorrect