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