Fast-forward three years. I’m not sure if this is now supported but I was able to serialize and deserialize a NativeList without having to write a concrete Adapter for every T. Though I had to write two separate adapters for NativeList and UnsafeList but was able to condense them to remove code duplication as much as possible (see spoilers at bottom).
This works for me:
public class NativeListMax65kOfT<T> : IBinaryAdapter<NativeList<T>> where T : unmanaged
{
public unsafe void Serialize(in BinarySerializationContext<NativeList<T>> context, NativeList<T> value)
{
var itemCount = value.Length;
if (itemCount > UInt16.MaxValue)
throw new ArgumentOutOfRangeException($"List too long: max. {UInt16.MaxValue} length allowed");
var writer = context.Writer;
writer->Add((UInt16)itemCount);
for (var i = 0; i < itemCount; i++)
context.SerializeValue(value[i]);
}
public unsafe NativeList<T> Deserialize(in BinaryDeserializationContext<NativeList<T>> context)
{
var reader = context.Reader;
var itemCount = (Int32)reader->ReadNext<UInt16>();
var value = new NativeList<T>(itemCount, Allocator.Temp);
for (var i = 0; i < itemCount; i++)
value.Add(context.DeserializeValue<T>());
return value;
}
}
Notes:
- list length is limited to 65k (UInt16) to conserve some memory
- list Allocator.Temp is used so I don’t need to worry about disposing for unit tests
The unit test code for this:
[Test] public void CanSerializeAndDeserializeNativeListOfLinearTileDataStruct()
{
var linearData = new LinearTileData(2, TileFlags.DirectionSouth);
var list = new NativeList<LinearTileData>(1, Allocator.Temp);
list.Add(linearData);
var adapters = new List<IBinaryAdapter> { new BinaryAdapters.NativeListMax65kOfT<LinearTileData>() };
var bytes = BinarySerializer.Serialize(list, adapters);
var deserialList = BinarySerializer.Deserialize<NativeList<LinearTileData>>(bytes, adapters);
Debug.Log($"{bytes.Length} Bytes: {bytes.AsString()}");
Assert.That(deserialList.Length, Is.EqualTo(1));
Assert.That(deserialList[0], Is.EqualTo(linearData));
}
For completeness sake the LinearTileData struct
[StructLayout(LayoutKind.Explicit)]
public struct LinearTileData : ILinearTileData, IEquatable<LinearTileData>
{
[FieldOffset(0)] [CreateProperty] private UInt32 m_TileIndexFlags;
[FieldOffset(0)] private UInt16 m_TileIndex;
[FieldOffset(2)] private TileFlags m_TileFlags;
public UInt16 TileIndex { get => m_TileIndex; set => m_TileIndex = value; }
public TileFlags TileFlags { get => m_TileFlags; set => m_TileFlags = value; }
public UInt32 TileIndexFlags { get => m_TileIndexFlags; set => m_TileIndexFlags = value; }
public LinearTileData(UInt32 tileIndexFlags)
{
m_TileIndex = 0;
m_TileFlags = 0;
m_TileIndexFlags = tileIndexFlags;
}
public LinearTileData(UInt16 tileIndex, TileFlags tileFlags)
{
m_TileIndexFlags = 0;
m_TileIndex = tileIndex;
m_TileFlags = tileFlags;
}
public Boolean Equals(LinearTileData other) => m_TileIndexFlags == other.m_TileIndexFlags;
public override Boolean Equals(Object obj) => obj is LinearTileData other && Equals(other);
public override Int32 GetHashCode() => (Int32)m_TileIndexFlags;
public static Boolean operator ==(LinearTileData left, LinearTileData right) => left.Equals(right);
public static Boolean operator !=(LinearTileData left, LinearTileData right) => !left.Equals(right);
}
And the serialized buffer is 6 bytes: “102040” (length == 1, tile index == 2, flags == 4)
EDIT
You can extend that to NativeList<UnsafeList> …
You need to have two adapters, one for NativeList and another for UnsafeList. I did that and refactored the common parts into a static helper class.
Here’s the final solution (some names were refactored so it won’t work out of the box with the code above) as a gist with tests and adapters.
Output of the NativeList<UnsafeList> test code (brackets mine):
14 Bytes: 20<10<2040>><10<3080>>
2 items in the enclosing NativeList
1 item in each UnsafeList
values 2 + 4 for linearData1
values 3 + 8 for linearData2