Search Unity

Question How to (de)serialize an object containing NativeArrays?

Discussion in 'Entity Component System' started by Cell-i-Zenit, Jul 18, 2020.

  1. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    290
    Hi,

    I tried to use code like this to convert an object from and to byte[]

    Code (CSharp):
    1.  private byte[] ObjectToByteArray<T>(T obj)
    2.         {
    3.             var mStream = new MemoryStream();
    4.             _binaryFormatter.Serialize(mStream, obj);
    5.  
    6.             return mStream.ToArray();
    7.         }
    8.  
    9.         private T ByteArrayToObject<T>(byte[] arr)
    10.         {
    11.             var mStream = new MemoryStream(arr);
    12.             return (T) _binaryFormatter.Deserialize(mStream);
    13.         }
    But my object contains NativeArray which is not marked as serializable :(

    how would you do this? I need to store a class in a db and i thought that storing as blob would be the easiest.
     
  2. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    290
    Push here..

    I saw that we have a Serialization Package for Dots, but inside the Readme there is a "more documentation is coming soon" under the Binary part .. this is exactly what i was looking for :(

    Can anyone help me?
     
  3. silantzis

    silantzis

    Unity Technologies

    Joined:
    Sep 19, 2017
    Posts:
    13
    Hi,

    We are still working on the high level API for the binary serialization of the package which is why the documentation is missing.

    While the package does not explicitly support NativeArray types yet (this is something we plan to add moving forward). You should be able to work around by using adapters. The binary ones have the same API as the JSON version.

    Unfortunately there is support for generic type adapters yet. So you will need to have one explicitly for the types you need.

    Here is an example for an int array:

    Code (CSharp):
    1. class NativeArrayAdapter : IBinaryAdapter<NativeArray<int>>
    2. {
    3.     public unsafe void Serialize(UnsafeAppendBuffer* writer, NativeArray<int> value)
    4.     {
    5.         writer->Add(value.Length);
    6.  
    7.         for (var i = 0; i < value.Length; i++)
    8.             writer->Add(value[i]);
    9.     }
    10.  
    11.     public unsafe NativeArray<int> Deserialize(UnsafeAppendBuffer.Reader* reader)
    12.     {
    13.         var length = reader->ReadNext<int>();
    14.  
    15.         var value = new NativeArray<int>(length, Allocator.Persistent);
    16.  
    17.         for (var i = 0; i < length; i++)
    18.             value[i] = reader->ReadNext<int>();
    19.  
    20.         return value;
    21.     }
    22. }
    23.  
    24. byte[] ObjectToByteArray<T>(T obj)
    25. {
    26.     using (var stream = new UnsafeAppendBuffer(16, 8, Allocator.Temp))
    27.     {
    28.         unsafe
    29.         {
    30.             BinarySerialization.ToBinary(&stream, obj, new BinarySerializationParameters
    31.             {
    32.                 UserDefinedAdapters = new List<IBinaryAdapter>
    33.                 {
    34.                     new NativeArrayAdapter()
    35.                 }
    36.             });
    37.         }
    38.  
    39.         return stream.ToBytes();
    40.     }
    41. }
    42.  
    43. T ByteArrayToObject<T>(byte[] arr)
    44. {
    45.     unsafe
    46.     {
    47.         fixed (byte* ptr = arr)
    48.         {
    49.             var reader = new UnsafeAppendBuffer.Reader(ptr, arr.Length);
    50.          
    51.             return BinarySerialization.FromBinary<T>(&reader, new BinarySerializationParameters
    52.             {
    53.                 UserDefinedAdapters = new List<IBinaryAdapter>
    54.                 {
    55.                     new NativeArrayAdapter()
    56.                 }
    57.             });
    58.         }
    59.     }
    60. }
     
  4. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    290
    thanks for the answer, i will look into it. No problem with workarounds currently if the code isnt ready
     
  5. Cell-i-Zenit

    Cell-i-Zenit

    Joined:
    Mar 11, 2016
    Posts:
    290
    So i finally tried it out and it is suprisingly easy to do with the code snippet and a little debugging skills.

    The only question i have now is how i would handle "Memory Leaks".

    Example code is below. So i have these native arrays stored inside a class and i need them to do some lookups. I am tracking and cleaning them up by myself, but unity thinks i "forgot" them and is throwing lots of errors. how should i do it?

    A Native Collection has not been disposed, resulting in a memory leak. Allocated from:
    Unity.Collections.NativeArray`1:.ctor(Int32, Allocator, NativeArrayOptions)
    Assets.Modules.Data.Classes.Adapters.NativeArrayTranslationAdapter:Deserialize(Reader*) (at Assets\Modules\Data\Classes\Adapters\NativeArrayTranslationAdapter.cs:22)

    Code (CSharp):
    1. public unsafe NativeArray<Translation> Deserialize(UnsafeAppendBuffer.Reader* reader)
    2.         {
    3.             var length = reader->ReadNext<int>();
    4.  
    5.             var value = new NativeArray<Translation>(length, Allocator.Persistent); //this is line 22
    6.  
    7.             for (var i = 0; i < length; i++) value[i] = new Translation() {Value = reader->ReadNext<float3>()};
    8.  
    9.             return value;
    10.         }
     
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,975
    Fast-forward three years. I'm not sure if this is now supported but I was able to serialize and deserialize a NativeList<T> without having to write a concrete Adapter for every T. Though I had to write two separate adapters for NativeList<T> and UnsafeList<T> but was able to condense them to remove code duplication as much as possible (see spoilers at bottom).

    This works for me:
    Code (CSharp):
    1. public class NativeListMax65kOfT<T> : IBinaryAdapter<NativeList<T>> where T : unmanaged
    2. {
    3.     public unsafe void Serialize(in BinarySerializationContext<NativeList<T>> context, NativeList<T> value)
    4.     {
    5.         var itemCount = value.Length;
    6.         if (itemCount > UInt16.MaxValue)
    7.             throw new ArgumentOutOfRangeException($"List too long: max. {UInt16.MaxValue} length allowed");
    8.  
    9.         var writer = context.Writer;
    10.         writer->Add((UInt16)itemCount);
    11.  
    12.         for (var i = 0; i < itemCount; i++)
    13.             context.SerializeValue(value[i]);
    14.     }
    15.  
    16.     public unsafe NativeList<T> Deserialize(in BinaryDeserializationContext<NativeList<T>> context)
    17.     {
    18.         var reader = context.Reader;
    19.         var itemCount = (Int32)reader->ReadNext<UInt16>();
    20.         var value = new NativeList<T>(itemCount, Allocator.Temp);
    21.  
    22.         for (var i = 0; i < itemCount; i++)
    23.             value.Add(context.DeserializeValue<T>());
    24.  
    25.         return value;
    26.     }
    27. }
    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:
    Code (CSharp):
    1. [Test] public void CanSerializeAndDeserializeNativeListOfLinearTileDataStruct()
    2. {
    3.     var linearData = new LinearTileData(2, TileFlags.DirectionSouth);
    4.     var list = new NativeList<LinearTileData>(1, Allocator.Temp);
    5.     list.Add(linearData);
    6.  
    7.     var adapters = new List<IBinaryAdapter> { new BinaryAdapters.NativeListMax65kOfT<LinearTileData>() };
    8.     var bytes = BinarySerializer.Serialize(list, adapters);
    9.     var deserialList = BinarySerializer.Deserialize<NativeList<LinearTileData>>(bytes, adapters);
    10.  
    11.     Debug.Log($"{bytes.Length} Bytes: {bytes.AsString()}");
    12.     Assert.That(deserialList.Length, Is.EqualTo(1));
    13.     Assert.That(deserialList[0], Is.EqualTo(linearData));
    14. }
    Code (CSharp):
    1.     [StructLayout(LayoutKind.Explicit)]
    2.     public struct LinearTileData : ILinearTileData, IEquatable<LinearTileData>
    3.     {
    4.         [FieldOffset(0)] [CreateProperty] private UInt32 m_TileIndexFlags;
    5.         [FieldOffset(0)] private UInt16 m_TileIndex;
    6.         [FieldOffset(2)] private TileFlags m_TileFlags;
    7.  
    8.         public UInt16 TileIndex { get => m_TileIndex; set => m_TileIndex = value; }
    9.         public TileFlags TileFlags { get => m_TileFlags; set => m_TileFlags = value; }
    10.         public UInt32 TileIndexFlags { get => m_TileIndexFlags; set => m_TileIndexFlags = value; }
    11.  
    12.         public LinearTileData(UInt32 tileIndexFlags)
    13.         {
    14.             m_TileIndex = 0;
    15.             m_TileFlags = 0;
    16.             m_TileIndexFlags = tileIndexFlags;
    17.         }
    18.  
    19.         public LinearTileData(UInt16 tileIndex, TileFlags tileFlags)
    20.         {
    21.             m_TileIndexFlags = 0;
    22.             m_TileIndex = tileIndex;
    23.             m_TileFlags = tileFlags;
    24.         }
    25.  
    26.         public Boolean Equals(LinearTileData other) => m_TileIndexFlags == other.m_TileIndexFlags;
    27.         public override Boolean Equals(Object obj) => obj is LinearTileData other && Equals(other);
    28.         public override Int32 GetHashCode() => (Int32)m_TileIndexFlags;
    29.         public static Boolean operator ==(LinearTileData left, LinearTileData right) => left.Equals(right);
    30.         public static Boolean operator !=(LinearTileData left, LinearTileData right) => !left.Equals(right);
    31.     }

    And the serialized buffer is 6 bytes: "102040" (length == 1, tile index == 2, flags == 4)

    EDIT
    You can extend that to NativeList<UnsafeList<T>> ...

    You need to have two adapters, one for NativeList<T> and another for UnsafeList<T>. 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<T>> 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
     
    Last edited: Jun 5, 2023
    _geo__ likes this.
  7. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,975
    I'm now trying to do the same list serialization/deserialization for Json ... oh boy, Json serialization is FAR HARDER than I expected! Binary is trivial compared to Json ... understandably though, because binary has no structure besides the code, so the pay-off (or: payback) is in adapting code changes through versioning and fallback deserializers.

    After three days of trying to figure out how the Json API expects me to work with it, I could figure out a working solution but I'm not satisfied with it because it won't work with generics:
    Code (CSharp):
    1. public NativeList<T> Deserialize(in JsonDeserializationContext<NativeList<T>> context)
    2. {
    3.     var itemCount = context.SerializedValue["Length"].AsInt32();
    4.     var list = CreateResizedNativeList(itemCount, m_Allocator);
    5.     var array = context.SerializedValue["Items"].AsArrayView().ToArray();
    6.     var genericType = typeof(T);
    7.     switch (genericType)
    8.     {
    9.         case Type _ when genericType == typeof(Int32):
    10.             for (var i = 0; i < itemCount; i++)
    11.                 list[i] = (T)(Object)array[i].AsInt32();
    12.             break;
    13.         case Type _ when genericType == typeof(Int64):
    14.             for (var i = 0; i < itemCount; i++)
    15.                 list[i] = (T)(Object)array[i].AsInt64();
    16.             break;
    17.         default:
    18.             throw new ArgumentException($"unhandled generic type: {typeof(T).Name}");
    19.     }
    20.     return list;
    21. }
    I actually have to switch over the generic type and cast it back to (T) by going through (object) boxing. Is that the only solution, besides implementing concrete type adapters?
    (eg NativeListInt32Adapter + NativeListInt64Adapter instead of NativeListAdapter<T>)

    I get the feeling that this is what was actually meant by "not supporting generic type adapters yet". The generic T adapters work for binary, but not for Json. :(
     
    mopthrow likes this.
  8. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,341
    Is there any update on this yet?