From 8a0312b20156aa4df092cb6b3c665ef48cb6fa54 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Thu, 16 Jul 2015 17:03:06 +0100 Subject: First pass at wrapper types. - We do still generate the message types, as otherwise reflection breaks, even though it doesn't actually use those types. - JSON handling hasn't been implemented yet --- .../CodedOutputStream.ComputeSize.cs | 8 +- csharp/src/ProtocolBuffers/CodedOutputStream.cs | 4 +- csharp/src/ProtocolBuffers/Collections/MapField.cs | 34 ++++- .../ProtocolBuffers/Collections/RepeatedField.cs | 2 +- csharp/src/ProtocolBuffers/FieldCodec.cs | 149 ++++++++++++++++++--- 5 files changed, 166 insertions(+), 31 deletions(-) (limited to 'csharp/src/ProtocolBuffers') diff --git a/csharp/src/ProtocolBuffers/CodedOutputStream.ComputeSize.cs b/csharp/src/ProtocolBuffers/CodedOutputStream.ComputeSize.cs index a3e7c1bb..bf221c9c 100644 --- a/csharp/src/ProtocolBuffers/CodedOutputStream.ComputeSize.cs +++ b/csharp/src/ProtocolBuffers/CodedOutputStream.ComputeSize.cs @@ -129,8 +129,7 @@ namespace Google.Protobuf public static int ComputeStringSize(String value) { int byteArraySize = Utf8Encoding.GetByteCount(value); - return ComputeRawVarint32Size((uint) byteArraySize) + - byteArraySize; + return ComputeLengthSize(byteArraySize) + byteArraySize; } /// @@ -149,7 +148,7 @@ namespace Google.Protobuf public static int ComputeMessageSize(IMessage value) { int size = value.CalculateSize(); - return ComputeRawVarint32Size((uint) size) + size; + return ComputeLengthSize(size) + size; } /// @@ -158,8 +157,7 @@ namespace Google.Protobuf /// public static int ComputeBytesSize(ByteString value) { - return ComputeRawVarint32Size((uint) value.Length) + - value.Length; + return ComputeLengthSize(value.Length) + value.Length; } /// diff --git a/csharp/src/ProtocolBuffers/CodedOutputStream.cs b/csharp/src/ProtocolBuffers/CodedOutputStream.cs index 53f04c77..b91d6d70 100644 --- a/csharp/src/ProtocolBuffers/CodedOutputStream.cs +++ b/csharp/src/ProtocolBuffers/CodedOutputStream.cs @@ -274,7 +274,7 @@ namespace Google.Protobuf /// The value to write public void WriteMessage(IMessage value) { - WriteRawVarint32((uint) value.CalculateSize()); + WriteLength(value.CalculateSize()); value.WriteTo(this); } @@ -285,7 +285,7 @@ namespace Google.Protobuf /// The value to write public void WriteBytes(ByteString value) { - WriteRawVarint32((uint) value.Length); + WriteLength(value.Length); value.WriteRawBytesTo(this); } diff --git a/csharp/src/ProtocolBuffers/Collections/MapField.cs b/csharp/src/ProtocolBuffers/Collections/MapField.cs index 0f379eaa..ead45737 100644 --- a/csharp/src/ProtocolBuffers/Collections/MapField.cs +++ b/csharp/src/ProtocolBuffers/Collections/MapField.cs @@ -51,14 +51,38 @@ namespace Google.Protobuf.Collections public sealed class MapField : IDeepCloneable>, IFreezable, IDictionary, IEquatable>, IDictionary { // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.) + private bool allowNullValues; private bool frozen; private readonly Dictionary>> map = new Dictionary>>(); private readonly LinkedList> list = new LinkedList>(); + /// + /// Constructs a new map field, defaulting the value nullability to only allow null values for message types + /// and non-nullable value types. + /// + public MapField() : this(typeof(IMessage).IsAssignableFrom(typeof(TValue)) || Nullable.GetUnderlyingType(typeof(TValue)) != null) + { + } + + /// + /// Constructs a new map field, overriding the choice of whether null values are permitted in the map. + /// This is used by wrapper types, where maps with string and bytes wrappers as the value types + /// support null values. + /// + /// Whether null values are permitted in the map or not. + public MapField(bool allowNullValues) + { + if (allowNullValues && typeof(TValue).IsValueType && Nullable.GetUnderlyingType(typeof(TValue)) == null) + { + throw new ArgumentException("allowNullValues", "Non-nullable value types do not support null values"); + } + this.allowNullValues = allowNullValues; + } + public MapField Clone() { - var clone = new MapField(); + var clone = new MapField(allowNullValues); // Keys are never cloneable. Values might be. if (typeof(IDeepCloneable).IsAssignableFrom(typeof(TValue))) { @@ -138,7 +162,8 @@ namespace Google.Protobuf.Collections set { ThrowHelper.ThrowIfNull(key, "key"); - if (value == null && (typeof(TValue) == typeof(ByteString) || typeof(TValue) == typeof(string))) + // value == null check here is redundant, but avoids boxing. + if (value == null && !allowNullValues) { ThrowHelper.ThrowIfNull(value, "value"); } @@ -225,6 +250,11 @@ namespace Google.Protobuf.Collections } } + /// + /// Returns whether or not this map allows values to be null. + /// + public bool AllowsNullValues { get { return allowNullValues; } } + public int Count { get { return list.Count; } } public bool IsReadOnly { get { return frozen; } } diff --git a/csharp/src/ProtocolBuffers/Collections/RepeatedField.cs b/csharp/src/ProtocolBuffers/Collections/RepeatedField.cs index 8375ae0b..9bab41ea 100644 --- a/csharp/src/ProtocolBuffers/Collections/RepeatedField.cs +++ b/csharp/src/ProtocolBuffers/Collections/RepeatedField.cs @@ -123,7 +123,7 @@ namespace Google.Protobuf.Collections { int dataSize = CalculatePackedDataSize(codec); return CodedOutputStream.ComputeRawVarint32Size(tag) + - CodedOutputStream.ComputeRawVarint32Size((uint)dataSize) + + CodedOutputStream.ComputeLengthSize(dataSize) + dataSize; } else diff --git a/csharp/src/ProtocolBuffers/FieldCodec.cs b/csharp/src/ProtocolBuffers/FieldCodec.cs index c72a3e7b..caf03286 100644 --- a/csharp/src/ProtocolBuffers/FieldCodec.cs +++ b/csharp/src/ProtocolBuffers/FieldCodec.cs @@ -29,7 +29,7 @@ // (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.Collections.Generic; @@ -43,7 +43,7 @@ namespace Google.Protobuf // TODO: Avoid the "dual hit" of lambda expressions: create open delegates instead. (At least test...) public static FieldCodec ForString(uint tag) { - return new FieldCodec(input => input.ReadString(), (output, value) => output.WriteString(value), CodedOutputStream.ComputeStringSize, tag); + return new FieldCodec(input => input.ReadString(), (output, value) => output.WriteString(value), CodedOutputStream.ComputeStringSize, tag); } public static FieldCodec ForBytes(uint tag) @@ -131,6 +131,106 @@ namespace Google.Protobuf return new FieldCodec(input => { T message = parser.CreateTemplate(); input.ReadMessage(message); return message; }, (output, value) => output.WriteMessage(value), message => CodedOutputStream.ComputeMessageSize(message), tag); } + + /// + /// Creates a codec for a wrapper type of a class - which must be string or ByteString. + /// + public static FieldCodec ForClassWrapper(uint tag) where T : class + { + var nestedCodec = WrapperCodecs.GetCodec(); + return new FieldCodec( + input => WrapperCodecs.Read(input, nestedCodec), + (output, value) => WrapperCodecs.Write(output, value, nestedCodec), + value => WrapperCodecs.CalculateSize(value, nestedCodec), + tag, + null); // Default value for the wrapper + } + + /// + /// Creates a codec for a wrapper type of a struct - which must be Int32, Int64, UInt32, UInt64, + /// Bool, Single or Double. + /// + public static FieldCodec ForStructWrapper(uint tag) where T : struct + { + var nestedCodec = WrapperCodecs.GetCodec(); + return new FieldCodec( + input => WrapperCodecs.Read(input, nestedCodec), + (output, value) => WrapperCodecs.Write(output, value.Value, nestedCodec), + value => value == null ? 0 : WrapperCodecs.CalculateSize(value.Value, nestedCodec), + tag, + null); // Default value for the wrapper + } + + // Helper code to create codecs for wrapper types. Somewhat ugly with all the + private static class WrapperCodecs + { + private static readonly Dictionary Codecs = new Dictionary + { + { typeof(bool), ForBool(WireFormat.MakeTag(1, WireFormat.WireType.Varint)) }, + { typeof(int), ForInt32(WireFormat.MakeTag(1, WireFormat.WireType.Varint)) }, + { typeof(long), ForInt64(WireFormat.MakeTag(1, WireFormat.WireType.Varint)) }, + { typeof(uint), ForUInt32(WireFormat.MakeTag(1, WireFormat.WireType.Varint)) }, + { typeof(ulong), ForUInt64(WireFormat.MakeTag(1, WireFormat.WireType.Varint)) }, + { typeof(float), ForFloat(WireFormat.MakeTag(1, WireFormat.WireType.Fixed32)) }, + { typeof(double), ForDouble(WireFormat.MakeTag(1, WireFormat.WireType.Fixed64)) }, + { typeof(string), ForString(WireFormat.MakeTag(1, WireFormat.WireType.LengthDelimited)) }, + { typeof(ByteString), ForBytes(WireFormat.MakeTag(1, WireFormat.WireType.LengthDelimited)) } + }; + + /// + /// Returns a field codec which effectively wraps a value of type T in a message. + /// + /// + internal static FieldCodec GetCodec() + { + object value; + if (!Codecs.TryGetValue(typeof(T), out value)) + { + throw new InvalidOperationException("Invalid type argument requested for wrapper codec: " + typeof(T)); + } + return (FieldCodec) value; + } + + internal static T Read(CodedInputStream input, FieldCodec codec) + { + int length = input.ReadLength(); + int oldLimit = input.PushLimit(length); + + uint tag; + T value = codec.DefaultValue; + while (input.ReadTag(out tag)) + { + if (tag == 0) + { + throw InvalidProtocolBufferException.InvalidTag(); + } + if (tag == codec.Tag) + { + value = codec.Read(input); + } + if (WireFormat.IsEndGroupTag(tag)) + { + break; + } + } + input.CheckLastTagWas(0); + input.PopLimit(oldLimit); + + return value; + } + + internal static void Write(CodedOutputStream output, T value, FieldCodec codec) + { + output.WriteLength(codec.CalculateSizeWithTag(value)); + codec.WriteTagAndValue(output, value); + } + + internal static int CalculateSize(T value, FieldCodec codec) + { + int fieldLength = codec.CalculateSizeWithTag(value); + return CodedOutputStream.ComputeLengthSize(fieldLength) + fieldLength; + } + } } /// @@ -144,31 +244,19 @@ namespace Google.Protobuf /// public sealed class FieldCodec { - private static readonly Func IsDefault; - private static readonly T Default; + private static readonly T DefaultDefault; static FieldCodec() { if (typeof(T) == typeof(string)) { - Default = (T)(object)""; - IsDefault = CreateDefaultValueCheck(x => x.Length == 0); + DefaultDefault = (T)(object)""; } else if (typeof(T) == typeof(ByteString)) { - Default = (T)(object)ByteString.Empty; - IsDefault = CreateDefaultValueCheck(x => x.Length == 0); - } - else if (!typeof(T).IsValueType) - { - // Default default - IsDefault = CreateDefaultValueCheck(x => x == null); - } - else - { - // Default default - IsDefault = CreateDefaultValueCheck(x => EqualityComparer.Default.Equals(x, default(T))); + DefaultDefault = (T)(object)ByteString.Empty; } + // Otherwise it's the default value of the CLR type } private static Func CreateDefaultValueCheck(Func check) @@ -182,18 +270,32 @@ namespace Google.Protobuf private readonly uint tag; private readonly int tagSize; private readonly int fixedSize; + // Default value for this codec. Usually the same for every instance of the same type, but + // for string/ByteString wrapper fields the codec's default value is null, whereas for + // other string/ByteString fields it's "" or ByteString.Empty. + private readonly T defaultValue; internal FieldCodec( Func reader, Action writer, Func sizeCalculator, - uint tag) + uint tag) : this(reader, writer, sizeCalculator, tag, DefaultDefault) + { + } + + internal FieldCodec( + Func reader, + Action writer, + Func sizeCalculator, + uint tag, + T defaultValue) { this.reader = reader; this.writer = writer; this.sizeCalculator = sizeCalculator; this.fixedSize = 0; this.tag = tag; + this.defaultValue = defaultValue; tagSize = CodedOutputStream.ComputeRawVarint32Size(tag); } @@ -234,7 +336,7 @@ namespace Google.Protobuf public uint Tag { get { return tag; } } - public T DefaultValue { get { return Default; } } + public T DefaultValue { get { return defaultValue; } } /// /// Write a tag and the given value, *if* the value is not the default. @@ -260,6 +362,11 @@ namespace Google.Protobuf public int CalculateSizeWithTag(T value) { return IsDefault(value) ? 0 : sizeCalculator(value) + tagSize; - } + } + + private bool IsDefault(T value) + { + return EqualityComparer.Default.Equals(value, defaultValue); + } } } -- cgit v1.2.3