diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Core.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Core.verified.txt index df6b1461b8e..d0a6f9ed54c 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Core.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Core.verified.txt @@ -3378,6 +3378,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } public void WriteTo(System.IO.Stream stream) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 104827f2cf0..c8dcb164155 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -3796,6 +3796,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } public void WriteTo(System.IO.Stream stream) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt index 9b8a28e3d35..d1553938cc4 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt @@ -3786,6 +3786,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } public void WriteTo(System.IO.Stream stream) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.verified.txt index 280340c6f21..8bdae5a5617 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.verified.txt @@ -3341,6 +3341,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } public void WriteTo(System.IO.Stream stream) { } diff --git a/src/core/Akka.Tests/Util/ByteStringSpec.cs b/src/core/Akka.Tests/Util/ByteStringSpec.cs index 9605b2698c5..053cf6bbd43 100644 --- a/src/core/Akka.Tests/Util/ByteStringSpec.cs +++ b/src/core/Akka.Tests/Util/ByteStringSpec.cs @@ -5,9 +5,12 @@ // //----------------------------------------------------------------------- +using System; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Akka.IO; +using Akka.TestKit; using FluentAssertions; using FsCheck; using Xunit; @@ -42,6 +45,59 @@ public void A_ByteString_must_have_correct_size_when_concatenating() .QuickCheckThrowOnFailure(); } + [Fact] + public void A_ByteString_ToReadOnlySpan_must_have_correct_size() + { + Prop.ForAll((ByteString a, ByteString b) => + { + a.ToReadOnlySpan().Length.Should().Be(a.Count); + b.ToReadOnlySpan().Length.Should().Be(b.Count); + var concat = a + b; + var spanConcat = concat.ToReadOnlySpan(); + return spanConcat.Length == concat.Count; + }).QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_Compact_ToReadOnlySpan_must_have_identical_memory_ref() + { + var bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + var a = ByteString.FromBytes(bytes); + var aSpan = a.ToReadOnlySpan(); + + var memory = new ReadOnlyMemory(bytes); + var spanMemory = memory.Span; + + // span has memory reference to underlying byte array + (aSpan == spanMemory).Should().BeTrue(); + + // Do another ToReadOnlySpan + var span2 = a.ToReadOnlySpan(); + (aSpan == span2).Should().BeTrue(); + } + + [Fact] + public void A_ByteString_ToReadOnlySpan_compacted_must_have_identical_memory_ref() + { + var bytesA = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + var bytesB = new byte[] { 10, 11, 12, 13, 14, 15, 16, 17, 18 }; + var a = ByteString.FromBytes(bytesA); + var b = ByteString.FromBytes(bytesB); + + var concat = a + b; + + var concatSpan = concat.ToReadOnlySpan(); + var concatSpan2 = concat.ToReadOnlySpan(); + // not compact, returns a new span + (concatSpan == concatSpan2).Should().BeFalse(); + + // compact, will return same span + var compacted = concat.Compact(); + var concatSpan3 = compacted.ToReadOnlySpan(); + var concatSpan4 = compacted.ToReadOnlySpan(); + (concatSpan3 == concatSpan4).Should().BeTrue(); + } + [Fact] public void A_ByteString_must_have_correct_size_when_slicing_from_index() { diff --git a/src/core/Akka/Util/ByteString.cs b/src/core/Akka/Util/ByteString.cs index 096725579d8..46436cfc747 100644 --- a/src/core/Akka/Util/ByteString.cs +++ b/src/core/Akka/Util/ByteString.cs @@ -507,6 +507,27 @@ public byte[] ToArray() CopyTo(copy, 0, _count); return copy; } + + /// + /// Returns a ReadOnlySpan over the contents of this ByteString. + /// This is a non-copying operation, when the ByteString is compact. + /// When it is not compact, the contents are copied into a new array. + /// + /// A ReadOnlySpan over the byte data. + public ReadOnlySpan ToReadOnlySpan() + { + if (_count == 0) + return ReadOnlySpan.Empty; + + if (IsCompact) + { + // If compact, data is in a single buffer. + var compactBuffer = _buffers[0]; + return new ReadOnlySpan(compactBuffer.Array, compactBuffer.Offset, compactBuffer.Count); + } + + return new ReadOnlySpan(ToArray()); + } /// /// Appends at the tail