Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Protobuf-net (de)serialization of decimals throws when using custom decimal proto contract (C#/C++ interop)

Say I want to serialize, then deserialize a decimal using protobuf-net:

const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
{
    Serializer.Serialize(memoryStream, originalDecimal);
    memoryStream.Position = 0;
    var deserializedDecimal = Serializer.Deserialize<decimal>(memoryStream);
    Assert.AreEqual(originalDecimal, deserializedDecimal);
}

It works fine. Protobuf-net internally uses the following representation for decimals (cf. Bcl.proto):

message Decimal {
  optional uint64 lo = 1; // the first 64 bits of the underlying value
  optional uint32 hi = 2; // the last 32 bis of the underlying value
  optional sint32 signScale = 3; // the number of decimal digits, and the sign
}

Now say that I define a supposedly equivalent proto contract by code:

[ProtoContract]
public class MyDecimal
{
    [ProtoMember(1, IsRequired = false)]
    public ulong Lo;

    [ProtoMember(2, IsRequired = false)]
    public uint Hi;

    [ProtoMember(3, IsRequired = false)]
    public int SignScale;
}

...then I can't serialize a decimal and get a MyDecimal back, nor serialize a MyDecimal and get a decimal back.

From decimal to MyDecimal:

const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
{
    Serializer.Serialize(memoryStream, originalDecimal);
    memoryStream.Position = 0;

    // following line throws a Invalid wire-type ProtoException
    Serializer.Deserialize<MyDecimal>(memoryStream);
}

From MyDecimal to decimal:

var myDecimal = new MyDecimal
{
    Lo = 0x003b1ee886632642,
    Hi = 0x00000000,
    SignScale = 0x00000020,
};

using (var memoryStream = new MemoryStream())
{
    Serializer.Serialize(memoryStream, myDecimal);
    memoryStream.Position = 0;

    // following line throws a Invalid wire-type ProtoException
    Serializer.Deserialize<decimal>(memoryStream);
}

Am I missing something here?

I'm working on a C++ application which needs to communicate with a C# one through protocol buffers and can't figure why decimal deserializations fail.

like image 232
Romain Verdier Avatar asked Oct 22 '22 10:10

Romain Verdier


1 Answers

This is an edge case of the "is it an object? or a naked value?". You can't just serialize an int, say, in protobuf - you need a wrapper object. For naked values, therefore, it pretends that the value is actually field 1 of a hypothetical wrapper object. In the case of decimal, though, this is a bit tricky - since decimal is actually encoded as though it were an object. So technically decimal could be written as a naked value... but: it looks like it isn't (it is wrapping it) - and I doubt it would be a good idea to rectify that at this stage.

Basically, this will work a lot more reliably if instead of serializing a naked value, you serialize an object that has a value. It will also work more efficiently (protobuf-net looks for types it knows about, with the naked values very much a fallback scenario). For example:

[ProtoContract]
class DecimalWrapper {
    [ProtoMember(1)]
    public decimal Value { get; set; }
}
[ProtoContract]
class MyDecimalWrapper {
    [ProtoMember(1)]
    public MyDecimal Value { get; set; }
}

If we serialize these, they are 100% interchangeable:

const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
{
    var obj = new DecimalWrapper { Value = originalDecimal };
    Serializer.Serialize(memoryStream, obj);
    // or, as it happens (see text) - this is equal to
    // Serializer.Serialize(memoryStream, originalDecimal);

    memoryStream.Position = 0;
    var obj2 = Serializer.Deserialize<MyDecimalWrapper>(memoryStream);
    Console.WriteLine("{0}, {1}, {2}",
        obj2.Value.Lo, obj2.Value.Hi, obj2.Value.SignScale);
    // ^^^ 16641007661819458, 0, 32

    memoryStream.SetLength(0);
    Serializer.Serialize(memoryStream, obj2);
    memoryStream.Position = 0;
    var obj3 = Serializer.Deserialize<DecimalWrapper>(memoryStream);

    bool eq = obj3.Value == obj.Value; // True
}

Actually, because protobuf-net pretends there is an object, it is also true to say that Serialize<decimal> would be 100% compatible with Serialize<MyDecimalWrapper>, but for your own sanity it is probably just easier to stick to a simple "always serialize a DTO instance" approach, rather than having to think "is this a DTO? or is it a naked value?"


As a final thought: if you are using interop, I would suggest avoiding decimal, since that is not defined in the protobuf specification, and different platforms often have a different meaning of their "decimal" type. protobuf-net invents a meaning, mainly to allow protobuf-net to round-trip (to itself) a wider range of DTOs, but it may be awkward to parse that value into an arbitrary platform. When working cross-platform and using decimal, I recommend considering things like double/float, or some fixed precision via long/ulong, or maybe even just string.

like image 142
Marc Gravell Avatar answered Nov 03 '22 05:11

Marc Gravell