MemoryPack Documentation

repository·main·Indexed 26 days ago

https://github.com/cysharp/memorypack

A zero-encoding, high-performance binary serializer for C# and Unity. It utilizes C# source generators to implement IMemoryPackable<T> and optimizes speed by copying memory directly. Supports .NET 7 (recommended) and .NET Standard 2.1. Features include version tolerance, circular reference support, polymorphism via Unions, and custom serialization callbacks.

Tokens
12.1K
Snippets
25
Records
39
Agent score
39%

What's inside MemoryPack

  1. Setup MemoryPack in Unity

    main

    MemoryPack requires Unity version 2022.3.12f1 or higher. Follow these steps for installation:

    1. Install Core Package: Use NuGetForUnity to install MemoryPack from NuGet.
      • Note: If you encounter version conflicts, disable "Assembly Version Validation" in Project Settings > Player > Other Settings.
    2. Install Unity Extensions: Add the MemoryPack.Unity package via Git URL: https://github.com/Cysharp/MemoryPack.git?path=src/MemoryPack.Unity/Assets/MemoryPack.Unity
      • You can specify a version using the # suffix, e.g., #1.0.0.
    3. Unity-Specific Types: For shared code containing Unity types (like Vector3), install the MemoryPack.UnityShims NuGet package.

    Limitations in Unity:

    • CustomFormatter is not supported.
    • Binary format is not fully compatible with .NET 7+ if using [StructLayout(LayoutKind.Auto)] on value types like DateTimeOffset or ValueTuple. Avoid these types for cross-platform compatibility.
  2. Configure serialization info output to files

    main

    You can inspect which members are serialized by checking IntelliSense or by exporting serialization information to a file at compile time. Set the MemoryPackGenerator_SerializationInfoOutputDirectory property in your MSBuild project file to specify the output directory.

    <!-- output memorypack serialization info to directory -->
    <ItemGroup>
        <CompilerVisibleProperty Include="MemoryPackGenerator_SerializationInfoOutputDirectory" />
    </ItemGroup>
    <PropertyGroup>
        <MemoryPackGenerator_SerializationInfoOutputDirectory>$(MSBuildProjectDirectory)\MemoryPackLogs</MemoryPackGenerator_SerializationInfoOutputDirectory>
    </PropertyGroup>
  3. Integrate MemoryPack with ASP.NET Core MVC

    main

    Use the MemoryPack.AspNetCoreMvcFormatter package to enable MemoryPack as a content type for your ASP.NET Core controllers. This allows clients to send and receive binary data using the application/x-memorypack media type.

    Server-side Setup: Register MemoryPackInputFormatter and MemoryPackOutputFormatter in your Program.cs or Startup.cs.

    Client-side (HttpClient) Setup: When calling the API, set the Content-Type header to application/x-memorypack.

    // Server-side registration
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new MemoryPackInputFormatter());
        // checkContentType: false allows outputting multiple formats (e.g. JSON and MemoryPack)
        options.OutputFormatters.Insert(0, new MemoryPackOutputFormatter(checkContentType: false));
    });
    // Client-side HttpClient usage
    var content = new ByteArrayContent(bin)
    content.Headers.ContentType = new MediaTypeHeaderValue("application/x-memorypack");
  4. Optimize deserialization with array pooling

    main

    To efficiently deserialize large arrays and reduce allocations, use MemoryPoolFormatter<T> or ReadOnlyMemoryPoolFormatter<T>.

    When using these formatters, you are responsible for returning the memory to the pool. A common pattern is to use the [MemoryPackOnDeserialized] attribute to set a flag, and then implement IDisposable to return the memory to ArrayPool<T>.Shared.

    [MemoryPackable]
    public partial class PoolModelSample : IDisposable
    {
        public int Id { get; }
    
    [MemoryPoolFormatter<byte>]
        public Memory<byte> Payload { get; private set; }
    
        public PoolModelSample(int id, Memory<byte> payload)
        {
            Id = id;
            Payload = payload;
        }
    
        bool usePool;
    
    [MemoryPackOnDeserialized]
        void OnDeserialized()
        {
            usePool = true;
        }
    
        public void Dispose()
        {
            if (!usePool) return;
            Return(Payload);
            Payload = default;
        }
    
        static void Return<T>(Memory<T> memory) => Return((ReadOnlyMemory<T>)memory);
    
        static void Return(ReadOnlyMemory<byte> memory)
        {
            if (MemoryMarshal.TryGetArray(memory, out var segment) && segment.Array is { Length: > 0 })
            {
                ArrayPool<byte>.Shared.Return(segment.Array, clearArray: RuntimeHelpers.IsReferenceOrContainsReferences<byte>());
            }
        }
    }
  5. Use Brotli compression and decompression

    main

    MemoryPack provides BrotliCompressor and BrotliDecompressor structs for high-performance compression.

    Important Notes:

    • Both are structs and do not allocate on the heap, but they use an internal memory pool. You must use using to ensure memory is released.
    • The default compression level is CompressionLevel.Fastest (quality-1), which is optimized for serialization speed and is much faster than the .NET default (CompressionLevel.Optimal).
    • You can apply Brotli compression to specific members of a [MemoryPackable] class using the [BrotliFormatter] attribute.
    // Decompression(require using)
    using var decompressor = new BrotliDecompressor();
    
    // Get decompressed ReadOnlySequence<byte> from ReadOnlySpan<byte> or ReadOnlySequence<byte>
    var decompressedBuffer = decompressor.Decompress(buffer);
    
    var value = MemoryPackSerializer.Deserialize<T>(decompressedBuffer);
  6. Manage schema evolution and version tolerance

    main

    By default (GenerateType.Object), MemoryPack supports limited schema evolution:

    Allowed changes:

    • Adding new members.
    • Changing member names.

    Disallowed changes:

    • Deleting members.
    • Changing member order.
    • Changing member types.
    • Changing unmanaged structs.

    Handling missing data: When reading old data into a new schema, missing members are initialized to their default literal. To use custom initial values instead, use the [SuppressDefaultInitialization] attribute.

    Limitations of [SuppressDefaultInitialization]:

    • Cannot be used with readonly, init-only, or required modifiers.
    [MemoryPackable]
    public partial class DefaultValue
    {
        public string Prop1 { get; set; }
    
        [SuppressDefaultInitialization]
        public int Prop2 { get; set; } = 111; // If old data is missing, set to 111 instead of default.
    
        public int Prop3 { get; set; } = 222; // If old data is missing, set to default (0).
    }
  7. Quick Start with MemoryPack

    main

    To use MemoryPack, define a class, struct, record, or record struct and annotate it with the [MemoryPackable] attribute and the partial keyword. The C# source generator will implement the IMemoryPackable<T> interface automatically. Use MemoryPackSerializer.Serialize<T> and MemoryPackSerializer.Deserialize<T> to perform operations.

    using MemoryPack;
    
    [MemoryPackable]
    public partial class Person
    {
        public int Age { get; set; }
        public string Name { get; set; }
    }
    
    // Usage
    var v = new Person { Age = 40, Name = "John" };
    var bin = MemoryPackSerializer.Serialize(v);
    var val = MemoryPackSerializer.Deserialize<Person>(bin);
  8. Serialize external types via custom formatters

    main

    To serialize types that you cannot annotate with [MemoryPackable] (external types), you should create a custom MemoryPackFormatter<T>.

    Recommended Pattern: The Wrapper Approach

    1. Create a [MemoryPackable] wrapper struct/class that holds the external type.
    2. Use [MemoryPackIgnore] on the external type field.
    3. Use [MemoryPackInclude] on properties that extract the necessary data from the external type.
    4. Provide a [MemoryPackConstructor] that reconstructs the external type from the included properties.
    5. Implement a MemoryPackFormatter<T> that uses the wrapper to perform the actual serialization/deserialization.
    6. Register the formatter using MemoryPackFormatterProvider.Register<T>(new YourFormatter()) during application startup.
    // 1. Create a wrapper
    [MemoryPackable]
    public readonly partial struct SerializableAnimationCurve
    {
        [MemoryPackIgnore]
        public readonly AnimationCurve AnimationCurve;
    
        [MemoryPackInclude]
        WrapMode preWrapMode => AnimationCurve.preWrapMode;
        [MemoryPackInclude]
        WrapMode postWrapMode => AnimationCurve.postWrapMode;
        [MemoryPackInclude]
        Keyframe[] keys => AnimationCurve.keys;
    
        [MemoryPackConstructor]
        SerializableAnimationCurve(WrapMode preWrapMode, WrapMode postWrapMode, Keyframe[] keys)
        {
            var curve = new AnimationCurve(keys);
            curve.preWrapMode = preWrapMode;
            curve.postWrapMode = postWrapMode;
            this.AnimationCurve = curve;
        }
    
        public SerializableAnimationCurve(AnimationCurve animationCurve)
        {
            this.AnimationCurve = animationCurve;
        }
    }
    
    // 2. Create the formatter
    public class AnimationCurveFormatter : MemoryPackFormatter<AnimationCurve>
    {
        public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref AnimationCurve? value)
        {
            if (value == null)
            {
                writer.WriteNullObjectHeader();
                return;
            }
            writer.WritePackable(new SerializableAnimationCurve(value));
        }
    
        public override void Deserialize(ref MemoryPackReader reader, scoped ref AnimationCurve? value)
        {
            if (reader.PeekIsNull())
            {
                reader.Advance(1); // skip null block
                value = null;
                return;
            }
            var wrapped = reader.ReadPackable<SerializableAnimationCurve>();
            value = wrapped.AnimationCurve;
        }
    }
    
    // 3. Register
    MemoryPackFormatterProvider.Register<AnimationCurve>(new AnimationCurveFormatter());
  9. Implement polymorphism using Union

    main

    MemoryPack supports serializing interfaces and abstract classes via a feature called Union.

    1. Annotate the interface or abstract class with [MemoryPackUnion(tag, typeof(DerivedType))] for each implementation.
    2. Ensure each tag is unique (integers 0 to 65535). Tags below 250 are especially efficient.
    3. Derived types must also be marked [MemoryPackable].

    Cross-Assembly Unions: If the interface and derived types are in different assemblies, use [MemoryPackUnionFormatter(typeof(InterfaceType))] on a formatter class in the second assembly.

    Unity Note: ModuleInitializer is not supported in Unity. You must manually call the formatter's initializer (e.g., UnionSampleFormatterInitializer.RegisterFormatter()) during startup.

    Dynamic Unions: You can assemble unions at runtime using DynamicUnionFormatter<T> and registering it via MemoryPackFormatterProvider.Register(formatter).

    [MemoryPackable]
    [MemoryPackUnion(0, typeof(FooClass))]
    [MemoryPackUnion(1, typeof(BarClass))]
    public partial interface IUnionSample
    {
    }
    
    [MemoryPackable]
    public partial class FooClass : IUnionSample
    {
        public int XYZ { get; set; }
    }
    
    [MemoryPackable]
    public partial class BarClass : IUnionSample
    {
        public string? OPQ { get; set; }
    }
    
    // Usage
    IUnionSample data = new FooClass() { XYZ = 999 };
    var bin = MemoryPackSerializer.Serialize(data);
    var reData = MemoryPackSerializer.Deserialize<IUnionSample>(bin);
  10. Generate TypeScript code from C# types

    main

    MemoryPack can automatically generate TypeScript classes and serialization logic from your C# [MemoryPackable] types.

    Setup Steps:

    1. Configure Output Directory: In your .csproj, use the MemoryPackGenerator_TypeScriptOutputDirectory property to specify where the .js and .ts files should be generated.
    2. Annotate Types: Add the [GenerateTypeScript] attribute to the C# classes you want to export.
    3. Usage in TypeScript: The generated code provides static methods like serialize(value), deserialize(buffer), serializeArray(values), and deserializeArray(buffer). Use application/x-memorypack as the Content-Type when sending via fetch.
    <!-- output memorypack TypeScript code to directory -->
    <ItemGroup>
        <CompilerVisibleProperty Include="MemoryPackGenerator_TypeScriptOutputDirectory" />
    </ItemGroup>
    <PropertyGroup>
        <MemoryPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\memorypack</MemoryPackGenerator_TypeScriptOutputDirectory>
    </PropertyGroup>
    [MemoryPackable]
    [GenerateTypeScript]
    public partial class Person
    {
        public required Guid Id { get; init; }
        // ... other properties
    }
    // Usage in TypeScript
    let person = new Person();
    // ... set properties
    
    // serialize to Uint8Array
    let bin = Person.serialize(person);
    
    let blob = new Blob([bin.buffer], { type: "application/x-memorypack" });
    
    let response = await fetch("http://localhost:5260/api", {
        method: "POST", 
        body: blob, 
        headers: { "Content-Type": "application/x-memorypack" }
    });
    
    let buffer = await response.arrayBuffer();
    
    // deserialize from ArrayBuffer 
    let person2 = Person.deserialize(buffer);
  11. Configure string encoding with MemoryPackSerializerOptions

    main

    You can control whether strings are serialized as UTF8 or UTF16 using MemoryPackSerializerOptions.

    • MemoryPackSerializerOptions.Default (or null): Uses UTF8. This is the default and results in smaller payloads for ASCII strings.
    • MemoryPackSerializerOptions.Utf16: Uses UTF16. This may perform better for non-ASCII characters (like Japanese) or if you plan to compress the data separately.

    Note: You do not need to specify the encoding during deserialization; it is automatically detected.