Nino Serialization

website·Indexed 29 days ago

https://v2.nino.xgamedev.net

A high-performance serialization and deserialization library designed to be faster than JSON, Protobuf, MessagePack, and MemoryPack. The documentation covers installation, basic and advanced usage, serializable types, and performance microbenchmarks.

Tokens
4K
Snippets
15
Records
21
Agent score
50%

What's inside Nino

  1. Deserialize data using Nino

    The Deserializer.Deserialize method converts a ReadOnlySpan<byte> back into a NinoSerializableType. The method accepts various memory types, including byte[], ArraySegment<byte>, Span<byte>, or Memory<byte>.Span.
    // Assuming byteArr is byte[]
    Deserializer.Deserialize(byteArr, out ObjClass obj);
  2. Serialize data using Nino

    Nino provides a Serializer class to convert NinoSerializableType objects into byte arrays. You can either use the static Serializer.Serialize method or the Serialize() extension method on the object itself.
    ObjClass obj = new ObjClass();
    // Using static Serializer
    byte[] byteArr = Serializer.Serialize(obj);
    
    // Using extension method
    byteArr = obj.Serialize();
  3. Use Nino for Hot Update code with HybridCLR or ILRuntime

    Nino supports HybridCLR and ILRuntime. To write hot update code in an external C# project (outside of a Unity asmdef), create a .NET Core project, install the Nino library via NuGet, and then use the resulting compiled DLL within HybridCLR or ILRuntime.
  4. Install Nino in a .NET Project

    Nino supports projects using .NET 5.0+ or .NET Standard 2.0+. It requires two specific packages: Nino.Generator and Nino.Serialization. These are automatically installed when you install the main Nino package. Note that .NET Framework projects and IDEs that do not support Source Generators are not supported.
    dotnet add package Nino
  5. Serialize generic and collection types in Nino

    Nino supports serialization of generic types and various collection types, provided the type arguments are themselves serializable.

    Generic Types: Supports generic structs, classes, and records, including those with constraints (e.g., where T : IList).

    Supported Collections:

    • List<T>, T[], ICollection<T>, Span<T>, HashSet<T>, ArraySegment<T>
    • Dictionary<TKey, TValue>, IDictionary<TKey, TValue>

    Nested Types: Complex nesting is supported, such as List<Dictionary<int, SampleStruct>>[].

    [NinoType]
    public class Generic<T>
    {
        public T Val;
    }
    
    // Usage: Ensure T is serializable
    Generic<int> genericInt = new Generic<int>();
    Generic<List<int>> genericList = new Generic<List<int>>();
  6. Install Nino in a Unity Project

    Nino supports Unity 2022.3+ across all player platforms. To install:

    1. Download the Nino.unitypackage from the GitHub Releases page.
    2. Install the Microsoft.CodeAnalysis NuGet package to enable Source Generator support in Unity. This should be done from the Unity project root directory (the same level as the Assets folder).
    3. Import the Nino.unitypackage into the project.
    4. Move the imported Nino folder into a directory containing an Assembly Definition (.asmdef) file. This is required to prevent code generation errors due to a Unity bug.
    5. If multiple asmdefs are used, copy the Nino directory into each respective directory.

    Note: If you see a warning stating Nino.Core references netstandard, Version=2.1.0.0... A roslyn analyzer should reference netstandard version 2.0, this is a known Unity bug and can be safely ignored.

    # Run from the Unity project root directory
    dotnet add package Microsoft.CodeAnalysis.CSharp --version 4.10.0
  7. Use custom constructors for deserialization

    Nino supports custom constructors to perform additional operations during deserialization (e.g., initializing non-serialized fields). Use the [NinoConstructor] attribute to specify which members should be passed as arguments to the constructor.

    Behavior by Type:

    • Records: Nino prioritizes the primary constructor. Use [NinoConstructor] to override this.
    • Classes: Nino automatically uses the first public constructor if it is parameterless or if all parameters match the names of fields/properties being deserialized. Use [NinoConstructor] to specify a different one.
    • Structs: Nino automatically uses the parameterless constructor. Use [NinoConstructor] to force a parameterized constructor.

    Restrictions:

    • The constructor must be public.
    • The number of parameters in the constructor must match the number of parameters listed in the [NinoConstructor] attribute.
    • Parameters in the attribute must match the names (case-insensitive) of fields or properties that are actually being serialized.
    [NinoType]
    public record SimpleRecord
    {
        public int Id;
        public string Name;
        public DateTime CreateTime;
    
        // This constructor will be used for deserialization
        [NinoConstructor(nameof(Id), nameof(Name))]
        public SimpleRecord(int id, string name)
        {
            Id = id;
            Name = name;
            CreateTime = DateTime.Now; // Initialize non-serialized or derived field
        }
    }
    
    [NinoType]
    public struct SimpleStruct
    {
        public int Id;
        public string Name;
        public DateTime CreateTime;
        
        // Force struct to use this instead of parameterless constructor
        [NinoConstructor(nameof(Id), nameof(Name), nameof(CreateTime))]
        public SimpleStruct(int a, string b, DateTime c)
        {
            Id = a;
            Name = b;
            CreateTime = c;
        }
    }
  8. Handle polymorphic types and abstractions

    Nino supports polymorphism, allowing the serialization of interfaces, abstract classes, and derived classes. To enable this, every involved type (the interface/base class AND the implementation/derived class) must be decorated with the [NinoType] attribute.

    When a derived object is serialized as its base type, Nino preserves the metadata to automatically restore the original derived type during deserialization.

    [NinoType]
    public interface IBase { int A { get; set; } }
    
    [NinoType]
    public class Impl : IBase { public int A { get; set; } }
    
    // Example: Serializing a list of mixed types
    var list = new List<IBase> { new Impl { A = 1 } };
    var bytes = Serializer.Serialize(list);
    Deserializer.Deserialize(bytes, out List<IBase> result);
  9. Understand Nino serialization constraints and requirements

    Nino has several core limitations that developers must be aware of during implementation:

    • Source Generation: Nino must be used in conjunction with a Source Generator.
    • Member Visibility: Non-public fields and properties cannot be serialized or deserialized.
    • Supported Types: Attempting to serialize unsupported types (e.g., Queue) will result in a compilation error.
  10. Optimize serialization memory with IBufferWriter

    To avoid allocating a new byte array for every serialization operation, use the IBufferWriter<byte> overload. By implementing your own writer or using .NET Core's ArrayBufferWriter<byte>, you can reuse the same buffer across multiple operations by calling ResetWrittenCount().
    ObjClass obj = new ObjClass();
    ArrayBufferWriter<byte> bufferWriter = new ArrayBufferWriter<byte>();
    
    // Initial serialization
    Serializer.Serialize(obj, bufferWriter);
    
    // Reuse buffer for subsequent serialization to avoid memory allocation
    bufferWriter.ResetWrittenCount(); 
    obj.Serialize(bufferWriter);
  11. Efficiently deserialize ReadOnlySequence from network pipelines

    When dealing with ReadOnlySequence<byte> (e.g., from System.IO.Pipelines), data may be fragmented. To deserialize efficiently:

    1. If the data is a single segment, pass the FirstSpan directly.
    2. If fragmented and small (≤ 1024 bytes), use stackalloc to create a temporary contiguous buffer on the stack.
    3. If fragmented and large, rent a buffer from ArrayPool<byte>.Shared and return it after deserialization.
    ReadOnlySequence<byte> data = xxxxx;
    ObjClass obj;
    
    if(data.IsSingleSegment)
    {
      Span<byte> dataSpan = data.FirstSpan;
      Deserializer.Deserialize(dataSpan, out obj);
    }
    else
    {
      if(data.Length <= 1024)
      {
        // Allocate on the stack
        Span<byte> stackMemory = stackalloc byte[(int)data.Length];
        data.CopyTo(stackMemory);
        Deserializer.Deserialize(stackMemory, out obj);
      }
      else
      {
        // Allocate on the heap using object pool
        byte[] arr = ArrayPool<byte>.Shared.Rent((int)data.Length);
        data.CopyTo(arr);
        Deserializer.Deserialize(arr, out obj);
        ArrayPool<byte>.Shared.Return(arr);
      }
    }
  12. Manage version compatibility for Nino serializable types

    When modifying types decorated with [NinoType], certain changes are permitted while others will break serialization/deserialization:

    • Allowed Modifications:
      • Adding new fields or properties that are not serialized/deserialized (either by using the [NinoIgnore] attribute or by omitting [NinoMember] when [NinoType(false)] is used).
      • Adding new serializable members to a type. This allows a new version of a game to deserialize data from an older version (where those members were missing), though additional workarounds may be required for full stability (see Advanced Usage for weak-version tolerance).
    • Forbidden Modifications:
      • Deleting serialized fields or properties is not allowed.
      • Changing the type of a reference type field to another reference type is not allowed.
      • Adding reference type members to unmanaged structs is not possible.

    Note: In some cases, types with the same memory size (e.g., float and int) may be interchangeable in collections like List<T> depending on the specific runtime implementation.

    [NinoType]
    public class SampleClass
    {
      // Example of adding a non-serialized field (allowed)
      [NinoIgnore]
      public int TemporaryCache;
    }