From 9b2a50c5030521f278263f6c0bd55dceb500c2c1 Mon Sep 17 00:00:00 2001 From: sys32ish Date: Wed, 3 Jun 2026 12:17:07 +1000 Subject: [PATCH 1/5] add array length attribute --- sandbox/SandboxConsoleApp/Models.cs | 8 +++ sandbox/SandboxConsoleApp/Program.cs | 27 ++++---- src/MemoryPack.Core/Attributes.cs | 11 ++++ src/MemoryPack.Core/MemoryPackReader.cs | 31 +++++++++ src/MemoryPack.Core/MemoryPackWriter.cs | 25 ++++++++ .../DiagnosticDescriptors.cs | 15 +++++ .../MemoryPackGenerator.Emitter.cs | 6 ++ .../MemoryPackGenerator.Parser.cs | 41 +++++++++++- src/MemoryPack.Generator/ReferenceSymbols.cs | 2 + tests/MemoryPack.Tests/FixedArrayTest.cs | 64 +++++++++++++++++++ .../Models/BigFixedArrayCheck.cs | 8 +++ tests/MemoryPack.Tests/Models/FixedArrays.cs | 19 ++++++ 12 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 tests/MemoryPack.Tests/FixedArrayTest.cs create mode 100644 tests/MemoryPack.Tests/Models/BigFixedArrayCheck.cs create mode 100644 tests/MemoryPack.Tests/Models/FixedArrays.cs diff --git a/sandbox/SandboxConsoleApp/Models.cs b/sandbox/SandboxConsoleApp/Models.cs index 1c5be0a8..bc066518 100644 --- a/sandbox/SandboxConsoleApp/Models.cs +++ b/sandbox/SandboxConsoleApp/Models.cs @@ -114,7 +114,15 @@ public partial class LisList : List } +[MemoryPackable] +public partial class FixedArrays +{ + //[MemoryPackArrayLength(6)] public long[] data; + //[MemoryPackArrayLength(-6)] public long[] datawithwronglength; + + [MemoryPackArrayLength(1_000_000)] public byte[] data; +} [MemoryPackable] diff --git a/sandbox/SandboxConsoleApp/Program.cs b/sandbox/SandboxConsoleApp/Program.cs index 81fc1fa6..0e1d5dd2 100644 --- a/sandbox/SandboxConsoleApp/Program.cs +++ b/sandbox/SandboxConsoleApp/Program.cs @@ -32,22 +32,17 @@ using System.Runtime.InteropServices; using System.Diagnostics; -CollectionTest sourceCollection = new CollectionTest(); -sourceCollection.Collection.Add("1234"); -sourceCollection.Collection.Add("5678"); - -Pipe bufferPipe = new Pipe(); -MemoryPackSerializer.Serialize(bufferPipe.Writer, sourceCollection); -_ = await bufferPipe.Writer.FlushAsync().ConfigureAwait(false); -ReadResult resultBuffer = await bufferPipe.Reader.ReadAsync().ConfigureAwait(false); - - -//var newSource = new CollectionTest(); -var newSource = MemoryPackSerializer.Deserialize(resultBuffer.Buffer); -Console.WriteLine(newSource.Collection.Count); - - - +FixedArrays x = new() +{ + data = new byte[1_000_000], +}; +var data = MemoryPackSerializer.Serialize(x); +for (int i = 0; i < data.Length; i++) +{ + Console.Write($"{data[i]}" + ' '); +} +Console.WriteLine(); +MemoryPackSerializer.Deserialize(data); [MemoryPackable] public partial class Region diff --git a/src/MemoryPack.Core/Attributes.cs b/src/MemoryPack.Core/Attributes.cs index 9ace47c3..ead3f02b 100644 --- a/src/MemoryPack.Core/Attributes.cs +++ b/src/MemoryPack.Core/Attributes.cs @@ -92,6 +92,17 @@ public MemoryPackOrderAttribute(int order) } } +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class MemoryPackArrayLengthAttribute : Attribute +{ + public long Length { get; } + + public MemoryPackArrayLengthAttribute(long length) + { + this.Length = length; + } +} + #if !UNITY_2021_2_OR_NEWER [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] diff --git a/src/MemoryPack.Core/MemoryPackReader.cs b/src/MemoryPack.Core/MemoryPackReader.cs index e72d153e..d449c468 100644 --- a/src/MemoryPack.Core/MemoryPackReader.cs +++ b/src/MemoryPack.Core/MemoryPackReader.cs @@ -847,6 +847,37 @@ public void ReadSpanWithoutReadLengthHeader(int length, scoped ref Span v } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadArrayWithoutReadLengthHeader(int length, out T?[]? value) + { + if (length == 0) + { + value = Array.Empty(); + return; + } + + if (!RuntimeHelpers.IsReferenceOrContainsReferences()) + { + value = AllocateUninitializedArray(length); + var byteCount = length * Unsafe.SizeOf(); + ref var src = ref GetSpanReference(byteCount); + ref var dest = ref Unsafe.As(ref MemoryMarshal.GetReference(value)!); + Unsafe.CopyBlockUnaligned(ref dest, ref src, (uint)byteCount); + + Advance(byteCount); + } + else + { + value = new T[length]; + + var formatter = GetFormatter(); + for (int i = 0; i < length; i++) + { + formatter.Deserialize(ref this, ref value[i]); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ReadPackableSpanWithoutReadLengthHeader(int length, scoped ref Span value) where T : IMemoryPackable diff --git a/src/MemoryPack.Core/MemoryPackWriter.cs b/src/MemoryPack.Core/MemoryPackWriter.cs index 08f4a3ae..0c8f0d35 100644 --- a/src/MemoryPack.Core/MemoryPackWriter.cs +++ b/src/MemoryPack.Core/MemoryPackWriter.cs @@ -681,6 +681,31 @@ public void WriteSpanWithoutLengthHeader(scoped ReadOnlySpan value) { if (value.Length == 0) return; + if (!RuntimeHelpers.IsReferenceOrContainsReferences()) + { + var srcLength = Unsafe.SizeOf() * value.Length; + ref var dest = ref GetSpanReference(srcLength); + ref var src = ref Unsafe.As(ref MemoryMarshal.GetReference(value)!); + + Unsafe.CopyBlockUnaligned(ref dest, ref src, (uint)srcLength); + + Advance(srcLength); + return; + } + else + { + var formatter = GetFormatter(); + for (int i = 0; i < value.Length; i++) + { + formatter.Serialize(ref this, ref Unsafe.AsRef(in value[i])); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteArrayWithoutLengthHeader(T?[]? value) + { + if (value.Length == 0) return; + if (!RuntimeHelpers.IsReferenceOrContainsReferences()) { var srcLength = Unsafe.SizeOf() * value.Length; diff --git a/src/MemoryPack.Generator/DiagnosticDescriptors.cs b/src/MemoryPack.Generator/DiagnosticDescriptors.cs index 50a77087..76b3229b 100644 --- a/src/MemoryPack.Generator/DiagnosticDescriptors.cs +++ b/src/MemoryPack.Generator/DiagnosticDescriptors.cs @@ -340,4 +340,19 @@ internal static class DiagnosticDescriptors category: Category, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ArrayLengthAttributeMustHaveAValidValue = new( + id: "MEMPACK043", + title: "[MemoryPackArrayLength] must have a valid value", + messageFormat: "The MemoryPackable object '{0}' member '{1}' is annotated with [MemoryPackArrayLength], but it has a invalid value", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + public static readonly DiagnosticDescriptor ArrayLengthAttributeCanOnlyBeUsedForArrays = new( + id: "MEMPACK044", + title: "[MemoryPackArrayLength] can only be applied to arrays", + messageFormat: "The MemoryPackable object '{0}' member '{1}' is annotated with [MemoryPackArrayLength], but isn't a array", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs index d17735fa..af7b45f7 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs @@ -1316,6 +1316,8 @@ public string EmitSerialize(string writer) return $"global::MemoryPack.Formatters.ListFormatter.SerializePackable(ref {writer}, value.@{Name});"; case MemberKind.Array: return $"{writer}.WriteArray(value.@{Name});"; + case MemberKind.FixedArray: + return $"""if (value.@{Name}.Length != {ArrayLength}) throw new MemoryPackSerializationException("Array length mismatch for {Name}"); {writer}.WriteArrayWithoutLengthHeader(value.@{Name});"""; case MemberKind.Blank: return ""; case MemberKind.CustomFormatter: @@ -1373,6 +1375,8 @@ public string EmitReadToDeserialize(int i, bool requireDeltaCheck) return $"{pre}__{Name} = global::MemoryPack.Formatters.ListFormatter.DeserializePackable<{(MemberType as INamedTypeSymbol)!.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader);"; case MemberKind.Array: return $"{pre}__{Name} = reader.ReadArray<{(MemberType as IArrayTypeSymbol)!.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();"; + case MemberKind.FixedArray: + return $"{pre}reader.ReadArrayWithoutReadLengthHeader({ArrayLength}, out __{Name});"; case MemberKind.Blank: return $"{pre}reader.Advance(deltas[{i}]);"; case MemberKind.CustomFormatter: @@ -1410,6 +1414,8 @@ public string EmitReadRefDeserialize(int i, bool requireDeltaCheck) return $"{pre}global::MemoryPack.Formatters.ListFormatter.DeserializePackable(ref reader, ref __{Name});"; case MemberKind.Array: return $"{pre}reader.ReadArray(ref __{Name});"; + case MemberKind.FixedArray: + return $"{pre}reader.ReadArrayWithoutReadLengthHeader({ArrayLength}, out __{Name});"; case MemberKind.Blank: return $"{pre}reader.Advance(deltas[{i}]);"; case MemberKind.CustomFormatter: diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs index 0806cd14..900afa89 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.ComponentModel; +using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -34,6 +35,7 @@ public enum MemberKind // from attribute AllowSerialize, MemoryPackUnion, + FixedArray, Object, // others allow RefLike, // not allowed @@ -386,6 +388,16 @@ public bool Validate(TypeDeclarationSyntax syntax, IGeneratorContext context, bo context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name)); noError = false; } + else if (item is { HasArrayLength: true, ArrayLength: < 1 }) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ArrayLengthAttributeMustHaveAValidValue, item.GetLocation(syntax), Symbol.Name, item.Name)); + noError = false; + } + else if (item is { HasArrayLength: true } and not { MemberType.TypeKind: TypeKind.Array }) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ArrayLengthAttributeCanOnlyBeUsedForArrays, item.GetLocation(syntax), Symbol.Name, item.Name)); + noError = false; + } } } @@ -625,6 +637,8 @@ partial class MemberMeta public string? ConstructorParameterName { get; } public int Order { get; } public bool HasExplicitOrder { get; } + public bool HasArrayLength { get; } + public long? ArrayLength { get; } public MemberKind Kind { get; } public bool SuppressDefaultInitialization { get; } @@ -655,6 +669,21 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r this.HasExplicitOrder = false; } + var arrayLengthAttr = symbol.GetAttribute(references.MemoryPackArrayLengthAttribute); + if (arrayLengthAttr != null) + { + if ((long)arrayLengthAttr.ConstructorArguments[0].Value == null || (long)arrayLengthAttr.ConstructorArguments[0].Value < 1) + { + ArrayLength = -1; + } + else + { + ArrayLength = (long)arrayLengthAttr.ConstructorArguments[0].Value!; + } + + HasArrayLength = true; + } + if (constructor != null) { this.IsConstructorParameter = constructor.TryGetConstructorParameter(symbol, out var constructorParameter); @@ -740,9 +769,17 @@ public Location GetLocation(TypeDeclarationSyntax fallback) static MemberKind ParseMemberKind(ISymbol? memberSymbol, ITypeSymbol memberType, ReferenceSymbols references) { - if (memberType.SpecialType is SpecialType.System_Object or SpecialType.System_Array or SpecialType.System_Delegate or SpecialType.System_MulticastDelegate || memberType.TypeKind == TypeKind.Delegate) + if (memberSymbol?.GetAttribute(references.MemoryPackArrayLengthAttribute) != null) + { + return MemberKind.FixedArray; + } + if (memberType.SpecialType is SpecialType.System_Object + or SpecialType.System_Array + or SpecialType.System_Delegate + or SpecialType.System_MulticastDelegate || + memberType.TypeKind == TypeKind.Delegate) { - return MemberKind.NonSerializable; // object, Array, delegate is not allowed + return MemberKind.NonSerializable; // object, Array, delegate are not allowed except arrays with length attr } else if (memberType.TypeKind == TypeKind.Enum) { diff --git a/src/MemoryPack.Generator/ReferenceSymbols.cs b/src/MemoryPack.Generator/ReferenceSymbols.cs index f29e52d4..5f49ab2f 100644 --- a/src/MemoryPack.Generator/ReferenceSymbols.cs +++ b/src/MemoryPack.Generator/ReferenceSymbols.cs @@ -14,6 +14,7 @@ public class ReferenceSymbols public INamedTypeSymbol MemoryPackConstructorAttribute { get; } public INamedTypeSymbol MemoryPackAllowSerializeAttribute { get; } public INamedTypeSymbol MemoryPackOrderAttribute { get; } + public INamedTypeSymbol MemoryPackArrayLengthAttribute { get; } public INamedTypeSymbol? MemoryPackCustomFormatterAttribute { get; } // Unity is null. public INamedTypeSymbol? MemoryPackCustomFormatter2Attribute { get; } // Unity is null. public INamedTypeSymbol MemoryPackIgnoreAttribute { get; } @@ -39,6 +40,7 @@ public ReferenceSymbols(Compilation compilation) MemoryPackConstructorAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackConstructorAttribute"); MemoryPackAllowSerializeAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackAllowSerializeAttribute"); MemoryPackOrderAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOrderAttribute"); + MemoryPackArrayLengthAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackArrayLengthAttribute"); MemoryPackCustomFormatterAttribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`1")?.ConstructUnboundGenericType(); MemoryPackCustomFormatter2Attribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`2")?.ConstructUnboundGenericType(); MemoryPackIgnoreAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackIgnoreAttribute"); diff --git a/tests/MemoryPack.Tests/FixedArrayTest.cs b/tests/MemoryPack.Tests/FixedArrayTest.cs new file mode 100644 index 00000000..605cfd04 --- /dev/null +++ b/tests/MemoryPack.Tests/FixedArrayTest.cs @@ -0,0 +1,64 @@ +using MemoryPack.Tests.Models; +using Newtonsoft.Json; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MemoryPack.Tests; + +public class FixedArrayTest +{ + [Fact] + public void Check() + { + var checker = new FixedArrayCheck() + { + ByteData = new byte[10], + NestedData = + [ + new FixedArrayCheck.Nested() + { + Data1 = 0, + Data2 = "Hello World. !@#$%^&*()_+", + Data3 = (123456789, 987654321) + } + ], + StringData = new string[] + { + "Hello World. !@#$%^&*()_+", + "Hi", + "Greetings", + "Nice to meet you", + "The end" + } + }; + + var bin = MemoryPackSerializer.Serialize(checker); + var v2 = MemoryPackSerializer.Deserialize(bin); + + v2.Should().BeEquivalentTo(checker); + } + + [Fact] + public void Check2() + { + var checker = new BigFixedArrayCheck(); + + checker.BigData1 = new byte[100_000]; + checker.BigData2 = new byte[1_000_000]; + + Random.Shared.NextBytes(checker.BigData1); + Random.Shared.NextBytes(checker.BigData2); + + var bin = MemoryPackSerializer.Serialize(checker); + var v2 = MemoryPackSerializer.Deserialize(bin); +#pragma warning disable CS8602 + v2.BigData1.Should().BeEquivalentTo(checker.BigData1); + v2.BigData2.Should().BeEquivalentTo(checker.BigData2); + } +} + diff --git a/tests/MemoryPack.Tests/Models/BigFixedArrayCheck.cs b/tests/MemoryPack.Tests/Models/BigFixedArrayCheck.cs new file mode 100644 index 00000000..597d520e --- /dev/null +++ b/tests/MemoryPack.Tests/Models/BigFixedArrayCheck.cs @@ -0,0 +1,8 @@ +namespace MemoryPack.Tests.Models; + +[MemoryPackable] +public partial class BigFixedArrayCheck +{ + [MemoryPackArrayLength(100_000)] public byte[] BigData1; + [MemoryPackArrayLength(1_000_000)] public byte[] BigData2; +} diff --git a/tests/MemoryPack.Tests/Models/FixedArrays.cs b/tests/MemoryPack.Tests/Models/FixedArrays.cs new file mode 100644 index 00000000..1663d731 --- /dev/null +++ b/tests/MemoryPack.Tests/Models/FixedArrays.cs @@ -0,0 +1,19 @@ +namespace MemoryPack.Tests.Models; + +[MemoryPackable] +public partial class FixedArrayCheck +{ + [MemoryPackArrayLength(10)] public byte[] ByteData; + [MemoryPackArrayLength(5)] public string[] StringData { get; set; } + [MemoryPackArrayLength(1)] public Nested[] NestedData { get; set; } + + [MemoryPackable] + public partial class Nested + { + public byte Data1; + public string Data2; + public (int, int) Data3; + } +} + + From f734ce6b4a224cda69c5b8130bf9c7f0a2f8df91 Mon Sep 17 00:00:00 2001 From: sys32ish Date: Wed, 3 Jun 2026 12:22:26 +1000 Subject: [PATCH 2/5] change length type to int --- src/MemoryPack.Core/Attributes.cs | 4 ++-- src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MemoryPack.Core/Attributes.cs b/src/MemoryPack.Core/Attributes.cs index ead3f02b..4c66c7f8 100644 --- a/src/MemoryPack.Core/Attributes.cs +++ b/src/MemoryPack.Core/Attributes.cs @@ -95,9 +95,9 @@ public MemoryPackOrderAttribute(int order) [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class MemoryPackArrayLengthAttribute : Attribute { - public long Length { get; } + public int Length { get; } - public MemoryPackArrayLengthAttribute(long length) + public MemoryPackArrayLengthAttribute(int length) { this.Length = length; } diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs index 900afa89..e2b3e660 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs @@ -638,7 +638,7 @@ partial class MemberMeta public int Order { get; } public bool HasExplicitOrder { get; } public bool HasArrayLength { get; } - public long? ArrayLength { get; } + public int? ArrayLength { get; } public MemberKind Kind { get; } public bool SuppressDefaultInitialization { get; } @@ -672,13 +672,13 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r var arrayLengthAttr = symbol.GetAttribute(references.MemoryPackArrayLengthAttribute); if (arrayLengthAttr != null) { - if ((long)arrayLengthAttr.ConstructorArguments[0].Value == null || (long)arrayLengthAttr.ConstructorArguments[0].Value < 1) + if (arrayLengthAttr.ConstructorArguments[0].Value == null || (int?)arrayLengthAttr.ConstructorArguments[0].Value < 1) { ArrayLength = -1; } else { - ArrayLength = (long)arrayLengthAttr.ConstructorArguments[0].Value!; + ArrayLength = (int?)arrayLengthAttr.ConstructorArguments[0].Value; } HasArrayLength = true; From 3c3945be4b448914ba948793f0fa596dfdcf510c Mon Sep 17 00:00:00 2001 From: sys32ish Date: Wed, 3 Jun 2026 12:52:50 +1000 Subject: [PATCH 3/5] add information about [MemoryPackArrayLength] --- README.md | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9c6a39e6..3f477e30 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Zero encoding extreme performance binary serializer for C# and Unity. ![image](https://user-images.githubusercontent.com/46207/200979655-63ed38ae-dad2-4ca0-bbb7-9e0aa98914af.png) -> Compared with [System.Text.Json](https://learn.microsoft.com/ja-jp/dotnet/api/system.text.json), [protobuf-net](https://github.com/protobuf-net/protobuf-net), [MessagePack for C#](https://github.com/neuecc/MessagePack-CSharp), [Orleans.Serialization](https://github.com/dotnet/orleans/). Measured by .NET 7 / Ryzen 9 5950X machine. These serializers have `IBufferWriter` method, serialized using `ArrayBufferWriter` and reused to avoid measure buffer copy. +> Compared with [System.Text.Json](https://learn.microsoft.com/ja-jp/dotnet/api/system.text.json), [protobuf-net](https://github.com/protobuf-net/protobuf-net), [MessagePack for C#](https://github.com/neuecc/MessagePack-CSharp), [Orleans.Serialization](https://github.com/dotnet/orleans/). Measured by .NET 7 / Ryzen 9 5950X machine. These serializers have `IBufferWriter` method, serialized using `ArrayBufferWriter` and reused to avoid measure buffer copy. For standard objects, MemoryPack is x10 faster and x2 ~ x5 faster than other binary serializers. For struct array, MemoryPack is even more powerful, with speeds up to x50 ~ x200 greater than other serializers. @@ -80,7 +80,7 @@ These types can be serialized by default: * `T[]`, `T[,]`, `T[,,]`, `T[,,,]`, `Memory<>`, `ReadOnlyMemory<>`, `ArraySegment<>`, `ReadOnlySequence<>` * `Nullable<>`, `Lazy<>`, `KeyValuePair<,>`, `Tuple<,...>`, `ValueTuple<,...>` * `List<>`, `LinkedList<>`, `Queue<>`, `Stack<>`, `HashSet<>`, `SortedSet<>`, `PriorityQueue<,>` -* `Dictionary<,>`, `SortedList<,>`, `SortedDictionary<,>`, `ReadOnlyDictionary<,>` +* `Dictionary<,>`, `SortedList<,>`, `SortedDictionary<,>`, `ReadOnlyDictionary<,>` * `Collection<>`, `ReadOnlyCollection<>`, `ObservableCollection<>`, `ReadOnlyObservableCollection<>` * `IEnumerable<>`, `ICollection<>`, `IList<>`, `IReadOnlyCollection<>`, `IReadOnlyList<>`, `ISet<>` * `IDictionary<,>`, `IReadOnlyDictionary<,>`, `ILookup<,>`, `IGrouping<,>`, @@ -92,6 +92,7 @@ Define `[MemoryPackable]` `class` / `struct` / `record` / `record struct` `[MemoryPackable]` can annotate to any `class`, `struct`, `record`, `record struct` and `interface`. If a type is `struct` or `record struct` which contains no reference types ([C# Unmanaged types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/unmanaged-types)) any additional annotation (ignore, include, constructor, callbacks) is not used, that serialize/deserialize directly from the memory. Otherwise, by default, `[MemoryPackable]` serializes public instance properties or fields. You can use `[MemoryPackIgnore]` to remove serialization target, `[MemoryPackInclude]` promotes a private member to serialization target. +You can use `[MemoryPackArrayLength]` to make a fixed-length array without unsafe. Note that this is not version-tolerant. ```csharp [MemoryPackable] @@ -120,6 +121,12 @@ public partial class Sample int privateField2; [MemoryPackInclude] int privateProperty2 { get; set; } + + // use [MemoryPackArrayLength] to make a fixed-length array without unsafe + [MemoryPackArrayLength(10)] + public int[] IntFixedArrayField; + [MemoryPackArrayLength(10)] + public int[] IntFixedArrayProperty { get; set; } } ``` @@ -210,7 +217,7 @@ public partial class Person3 ### Serialization callbacks -When serializing/deserializing, MemoryPack can invoke a before/after event using the `[MemoryPackOnSerializing]`, `[MemoryPackOnSerialized]`, `[MemoryPackOnDeserializing]`, `[MemoryPackOnDeserialized]` attributes. It can annotate both static and instance (non-static) methods, and public and private methods. +When serializing/deserializing, MemoryPack can invoke a before/after event using the `[MemoryPackOnSerializing]`, `[MemoryPackOnSerialized]`, `[MemoryPackOnDeserializing]`, `[MemoryPackOnDeserialized]` attributes. It can annotate both static and instance (non-static) methods, and public and private methods. ```csharp [MemoryPackable] @@ -558,7 +565,7 @@ public partial class DefaultValue [SuppressDefaultInitialization] public int Prop2 { get; set; } = 111; // < if old data is missing, set `111`. - + public int Prop3 { get; set; } = 222; // < if old data is missing, set `default`. } ``` @@ -577,9 +584,11 @@ When using `GenerateType.VersionTolerant`, it supports full version-tolerant. * can't change member order * can't change member type +Note that arrays with [MemoryPackArrayLength] are not version-tolerant. The length cannot be changed without breaking compatibility with old data. + ```csharp -// Ok to serialize/deserialize both -// VersionTolerantObject1 -> VersionTolerantObject2 and +// Ok to serialize/deserialize both +// VersionTolerantObject1 -> VersionTolerantObject2 and // VersionTolerantObject2 -> VersionTolerantObject1 [MemoryPackable(GenerateType.VersionTolerant)] @@ -738,7 +747,7 @@ public partial class Sample // In deserialize, Dictionary is initialized with StringComparer.OrdinalIgnoreCase. [OrdinalIgnoreCaseStringDictionaryFormatter] public Dictionary? Ids { get; set; } - + // In deserialize time, all string is interned(see: String.Intern). If similar values come repeatedly, it saves memory. [InternStringFormatter] public string? Flag { get; set; } @@ -1014,7 +1023,7 @@ public class AnimationCurveFormatter : MemoryPackFormatter value = null; return; } - + var wrapped = reader.ReadPackable(); value = wrapped.AnimationCurve; } @@ -1088,7 +1097,7 @@ The generated code is as follows, with simple fields and static methods for seri ```typescript import { MemoryPackWriter } from "./MemoryPackWriter.js"; import { MemoryPackReader } from "./MemoryPackReader.js"; -import { Gender } from "./Gender.js"; +import { Gender } from "./Gender.js"; export class Person { id: string; @@ -1158,7 +1167,7 @@ let response = await fetch("http://localhost:5260/api", let buffer = await response.arrayBuffer(); -// deserialize from ArrayBuffer +// deserialize from ArrayBuffer let person2 = Person.deserialize(buffer); ``` @@ -1201,7 +1210,7 @@ There are a few restrictions on the types that can be generated. Among the primi | `ulong` | `bigint` | | `float` | `number` | | `double` | `number` | -| `string` | `string \| null` | +| `string` | `string \| null` | | `Guid` | `string` | In TypeScript, represents as string but serialize/deserialize as 16byte binary | `DateTime` | `Date` | DateTimeKind will be ignored | `enum` | `const enum` | `long` and `ulong` underlying type is not supported @@ -1384,7 +1393,7 @@ The `MemoryPack.UnityShims` package provides shims for Unity's standard structs Native AOT --- -Unfortunately, .NET 7 Native AOT causes crash (`Generic virtual method pointer lookup failure`) when use MemoryPack due to a runtime bug. It +Unfortunately, .NET 7 Native AOT causes crash (`Generic virtual method pointer lookup failure`) when use MemoryPack due to a runtime bug. It is going to be fixed in .NET 8. Using ``Microsoft.DotNet.ILCompiler` preview version, will fix it in .NET 7. Please see [issue's comment](https://github.com/Cysharp/MemoryPack/issues/75#issuecomment-1386884611) how setup it. Binary wire format specification @@ -1422,7 +1431,7 @@ Version Tolerant Object is similar as Object but has byte length of values in he ### Circular Reference Object -`(byte memberCount, [varint byte-length-of-values...], varint referenceId, [values...])` +`(byte memberCount, [varint byte-length-of-values...], varint referenceId, [values...])` `(250, varint referenceId)` Circular Reference Object is similar as Version Tolerant Object but if memberCount is 250, next varint(unsigned-int32) is referenceId. If not, after byte-length-of-values, varint referenceId is written. @@ -1439,16 +1448,22 @@ Tuple is fixed-size, non-nullable value collection. In .NET, `KeyValuePair`, utf16-value's byte count is utf16-length * 2). If first signed integer <= `-2`, value is encoded by UTF8. utf8-byte-count is encoded in complement, `~utf8-byte-count` to retrieve count of bytes. Next signed integer is utf16-length, it allows `-1` that represents unknown length. utf8-bytes store bytes for the number of utf8-byte-count. ### Union -`(byte tag, value)` +`(byte tag, value)` `(250, ushort tag, value)` First unsigned byte is tag that for discriminated value type or flag, `0` to `249` represents tag, `250` represents next unsigned short is tag, `255` represents union is `null`. From 3d28b27798f49f32fd3c3f20ad5f77aed28bf492 Mon Sep 17 00:00:00 2001 From: sys32ish Date: Wed, 3 Jun 2026 13:16:16 +1000 Subject: [PATCH 4/5] minor change --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f477e30..696fcb6e 100644 --- a/README.md +++ b/README.md @@ -584,7 +584,7 @@ When using `GenerateType.VersionTolerant`, it supports full version-tolerant. * can't change member order * can't change member type -Note that arrays with [MemoryPackArrayLength] are not version-tolerant. The length cannot be changed without breaking compatibility with old data. +Note that arrays with [MemoryPackArrayLength] are not compatible with GenerateType.VersionTolerant or GenerateType.CircularReference. ```csharp // Ok to serialize/deserialize both @@ -1452,7 +1452,7 @@ Collection has 4 byte signed integer as data count in header, `-1` represents `n `[values...]` -Fixed array don't have any data count. The count is derived from the C# schema with the \[MemoryPackArrayLength] attribute. +Fixed array doesn't have any data count. The count is derived from the C# schema with the \[MemoryPackArrayLength] attribute. ### String From 896e2d59ce8142bc968d6e0482c570d41e8462cd Mon Sep 17 00:00:00 2001 From: sys32ish Date: Wed, 3 Jun 2026 16:48:21 +1000 Subject: [PATCH 5/5] add null guard --- src/MemoryPack.Core/MemoryPackWriter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MemoryPack.Core/MemoryPackWriter.cs b/src/MemoryPack.Core/MemoryPackWriter.cs index 0c8f0d35..3a3dda51 100644 --- a/src/MemoryPack.Core/MemoryPackWriter.cs +++ b/src/MemoryPack.Core/MemoryPackWriter.cs @@ -704,6 +704,8 @@ public void WriteSpanWithoutLengthHeader(scoped ReadOnlySpan value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayWithoutLengthHeader(T?[]? value) { + if (value == null) return; + if (value.Length == 0) return; if (!RuntimeHelpers.IsReferenceOrContainsReferences())