Search code examples
unity-game-engineprotobuf-netil2cpp

Dictionary<string, int> throwing exception when serializing with protobuf-net


I am trying to make protobuf-net work with Unity. From the lot of research I've done the last days, it seems to be complicated to get both to work fine together. I have right now a problem I can't get around, when building using IL2CPP. The serialization appears to sometimes fail for certain types, like Dictionary<string, int> or Dictionary<string, float> but works fine for Dictionary<string, string>. To make things clear, I'm using protobuf-net version 2.4.6 from nuget as it seems that v3 isn't supported at all from unity (unimplemented IL2CPP methods). The occurring error is:

ExecutionEngineException: Attempting to call method 'ProtoBuf.Serializers.MapDecorator`3[[System.Collections.Generic.Dictionary`2[[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]::.cctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.MonoCMethod.InternalInvoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoCMethod.DoInvoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MonoCMethod.Invoke (System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.ConstructorInfo.Invoke (System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.ValueMember.BuildSerializer () [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.ValueMember.get_Serializer () [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.MetaType.BuildSerializer () [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.MetaType.get_Serializer () [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.RuntimeTypeModel.Serialize (System.Int32 key, System.Object value, ProtoBuf.ProtoWriter dest) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.TypeModel.SerializeCore (ProtoBuf.ProtoWriter writer, System.Object value) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.TypeModel.Serialize (System.IO.Stream dest, System.Object value, ProtoBuf.SerializationContext context) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Meta.TypeModel.Serialize (System.IO.Stream dest, System.Object value) [0x00000] in <00000000000000000000000000000000>:0 
  at ProtoBuf.Serializer.Serialize[T] (System.IO.Stream destination, T instance) [0x00000] in <00000000000000000000000000000000>:0 
  at SerializationTests.SerializeObject[T] (T objectToSave) [0x00000] in <00000000000000000000000000000000>:0 
  at SerializationTests.AttemptSerializingData[T] (T dataToSerialize, Entry entry) [0x00000] in <00000000000000000000000000000000>:0 
  at Entry.OnGUI () [0x00000] in <00000000000000000000000000000000>:0 

The class I'm trying to serialize on a side project to test looks like this:

    using System.Collections.Generic;
    using ProtoBuf;
    
    [ProtoContract]
    public class DataDictionnary<T, U>
    {
        [ProtoMember(1)] private readonly Dictionary<T, U> someDictionnary = new Dictionary<T, U>();
    
        public DataDictionnary()
        {
    
        }
    
        public void AddValue(T key, U value)
        {
            if (someDictionnary.ContainsKey(key)) return;
    
            someDictionnary.Add(key, value);
        }
    
        public override string ToString()
        {
            string s = $"{GetType()}";
    
            foreach (var key in someDictionnary.Keys)
            {
                s += $" ({key}, {someDictionnary[key]})";
            }
    
            return s;
        }
    }

Is there a reason I can serialize only certain types of Dictionaries with primitives, anything I could use to make it work ?

Edit: my link.xml file supposed to prevent code stripping

<linker>

    <assembly fullname="Test.Serialization"  preserve="all"/>

    <assembly fullname="protobuf-net"  preserve="all"/>

</linker>

SerializationTests.cs

using System.IO;
using ProtoBuf;

public static class SerializationTests
{
    public static void AttemptSerializingData<T>(T dataToSerialize, bool displayProto, Entry entry)
    {
        if (displayProto)
            entry.AddLog(Serializer.GetProto<T>());

        entry.AddLog($"Serializing {dataToSerialize}");
        var serializedObject = SerializeObject(dataToSerialize);
        entry.AddLog($"Done ({serializedObject.Length})");
        var deserializedObject = DeserializeObject<T>(serializedObject);
        entry.AddLog($"Done! {deserializedObject}");
    }

    private static T DeserializeObject<T>(byte[] bytes)
    {
        T result;

        using (var stream = new MemoryStream(bytes))
        {
            result = Serializer.Deserialize<T>(stream);
        }

        return result;
    }

    private static byte[] SerializeObject<T>(T objectToSave)
    {
        byte[] convertedObject;

        using (var stream = new MemoryStream())
        {
            Serializer.Serialize(stream, objectToSave);
            convertedObject = stream.ToArray();
        }

        return convertedObject;
    }
}

The error is thrown when calling AttemptSerializingData from this GUI code:

using System.Collections.Generic;
using UnityEngine;

public class Entry : MonoBehaviour
{
    private readonly List<string> logs = new List<string>();

    private string a = "0", b = "5", c = "-2";
    private bool displayProto;


    private void OnGUI()
    {
        displayProto = GUILayout.Toggle(displayProto, "Display proto data");

        a = GUILayout.TextField(a);
        b = GUILayout.TextField(b);
        c = GUILayout.TextField(c);

        {
            if (GUILayout.Button("Test int") && int.TryParse(a, out int _a) && int.TryParse(b, out int _b) && int.TryParse(c, out int _c))
            {
                logs.Clear();
                var data = new DataDictionnary<string, int>();
                data.AddValue("plop", _a);
                data.AddValue("pop", _b);
                data.AddValue("plp", _c);
                SerializationTests.AttemptSerializingData(data, displayProto, this);
            }
        }

        if (GUILayout.Button("Test string"))
        {
            logs.Clear();
            var data = new DataDictionnary<string, string>();
            data.AddValue("plop", a);
            data.AddValue("pop", b);
            data.AddValue("plp", c);
            SerializationTests.AttemptSerializingData(data, displayProto, this);
        }

        {
            if (GUILayout.Button("Test float") && float.TryParse(a, out float _a) && float.TryParse(b, out float _b) && float.TryParse(c, out float _c))
            {
                logs.Clear();
                var data = new DataDictionnary<string, float>();
                data.AddValue("plop", _a);
                data.AddValue("pop", _b);
                data.AddValue("plp", _c);
                SerializationTests.AttemptSerializingData(data, displayProto, this);
            }

            if (GUILayout.Button("Test int int"))
            {
                logs.Clear();
                var data = new DataDictionnary<int, int>();
                data.AddValue(0, 5);
                data.AddValue(4, 41);
                data.AddValue(2, 8);
                SerializationTests.AttemptSerializingData(data, displayProto, this);
            }

            if (GUILayout.Button("Test bool string"))
            {
                logs.Clear();
                var data = new DataDictionnary<bool, string>();
                data.AddValue(true, "ouyhgv");
                data.AddValue(false, "hoipkip");
                data.AddValue(false, "fguoii");
                SerializationTests.AttemptSerializingData(data, displayProto, this);
            }
        }

        foreach (var log in logs)
        {
            GUILayout.Label(log);
        }
    }

    public void AddLog(string text)
    {
        logs.Add(text);
    }
}

Solution

  • I found a solution here. Declaring all of my Dictionnaries with the attribute solves the problem.

    [ProtoMap(DisableMap = true)]
    [ProtoMember(1)]
    private readonly Dictionary<string, string> _stringData;