From f9d9019e272990eaeda68bb4cb079b6d15d191bd Mon Sep 17 00:00:00 2001 From: Jan Tattermusch Date: Thu, 28 May 2020 15:58:34 +0200 Subject: [PATCH] more progress --- .../src/Google.Protobuf/WriteBufferHelper.cs | 4 +- csharp/src/Google.Protobuf/WriteContext.cs | 100 ++++++++++++++++++ .../Google.Protobuf/WriterInternalState.cs | 4 + .../WritingPrimitivesMessages.cs | 55 ++++++++-- 4 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 csharp/src/Google.Protobuf/WriteContext.cs diff --git a/csharp/src/Google.Protobuf/WriteBufferHelper.cs b/csharp/src/Google.Protobuf/WriteBufferHelper.cs index 65da9612ca..4b9960ea26 100644 --- a/csharp/src/Google.Protobuf/WriteBufferHelper.cs +++ b/csharp/src/Google.Protobuf/WriteBufferHelper.cs @@ -47,6 +47,8 @@ namespace Google.Protobuf private IBufferWriter bufferWriter; private CodedOutputStream codedOutputStream; + public CodedOutputStream CodedOutputStream => CodedOutputStream; + /// /// Initialize an instance with a coded output stream. /// This approach is faster than using a constructor because the instance to initialize is passed by reference @@ -65,7 +67,7 @@ namespace Google.Protobuf /// and we can write directly into it without copying. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Initialize(IBufferWriter bufferWriter, out WriteBufferHelper instance, out ReadOnlySpan buffer) + public static void Initialize(IBufferWriter bufferWriter, out WriteBufferHelper instance, out Span buffer) { instance.bufferWriter = bufferWriter; instance.codedOutputStream = null; diff --git a/csharp/src/Google.Protobuf/WriteContext.cs b/csharp/src/Google.Protobuf/WriteContext.cs new file mode 100644 index 0000000000..3cd3c70858 --- /dev/null +++ b/csharp/src/Google.Protobuf/WriteContext.cs @@ -0,0 +1,100 @@ +#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.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using Google.Protobuf.Collections; + +namespace Google.Protobuf +{ + /// + /// An opaque struct that represents the current serialization state and is passed along + /// as the serialization proceeds. + /// All the public methods are intended to be invoked only by the generated code, + /// users should never invoke them directly. + /// + [SecuritySafeCritical] + public ref struct WriteContext + { + internal Span buffer; + internal WriterInternalState state; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Initialize(ref Span buffer, ref WriterInternalState state, out WriteContext ctx) + { + ctx.buffer = buffer; + ctx.state = state; + } + + /// + /// Creates a WriteContext instance from CodedOutputStream. + /// WARNING: internally this copies the CodedOutputStream's state, so after done with the WriteContext, + /// the CodedOutputStream's state needs to be updated. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Initialize(CodedOutputStream output, out WriteContext ctx) + { + ctx.buffer = new Span(output.InternalBuffer); + // ideally we would use a reference to the original state, but that doesn't seem possible + // so we just copy the struct that holds the state. We will need to later store the state back + // into CodedOutputStream if we want to keep it usable. + ctx.state = output.InternalState; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void Initialize(IBufferWriter output, out WriteContext ctx) + { + ctx.buffer = default; + ctx.state = default; + WriteBufferHelper.Initialize(output, out ctx.state.writeBufferHelper, out ctx.buffer); + ctx.state.limit = ctx.buffer.Length; + ctx.state.position = 0; + } + + internal void CopyStateTo(CodedOutputStream output) + { + output.InternalState = state; + } + + internal void LoadStateFrom(CodedOutputStream output) + { + state = output.InternalState; + } + } +} \ No newline at end of file diff --git a/csharp/src/Google.Protobuf/WriterInternalState.cs b/csharp/src/Google.Protobuf/WriterInternalState.cs index 521c891ffb..c9972b9aad 100644 --- a/csharp/src/Google.Protobuf/WriterInternalState.cs +++ b/csharp/src/Google.Protobuf/WriterInternalState.cs @@ -54,5 +54,9 @@ namespace Google.Protobuf internal int position; internal WriteBufferHelper writeBufferHelper; + + // If non-null, the top level parse method was started with given coded output stream as an argument + // which also means we can potentially fallback to calling WriteTo(CodedOutputStream cos) if needed. + internal CodedOutputStream CodedOutputStream => writeBufferHelper.CodedOutputStream; } } \ No newline at end of file diff --git a/csharp/src/Google.Protobuf/WritingPrimitivesMessages.cs b/csharp/src/Google.Protobuf/WritingPrimitivesMessages.cs index c75829c6f7..b0b57a9350 100644 --- a/csharp/src/Google.Protobuf/WritingPrimitivesMessages.cs +++ b/csharp/src/Google.Protobuf/WritingPrimitivesMessages.cs @@ -32,6 +32,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; using System.Security; namespace Google.Protobuf @@ -47,21 +48,61 @@ namespace Google.Protobuf /// The data is length-prefixed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteMessage(ref Span buffer, ref WriterInternalState state, IMessage value) + public static void WriteMessage(ref WriteContext ctx, IMessage value) { - WritingPrimitives.WriteLength(ref buffer, ref state, value.CalculateSize()); - // TODO: - //value.WriteTo(this); + WritingPrimitives.WriteLength(ref ctx.buffer, ref ctx.state, value.CalculateSize()); + WriteInternal(ref ctx, value); } /// /// Writes a group, without a tag, to the stream. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteGroup(ref Span buffer, ref WriterInternalState state, IMessage value) + public static void WriteGroup(ref WriteContext ctx, IMessage value) { - // TODO: - //value.WriteTo(this); + WriteInternal(ref ctx, value); + } + + private static void WriteInternal(ref WriteContext ctx, IMessage message) + { + if (message is IBufferMessage bufferMessage) + { + // TODO: actually invoke the method + //bufferMessage.InternalWriteTo(ref ctx); + } + else + { + // If we reached here, it means we've ran into a nested message with older generated code + // which doesn't provide the InternalWriteTo method that takes a WriteContext. + // With a slight performance overhead, we can still serialize this message just fine, + // but we need to find the original CodedOutputStream instance that initiated this + // serialization process and make sure its internal state is up to date. + // Note that this performance overhead is not very high (basically copying contents of a struct) + // and it will only be incurred in case the application mixes older and newer generated code. + // Regenerating the code from .proto files will remove this overhead because it will + // generate the InternalWriteTo method we need. + + if (ctx.state.CodedOutputStream == null) + { + // This can only happen when the serialization started without providing a CodedOutputStream instance + // (e.g. WriteContext was created directly from a IBufferWriter). + // That also means that one of the new parsing APIs was used at the top level + // and in such case it is reasonable to require that all the nested message provide + // up-to-date generated code with WriteContext support (and fail otherwise). + throw new InvalidProtocolBufferException($"Message {message.GetType().Name} doesn't provide the generated method that enables WriteContext-based serialization. You might need to regenerate the generated protobuf code."); + } + + ctx.CopyStateTo(ctx.state.CodedOutputStream); + try + { + // fallback parse using the CodedOutputStream that started current serialization tree + message.WriteTo(ctx.state.CodedOutputStream); + } + finally + { + ctx.LoadStateFrom(ctx.state.CodedOutputStream); + } + } } } } \ No newline at end of file