Protobuf-net Generics on Unity3D IL2CPP.

When using Protobuf-net in Unity3D, if your Proto-Contracts include generics you might run into a few issues like Unable to resolve MapDecorator constructor.

I had to work with the following contract. It was s custom collection class which had a functionality of caching removed elements in the collection.

[ProtoContract]
public class CustomCollectionBase<TCollection, TElement, TKey>
{
	[ProtoMember(1)]
	protected TCollection _collection;

	[ProtoMember(2)]
	protected SortedDictionary<TKey, bool> _removed;

	public CustomCollectionBase(TCollection collection)
	{
		_collection = collection;
	}
}

[ProtoContract]
public class CustomList<TElement> : CustomCollectionBase<List<TElement>, TElement, int>
{
	public CustomList() : base( new List<TElement>())
	{
	}
}

[ProtoContract]
public class CustomIntDictionary<TElement> : CustomCollectionBase<Dictionary<int, TElement>, TElement, int>
{
	public CustomIntDictionary() : base(new Dictionary<int, TElement>())
	{
	}
}

[ProtoContract]
public class CustomStringDictionary<TElement> : CustomCollectionBase<Dictionary<string, TElement>, TElement, string>
{
	public CustomStringDictionary() : base(new Dictionary<string, TElement>())
	{
	}
}

With this type of contract, I had register all the run-time types using RuntimeTypeModel.

Doing this caused protobuf-net to not work on IL2CPP build as it had to use Relfection.Emit to create run-time serializers and deserializers.

To overcome this issue, the contract was updated in the following way.

public class CustomCollectionBase<TCollection, TElement, TKey>
{
	protected TCollection _collection;

	protected SortedDictionary<TKey, bool> _removed;

	public CustomCollectionBase(TCollection collection)
	{
		_collection = collection;
	}

	protected byte[] _cBytes;

	protected byte[] _rBytes;

	protected void __PreSerialize()
	{
		using (MemoryStream stream = new MemoryStream())
		{
			Serializer.Serialize(stream, this._collection);
			this._cBytes = stream.ToArray();
		}

		using (MemoryStream stream = new MemoryStream())
		{
			Serializer.Serialize(stream, this._removed);
			this._rBytes = stream.ToArray();
		}
	}

	protected void __PostDeserialize()
	{
		using (MemoryStream stream = new MemoryStream(this._cBytes))
		{
			this._collection = Serializer.Deserialize<TCollection>(stream);
		}

		using (MemoryStream stream = new MemoryStream(this._rBytes))
		{
			this._removed = Serializer.Deserialize<SortedDictionary<TKey, bool>>(stream);
		}
	}
}

[ProtoContract]
public class CustomList<TElement> : CustomCollectionBase<List<TElement>, TElement, int>
{
	public CustomList() : base( new List<TElement>())
	{
	}

	[ProtoMember(1)]
	private byte[] _cProto
	{
		get { return base._cBytes; }
		set { base._cBytes = value; }
	}

	[ProtoMember(2)]
	private byte[] _rProto
	{
		get { return base._rBytes; }
		set { base._rBytes = value; }
	}

	[ProtoBeforeSerialization]
	private void __PreSerialize()
	{
		base.__PreSerialize();
	}

	[ProtoAfterDeserialization]
	private void __PostDeserialize()
	{
		base.__PostDeserialize();
	}
}

[ProtoContract]
public class CustomIntDictionary<TElement> : CustomCollectionBase<Dictionary<int, TElement>, TElement, int>
{
	public CustomIntDictionary() : base(new Dictionary<int, TElement>())
	{
	}

	[ProtoMember(1)]
	private byte[] _cProto
	{
		get { return base._cBytes; }
		set { base._cBytes = value; }
	}

	[ProtoMember(2)]
	private byte[] _rProto
	{
		get { return base._rBytes; }
		set { base._rBytes = value; }
	}

	[ProtoBeforeSerialization]
	private void __PreSerialize()
	{
		base.__PreSerialize();
	}

	[ProtoAfterDeserialization]
	private void __PostDeserialize()
	{
		base.__PostDeserialize();
	}
}

[ProtoContract]
public class CustomStringDictionary<TElement> : CustomCollectionBase<Dictionary<string, TElement>, TElement, string>
{
	public CustomStringDictionary() : base(new Dictionary<string, TElement>())
	{
	}

	[ProtoMember(1)]
	private byte[] _cProto
	{
		get { return base._cBytes; }
		set { base._cBytes = value; }
	}

	[ProtoMember(2)]
	private byte[] _rProto
	{
		get { return base._rBytes; }
		set { base._rBytes = value; }
	}

	[ProtoBeforeSerialization]
	private void __PreSerialize()
	{
		base.__PreSerialize();
	}

	[ProtoAfterDeserialization]
	private void __PostDeserialize()
	{
		base.__PostDeserialize();
	}
}

So basically there are two methods that are defined.

  1. __PreSerialize – Converts the collection in to a byte array which becomes the proto member.
  2. __PostDeserialize – Converts the byte array back to the collection.

We can completely avoid defining run time types for this generic type. Instead of protobuf-net being responsible of creating the type, we create the type by ourselves using the same protobuf Serializer.

This kind of technique can be used for other generic types as well. Please comment your ideas about this approach.

Cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.