diff --git a/csharp/src/Google.Protobuf/CodedOutputStream.cs b/csharp/src/Google.Protobuf/CodedOutputStream.cs index b4c90454fb..1c4e40b71e 100644 --- a/csharp/src/Google.Protobuf/CodedOutputStream.cs +++ b/csharp/src/Google.Protobuf/CodedOutputStream.cs @@ -636,7 +636,7 @@ namespace Google.Protobuf public void Flush() { var span = new Span(buffer); - state.writeBufferHelper.Flush(ref span, ref state); + WriteBufferHelper.Flush(ref span, ref state); /*if (output != null) { @@ -648,36 +648,18 @@ namespace Google.Protobuf /// Verifies that SpaceLeft returns zero. It's common to create a byte array /// that is exactly big enough to hold a message, then write to it with /// a CodedOutputStream. Calling CheckNoSpaceLeft after writing verifies that - /// the message was actually as big as expected, which can help bugs. + /// the message was actually as big as expected, which can help finding bugs. /// public void CheckNoSpaceLeft() { - if (SpaceLeft != 0) - { - throw new InvalidOperationException("Did not write as much data as expected."); - } + WriteBufferHelper.CheckNoSpaceLeft(ref state); } /// /// If writing to a flat array, returns the space left in the array. Otherwise, /// throws an InvalidOperationException. /// - public int SpaceLeft - { - get - { - if (output == null) - { - return state.limit - state.position; - } - else - { - throw new InvalidOperationException( - "SpaceLeft can only be called on CodedOutputStreams that are " + - "writing to a flat array."); - } - } - } + public int SpaceLeft => WriteBufferHelper.GetSpaceLeft(ref state); internal byte[] InternalBuffer => buffer; diff --git a/csharp/src/Google.Protobuf/MessageExtensions.cs b/csharp/src/Google.Protobuf/MessageExtensions.cs index e9a408c9e2..d0db44bbc3 100644 --- a/csharp/src/Google.Protobuf/MessageExtensions.cs +++ b/csharp/src/Google.Protobuf/MessageExtensions.cs @@ -33,6 +33,7 @@ using Google.Protobuf.Reflection; using System.Buffers; using System.Collections; +using System; using System.IO; using System.Linq; using System.Security; @@ -145,6 +146,40 @@ namespace Google.Protobuf return ByteString.AttachBytes(message.ToByteArray()); } + /// + /// Writes the given message data to the given buffer writer in protobuf encoding. + /// + /// The message to write to the stream. + /// The stream to write to. + public static void WriteTo(this IMessage message, IBufferWriter output) + { + ProtoPreconditions.CheckNotNull(message, nameof(message)); + ProtoPreconditions.CheckNotNull(output, nameof(output)); + + WriteContext.Initialize(output, out WriteContext ctx); + WritingPrimitivesMessages.WriteRawMessage(ref ctx, message); + ctx.Flush(); + + // TODO: handling errors when IBufferWriter is used? + } + + /// + /// Writes the given message data to the given span in protobuf encoding. + /// The size of the destination span needs to fit the serialized size + /// of the message exactly, otherwise an exception is thrown. + /// + /// The message to write to the stream. + /// The span to write to. Size must match size of the message exactly. + public static void WriteTo(this IMessage message, Span output) + { + ProtoPreconditions.CheckNotNull(message, nameof(message)); + + WriteContext.Initialize(ref output, out WriteContext ctx); + WritingPrimitivesMessages.WriteRawMessage(ref ctx, message); + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + /// /// Checks if all required fields in a message have values set. For proto3 messages, this returns true /// diff --git a/csharp/src/Google.Protobuf/WriteBufferHelper.cs b/csharp/src/Google.Protobuf/WriteBufferHelper.cs index bf29b22dd2..9bd5061da4 100644 --- a/csharp/src/Google.Protobuf/WriteBufferHelper.cs +++ b/csharp/src/Google.Protobuf/WriteBufferHelper.cs @@ -62,7 +62,7 @@ namespace Google.Protobuf } /// - /// Initialize an instance with a coded output stream. + /// Initialize an instance with a buffer writer. /// This approach is faster than using a constructor because the instance to initialize is passed by reference /// and we can write directly into it without copying. /// @@ -74,21 +74,65 @@ namespace Google.Protobuf buffer = default; // TODO: initialize the initial buffer so that the first write is not via slowpath. } - public void RefreshBuffer(ref Span buffer, ref WriterInternalState state) + /// + /// Initialize an instance with a buffer represented by a single span (i.e. buffer cannot be refreshed) + /// This approach is faster than using a constructor because the instance to initialize is passed by reference + /// and we can write directly into it without copying. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InitializeNonRefreshable(out WriteBufferHelper instance) + { + instance.bufferWriter = null; + instance.codedOutputStream = null; + } + + /// + /// Verifies that SpaceLeft returns zero. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckNoSpaceLeft(ref WriterInternalState state) + { + if (GetSpaceLeft(ref state) != 0) + { + throw new InvalidOperationException("Did not write as much data as expected."); + } + } + + /// + /// If writing to a flat array, returns the space left in the array. Otherwise, + /// throws an InvalidOperationException. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetSpaceLeft(ref WriterInternalState state) + { + if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream == null && state.writeBufferHelper.bufferWriter == null) + { + return state.limit - state.position; + } + else + { + throw new InvalidOperationException( + "SpaceLeft can only be called on CodedOutputStreams that are " + + "writing to a flat array or when writing to a single span."); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RefreshBuffer(ref Span buffer, ref WriterInternalState state) { - if (codedOutputStream?.InternalOutputStream != null) + if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream != null) { // because we're using coded output stream, we know that "buffer" and codedOutputStream.InternalBuffer are identical. - codedOutputStream.InternalOutputStream.Write(codedOutputStream.InternalBuffer, 0, state.position); + state.writeBufferHelper.codedOutputStream.InternalOutputStream.Write(state.writeBufferHelper.codedOutputStream.InternalBuffer, 0, state.position); // reset position, limit stays the same because we are reusing the codedOutputStream's internal buffer. state.position = 0; } - else if (bufferWriter != null) + else if (state.writeBufferHelper.bufferWriter != null) { // commit the bytes and get a new buffer to write to. - bufferWriter.Advance(state.position); + state.writeBufferHelper.bufferWriter.Advance(state.position); state.position = 0; - buffer = bufferWriter.GetSpan(); + buffer = state.writeBufferHelper.bufferWriter.GetSpan(); state.limit = buffer.Length; } else @@ -98,17 +142,18 @@ namespace Google.Protobuf } } - public void Flush(ref Span buffer, ref WriterInternalState state) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Flush(ref Span buffer, ref WriterInternalState state) { - if (codedOutputStream?.InternalOutputStream != null) + if (state.writeBufferHelper.codedOutputStream?.InternalOutputStream != null) { // because we're using coded output stream, we know that "buffer" and codedOutputStream.InternalBuffer are identical. - codedOutputStream.InternalOutputStream.Write(codedOutputStream.InternalBuffer, 0, state.position); + state.writeBufferHelper.codedOutputStream.InternalOutputStream.Write(state.writeBufferHelper.codedOutputStream.InternalBuffer, 0, state.position); state.position = 0; } - else if (bufferWriter != null) + else if (state.writeBufferHelper.bufferWriter != null) { - bufferWriter.Advance(state.position); + state.writeBufferHelper.bufferWriter.Advance(state.position); state.position = 0; state.limit = 0; buffer = default; // invalidate the current buffer diff --git a/csharp/src/Google.Protobuf/WriteContext.cs b/csharp/src/Google.Protobuf/WriteContext.cs index c4d0343e47..e822e1d6da 100644 --- a/csharp/src/Google.Protobuf/WriteContext.cs +++ b/csharp/src/Google.Protobuf/WriteContext.cs @@ -87,6 +87,16 @@ namespace Google.Protobuf ctx.state.position = 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Initialize(ref Span buffer, out WriteContext ctx) + { + ctx.buffer = buffer; + ctx.state = default; + ctx.state.limit = ctx.buffer.Length; + ctx.state.position = 0; + WriteBufferHelper.InitializeNonRefreshable(out ctx.state.writeBufferHelper); + } + /// /// Writes a double field value, without a tag. /// @@ -340,8 +350,12 @@ namespace Google.Protobuf internal void Flush() { - // TODO: should the method be static or not? - state.writeBufferHelper.Flush(ref buffer, ref state); + WriteBufferHelper.Flush(ref buffer, ref state); + } + + internal void CheckNoSpaceLeft() + { + WriteBufferHelper.CheckNoSpaceLeft(ref state); } internal void CopyStateTo(CodedOutputStream output) diff --git a/csharp/src/Google.Protobuf/WritingPrimitives.cs b/csharp/src/Google.Protobuf/WritingPrimitives.cs index 76df2df558..f618789aab 100644 --- a/csharp/src/Google.Protobuf/WritingPrimitives.cs +++ b/csharp/src/Google.Protobuf/WritingPrimitives.cs @@ -376,7 +376,7 @@ namespace Google.Protobuf { if (state.position == state.limit) { - state.writeBufferHelper.RefreshBuffer(ref buffer, ref state); + WriteBufferHelper.RefreshBuffer(ref buffer, ref state); } buffer[state.position++] = value; @@ -429,7 +429,7 @@ namespace Google.Protobuf value.Slice(bytesWritten, length).CopyTo(buffer.Slice(state.position, length)); bytesWritten += length; state.position += length; - state.writeBufferHelper.RefreshBuffer(ref buffer, ref state); + WriteBufferHelper.RefreshBuffer(ref buffer, ref state); } // copy the remaining data