From 92820d3d1ccc54c0108904d51a326bc7ee234a5c Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Thu, 13 Feb 2025 21:41:48 +0200 Subject: [PATCH 1/3] wip add support for required and init-only properties --- src/MemoryPack.Core/UnsafePropertyUpdater.cs | 61 ++++++++++++++ .../FullyQualifiedNameRewriter.cs | 33 ++++++++ .../MemoryPackGenerator.Emitter.cs | 37 +++++++-- .../MemoryPackGenerator.Parser.cs | 34 ++++++-- .../MemoryPack.Tests/CircularReferenceTest.cs | 83 +++++++++++++++++-- .../Models/CircularReference.cs | 21 +++-- .../GeneratorDiagnosticsTest.cs | 2 +- .../IncrementalGeneratorTest.cs | 21 +++++ 8 files changed, 266 insertions(+), 26 deletions(-) create mode 100644 src/MemoryPack.Core/UnsafePropertyUpdater.cs create mode 100644 src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs diff --git a/src/MemoryPack.Core/UnsafePropertyUpdater.cs b/src/MemoryPack.Core/UnsafePropertyUpdater.cs new file mode 100644 index 00000000..88273a42 --- /dev/null +++ b/src/MemoryPack.Core/UnsafePropertyUpdater.cs @@ -0,0 +1,61 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace MemoryPack; + +public static class UnsafePropertyUpdater +{ + private static readonly ConcurrentDictionary<(Type Type, string FieldName), Delegate> FieldGetterCache = + new ConcurrentDictionary<(Type, string), Delegate>(); + + public static Func GetOrCreateFieldGetter(string backingFieldName) + { + var key = (typeof(T), backingFieldName); + if (FieldGetterCache.TryGetValue(key, out Delegate? cached)) + { + return (Func)cached; + } + else + { + Func getter = CreateFieldGetter(backingFieldName); + FieldGetterCache.TryAdd(key, getter); + return getter; + } + } + + public static Func CreateFieldGetter(string backingFieldName) + { + FieldInfo? field = typeof(T).GetField(backingFieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field is null) + { + throw new InvalidOperationException($"Field '{backingFieldName}' not found on type {typeof(T)}."); + } + + var dm = new DynamicMethod( + "GetFieldAddress", + typeof(IntPtr), + new Type[] { typeof(T) }, + typeof(T), + true); + + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldflda, field); + il.Emit(OpCodes.Conv_I); + il.Emit(OpCodes.Ret); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + + public static unsafe void SetInitOnlyProperty(T instance, string propertyName, TValue newValue) + where T : class + { + string backingFieldName = $"<{propertyName}>k__BackingField"; + Func getter = GetOrCreateFieldGetter(backingFieldName); + IntPtr fieldPtr = getter(instance); + + Unsafe.WriteUnaligned(fieldPtr.ToPointer(), newValue); + } +} diff --git a/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs b/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs new file mode 100644 index 00000000..a95f9470 --- /dev/null +++ b/src/MemoryPack.Generator/FullyQualifiedNameRewriter.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MemoryPack.Generator; + +internal class FullyQualifiedNameRewriter : CSharpSyntaxRewriter +{ + private readonly SemanticModel _semanticModel; + + public FullyQualifiedNameRewriter(SemanticModel semanticModel) + { + _semanticModel = semanticModel; + } + + public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) + { + var typeInfo = _semanticModel.GetTypeInfo(node.Type); + var typeSymbol = typeInfo.Type ?? _semanticModel.GetSymbolInfo(node.Type).Symbol as ITypeSymbol; + if (typeSymbol != null) + { + var fullyQualifiedName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + var newTypeSyntax = SyntaxFactory.ParseTypeName(fullyQualifiedName) + .WithLeadingTrivia(node.Type.GetLeadingTrivia()) + .WithTrailingTrivia(node.Type.GetTrailingTrivia()); + + node = node.WithType(newTypeSyntax); + } + + return base.VisitObjectCreationExpression(node); + } +} diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs index d17735fa..ccdb266a 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs @@ -471,6 +471,10 @@ private string EmitDeserializeBody() var commentOutInvalidBody = ""; var circularReferenceBody = ""; var circularReferenceBody2 = ""; + var instanceCreationBody = Members.Any(x => x.IsRequired) ? $"({TypeName})global::System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof({TypeName}))" : EmitConstructor(); + var membersDeclaration = Members + .Where(x => x.Symbol != null) + .Select(x => $" {x.MemberType.FullyQualifiedToString()} __{x.Name}" + (!string.IsNullOrEmpty(x.DefaultInitializer) ? $" = {x.DefaultInitializer};" : ";")).NewLine(); if (isVersionTolerant) { @@ -509,7 +513,7 @@ private string EmitDeserializeBody() id = reader.ReadVarIntUInt32(); if (value == null) { - value = new {{TypeName}}(); + value = {{instanceCreationBody }}; } reader.OptionalState.AddObjectReference(id, value); """; @@ -524,7 +528,7 @@ private string EmitDeserializeBody() {{circularReferenceBody}} {{readBeginBody}} {{circularReferenceBody2}} -{{Members.Where(x => x.Symbol != null).Select(x => $" {x.MemberType.FullyQualifiedToString()} __{x.Name};").NewLine()}} +{{membersDeclaration}} {{(!isVersionTolerant ? "" : "var readCount = " + count + ";")}} if (count == {{count}}) @@ -579,7 +583,11 @@ private string EmitDeserializeBody() SET: {{(!IsUseEmptyConstructor ? "goto NEW;" : "")}} + /* {{Members.Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}} +{{Members.Where(x => x.IsInitOnly).Select(x => $" global::MemoryPack.UnsafePropertyUpdater.SetInitOnlyProperty<{TypeName}, {x.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{x.Name}), __{x.Name});").NewLine()}} + */ +{{Members.Where(x => x.IsAssignable || x.IsInitOnly).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}{EmitPropertyAssignValue(x)}").NewLine()}} goto READ_END; NEW: @@ -957,7 +965,8 @@ string EmitDeserializeConstruction(string indent) { // all value is deserialized, __Name is exsits. return string.Join("," + Environment.NewLine, Members - .Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false }) + .Where(x => (x.IsSettable && !x.IsConstructorParameter && !x.SuppressDefaultInitialization) || x.IsRequired) + //.Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false }) .Select(x => $"{indent}@{x.Name} = __{x.Name}")); } @@ -967,13 +976,31 @@ string EmitDeserializeConstructionWithBranching(string indent) .Select((x, i) => (x, i)) .Where(v => v.x.SuppressDefaultInitialization); + //var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference + // ? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};") + // : members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};"); + var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference - ? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};") - : members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};"); + ? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) {EmitPropertyAssignValue(v.x)}") + : members.Select(v => $"{indent}if ({v.i + 1} <= count) {EmitPropertyAssignValue(v.x)}"); return lines.NewLine(); } + string EmitPropertyAssignValue(MemberMeta meta) + { + if (meta.IsAssignable) + { + return $"value.@{meta.Name} = __{meta.Name};"; + } + else if (meta.IsInitOnly) + { + return $"global::MemoryPack.UnsafePropertyUpdater.SetInitOnlyProperty<{TypeName}, {meta.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{meta.Name}), __{meta.Name});"; + } + + throw new InvalidOperationException($"Unable to emit property assignation expression on non-assignable member {meta.Name}"); + } + string EmitUnionTemplate(IGeneratorContext context) { var classOrInterfaceOrRecord = IsRecord ? "record" : (Symbol.TypeKind == TypeKind.Interface) ? "interface" : "class"; diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs index 0806cd14..5f046808 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs @@ -383,8 +383,8 @@ public bool Validate(TypeDeclarationSyntax syntax, IGeneratorContext context, bo } else if (item is { SuppressDefaultInitialization: true, IsAssignable: false }) { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name)); - noError = false; + //context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name)); + //noError = false; } } } @@ -620,9 +620,12 @@ partial class MemberMeta public bool IsField { get; } public bool IsProperty { get; } public bool IsSettable { get; } + public bool IsRequired { get; } public bool IsAssignable { get; } + public bool IsInitOnly { get; } public bool IsConstructorParameter { get; } public string? ConstructorParameterName { get; } + public string? DefaultInitializer { get; } public int Order { get; } public bool HasExplicitOrder { get; } public MemberKind Kind { get; } @@ -665,6 +668,25 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r this.IsConstructorParameter = false; } + var equalsSyntax = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() switch + { + PropertyDeclarationSyntax property => property.Initializer, + VariableDeclaratorSyntax variable => variable.Initializer, + _ => null + }; + + if (equalsSyntax is not null) + { + var syntaxTree = equalsSyntax.SyntaxTree; + var semanticModel = references.Compilation.GetSemanticModel(syntaxTree); + + var rewrittenInitializer = new FullyQualifiedNameRewriter(semanticModel) + .Visit(equalsSyntax.Value); + + DefaultInitializer = rewrittenInitializer.ToString(); + } + //DefaultInitializer = equalsSyntax?.Value.ToString(); + if (symbol is IFieldSymbol f) { IsProperty = false; @@ -682,11 +704,9 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r IsProperty = true; IsField = false; IsSettable = !p.IsReadOnly; - IsAssignable = IsSettable -#if !ROSLYN3 - && !p.IsRequired -#endif - && (p.SetMethod != null && !p.SetMethod.IsInitOnly); + IsRequired = p.IsRequired; + IsInitOnly = p.SetMethod is not null && p.SetMethod.IsInitOnly; + IsAssignable = IsSettable && (p.SetMethod != null && !p.SetMethod.IsInitOnly); MemberType = p.Type; } else diff --git a/tests/MemoryPack.Tests/CircularReferenceTest.cs b/tests/MemoryPack.Tests/CircularReferenceTest.cs index b9b1f17d..8d34d4e0 100644 --- a/tests/MemoryPack.Tests/CircularReferenceTest.cs +++ b/tests/MemoryPack.Tests/CircularReferenceTest.cs @@ -1,13 +1,7 @@ #pragma warning disable CS8602 using MemoryPack.Tests.Models; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Text.Json; -using System.Threading.Tasks; namespace MemoryPack.Tests; @@ -151,4 +145,81 @@ public void Sequential() tylerDeserialized?.DirectReports?[0].Manager.Should().BeSameAs(tylerDeserialized); } + [Fact] + public void RequiredProperties() + { + CircularReferenceWithRequiredProperties manager = new() + { + FirstName = "Tyler", + LastName = "Stein", + Manager = null, + DirectReports = [] + }; + + CircularReferenceWithRequiredProperties emp1 = new() + { + FirstName = "Adrian", + LastName = "King", + Manager = manager, + DirectReports = [] + }; + CircularReferenceWithRequiredProperties emp2 = new() + { + FirstName = "Ben", + LastName = "Aston", + Manager = manager, + DirectReports = [] + }; + CircularReferenceWithRequiredProperties emp3 = new() + { + FirstName = "Emily", + LastName = "Ottoline", + Manager = emp2, + DirectReports = [] + }; + CircularReferenceWithRequiredProperties emp4 = new() + { + FirstName = "Jaymes", + LastName = "Jaiden", + Manager = emp2, + DirectReports = [] + }; + manager.DirectReports = [emp1, emp2]; + emp2.DirectReports = [emp3, emp4]; + + + var bin = MemoryPackSerializer.Serialize(manager); + + CircularReferenceWithRequiredProperties? deserialized = MemoryPackSerializer.Deserialize(bin); + + deserialized.Should().NotBeNull(); + deserialized!.FirstName.Should().Be("Tyler"); + deserialized.LastName.Should().Be("Stein"); + deserialized.Manager.Should().BeNull(); + deserialized.DirectReports.Should().HaveCount(2); + + var dEmp1 = deserialized.DirectReports[0]; + dEmp1.FirstName.Should().Be("Adrian"); + dEmp1.LastName.Should().Be("King"); + dEmp1.Manager.Should().BeSameAs(deserialized); + dEmp1.DirectReports.Should().BeEmpty(); + + var dEmp2 = deserialized.DirectReports[1]; + dEmp2.FirstName.Should().Be("Ben"); + dEmp2.LastName.Should().Be("Aston"); + dEmp2.Manager.Should().BeSameAs(deserialized); + dEmp2.DirectReports.Should().HaveCount(2); + + var dEmp3 = dEmp2.DirectReports[0]; + dEmp3.FirstName.Should().Be("Emily"); + dEmp3.LastName.Should().Be("Ottoline"); + dEmp3.Manager.Should().BeSameAs(dEmp2); + dEmp3.DirectReports.Should().BeEmpty(); + + var dEmp4 = dEmp2.DirectReports[1]; + dEmp4.FirstName.Should().Be("Jaymes"); + dEmp4.LastName.Should().Be("Jaiden"); + dEmp4.Manager.Should().BeSameAs(dEmp2); + dEmp4.DirectReports.Should().BeEmpty(); + } } diff --git a/tests/MemoryPack.Tests/Models/CircularReference.cs b/tests/MemoryPack.Tests/Models/CircularReference.cs index 3de88b04..701b7e70 100644 --- a/tests/MemoryPack.Tests/Models/CircularReference.cs +++ b/tests/MemoryPack.Tests/Models/CircularReference.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace MemoryPack.Tests.Models; @@ -44,8 +40,6 @@ public partial class Employee public List? DirectReports { get; set; } } - - [MemoryPackable(GenerateType.CircularReference, SerializeLayout.Sequential)] public partial class SequentialCircularReference { @@ -53,3 +47,16 @@ public partial class SequentialCircularReference public SequentialCircularReference? Manager { get; set; } public List? DirectReports { get; set; } } + +[MemoryPackable(GenerateType.CircularReference)] +public partial class CircularReferenceWithRequiredProperties +{ + [MemoryPackOrder(0)] + public required string FirstName { get; init; } + [MemoryPackOrder(1)] + public required string LastName { get; set; } + [MemoryPackOrder(2)] + public CircularReferenceWithRequiredProperties? Manager { get; set; } + [MemoryPackOrder(3)] + public required List DirectReports { get; set; } +} diff --git a/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs b/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs index 9ba79cb2..69b0106d 100644 --- a/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs +++ b/tests/MemoryPack.Tests/SourceGeneratorTests/GeneratorDiagnosticsTest.cs @@ -627,7 +627,7 @@ public partial class Tester """); } - [Fact] + [Fact(Skip = "Obsolete")] public void MEMPACK040_SuppressDefaultInitializationMustBeSettable() { Compile(40, """ diff --git a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs index 87f6a9eb..b1234edd 100644 --- a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs @@ -12,6 +12,27 @@ public class IncrementalGeneratorTest public void Run() { // lang=C#-test + + //TODO: this is just while WIP + var code = """ + using System.Collections.ObjectModel; + using MemoryPack; + + [MemoryPackable] + public partial class CollectionTest + { + public Collection Collection { get; } = new Collection(); + + [MemoryPackOnSerializing] + void OnSerializing2() + { + Console.WriteLine(nameof(OnSerializing2)); + } + } + + """; + var k = string.Join(Environment.NewLine, CSharpGeneratorRunner.RunGenerator(code).Item1.SyntaxTrees.Select(x => x.ToString())); + var step1 = """ [MemoryPackable] public partial class MyClass From e01b70068dbf752a15be7582d2626b468dba44c3 Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Tue, 18 Feb 2025 22:18:42 +0200 Subject: [PATCH 2/3] Use expressions to update init-only property to ensure native aot compliance --- src/MemoryPack.Core/PropertyHelper.cs | 39 ++++++++++++ src/MemoryPack.Core/UnsafePropertyUpdater.cs | 61 ------------------- .../MemoryPackGenerator.Emitter.cs | 4 +- tests/MemoryPack.Tests/GeneratorTest.cs | 16 +++++ .../Models/CircularReference.cs | 2 +- tests/MemoryPack.Tests/Models/Records.cs | 25 ++++++++ .../IncrementalGeneratorTest.cs | 20 +++--- 7 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 src/MemoryPack.Core/PropertyHelper.cs delete mode 100644 src/MemoryPack.Core/UnsafePropertyUpdater.cs diff --git a/src/MemoryPack.Core/PropertyHelper.cs b/src/MemoryPack.Core/PropertyHelper.cs new file mode 100644 index 00000000..532fd65a --- /dev/null +++ b/src/MemoryPack.Core/PropertyHelper.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace MemoryPack; + +public static class PropertyHelper +{ + private static readonly ConcurrentDictionary<(Type Type, string FieldName), Delegate> _cache = new(); + + public static void SetInitOnlyProperty(T instance, string propertyName, TValue newValue) + { + Type typeOfT = typeof(T); + var key = (typeOfT, propertyName); + if (!_cache.TryGetValue(key, out Delegate? setter)) + { + var prop = typeOfT.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (prop is null) + { + throw new ArgumentException($"Property \"{propertyName}\" not found in type \"{typeOfT}\""); + } + + var setMethod = prop.GetSetMethod(true); + if (setMethod is null) + { + throw new ArgumentException($"Property \"{propertyName}\" does not have a setter."); + } + + var instanceParameter = Expression.Parameter(typeOfT, "instance"); + var valueParameter = Expression.Parameter(typeof(TValue), "value"); + var callExpr = Expression.Call(instanceParameter, setMethod, valueParameter); + var lambda = Expression.Lambda>(callExpr, instanceParameter, valueParameter); + setter = lambda.Compile(); + _cache.TryAdd(key, setter); + } + + ((Action)setter).Invoke(instance, newValue); + } +} diff --git a/src/MemoryPack.Core/UnsafePropertyUpdater.cs b/src/MemoryPack.Core/UnsafePropertyUpdater.cs deleted file mode 100644 index 88273a42..00000000 --- a/src/MemoryPack.Core/UnsafePropertyUpdater.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Concurrent; -using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.CompilerServices; - -namespace MemoryPack; - -public static class UnsafePropertyUpdater -{ - private static readonly ConcurrentDictionary<(Type Type, string FieldName), Delegate> FieldGetterCache = - new ConcurrentDictionary<(Type, string), Delegate>(); - - public static Func GetOrCreateFieldGetter(string backingFieldName) - { - var key = (typeof(T), backingFieldName); - if (FieldGetterCache.TryGetValue(key, out Delegate? cached)) - { - return (Func)cached; - } - else - { - Func getter = CreateFieldGetter(backingFieldName); - FieldGetterCache.TryAdd(key, getter); - return getter; - } - } - - public static Func CreateFieldGetter(string backingFieldName) - { - FieldInfo? field = typeof(T).GetField(backingFieldName, BindingFlags.Instance | BindingFlags.NonPublic); - if (field is null) - { - throw new InvalidOperationException($"Field '{backingFieldName}' not found on type {typeof(T)}."); - } - - var dm = new DynamicMethod( - "GetFieldAddress", - typeof(IntPtr), - new Type[] { typeof(T) }, - typeof(T), - true); - - ILGenerator il = dm.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldflda, field); - il.Emit(OpCodes.Conv_I); - il.Emit(OpCodes.Ret); - - return (Func)dm.CreateDelegate(typeof(Func)); - } - - public static unsafe void SetInitOnlyProperty(T instance, string propertyName, TValue newValue) - where T : class - { - string backingFieldName = $"<{propertyName}>k__BackingField"; - Func getter = GetOrCreateFieldGetter(backingFieldName); - IntPtr fieldPtr = getter(instance); - - Unsafe.WriteUnaligned(fieldPtr.ToPointer(), newValue); - } -} diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs index ccdb266a..389074b3 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs @@ -585,7 +585,7 @@ private string EmitDeserializeBody() {{(!IsUseEmptyConstructor ? "goto NEW;" : "")}} /* {{Members.Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}} -{{Members.Where(x => x.IsInitOnly).Select(x => $" global::MemoryPack.UnsafePropertyUpdater.SetInitOnlyProperty<{TypeName}, {x.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{x.Name}), __{x.Name});").NewLine()}} +{{Members.Where(x => x.IsInitOnly).Select(x => $" global::MemoryPack.PropertyHelper.SetInitOnlyProperty<{TypeName}, {x.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{x.Name}), __{x.Name});").NewLine()}} */ {{Members.Where(x => x.IsAssignable || x.IsInitOnly).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}{EmitPropertyAssignValue(x)}").NewLine()}} goto READ_END; @@ -995,7 +995,7 @@ string EmitPropertyAssignValue(MemberMeta meta) } else if (meta.IsInitOnly) { - return $"global::MemoryPack.UnsafePropertyUpdater.SetInitOnlyProperty<{TypeName}, {meta.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{meta.Name}), __{meta.Name});"; + return $"global::MemoryPack.PropertyHelper.SetInitOnlyProperty<{TypeName}, {meta.MemberType.FullyQualifiedToString()}>(value, nameof({TypeName}.{meta.Name}), __{meta.Name});"; } throw new InvalidOperationException($"Unable to emit property assignation expression on non-assignable member {meta.Name}"); diff --git a/tests/MemoryPack.Tests/GeneratorTest.cs b/tests/MemoryPack.Tests/GeneratorTest.cs index cb388931..8b667277 100644 --- a/tests/MemoryPack.Tests/GeneratorTest.cs +++ b/tests/MemoryPack.Tests/GeneratorTest.cs @@ -133,7 +133,23 @@ public void Records() VerifyEquivalent(new IncludesReferenceStruct { X = 9, Y = "foobarbaz" }); #if NET7_0_OR_GREATER VerifyEquivalent(new RequiredType { MyProperty1 = 10, MyProperty2 = "hogemogehuga" }); + VerifyEquivalent(new RequiredInitOnlyType + { + MyProperty1 = 10, + MyProperty2 = "hogemogehuga", + MyProperty3 = "qwerty", + MyProperty4 = "property4", + MyProperty5 = "property5" + }); VerifyEquivalent(new RequiredType2 { MyProperty1 = 10, MyProperty2 = "hogemogehuga" }); + VerifyEquivalent(new RequiredInitOnlyType2 + { + MyProperty1 = 10, + MyProperty2 = "hogemogehuga", + MyProperty3 = "qwerty", + MyProperty4 = "property4", + MyProperty5 = "property5" + }); #endif VerifyEquivalent(new StructWithConstructor1("foo")); VerifyEquivalent(new MyRecord(10, 20, "haa")); diff --git a/tests/MemoryPack.Tests/Models/CircularReference.cs b/tests/MemoryPack.Tests/Models/CircularReference.cs index 701b7e70..38b58a53 100644 --- a/tests/MemoryPack.Tests/Models/CircularReference.cs +++ b/tests/MemoryPack.Tests/Models/CircularReference.cs @@ -56,7 +56,7 @@ public partial class CircularReferenceWithRequiredProperties [MemoryPackOrder(1)] public required string LastName { get; set; } [MemoryPackOrder(2)] - public CircularReferenceWithRequiredProperties? Manager { get; set; } + public CircularReferenceWithRequiredProperties? Manager { get; init; } [MemoryPackOrder(3)] public required List DirectReports { get; set; } } diff --git a/tests/MemoryPack.Tests/Models/Records.cs b/tests/MemoryPack.Tests/Models/Records.cs index d5f5b48a..2a9b6657 100644 --- a/tests/MemoryPack.Tests/Models/Records.cs +++ b/tests/MemoryPack.Tests/Models/Records.cs @@ -30,6 +30,16 @@ public partial class RequiredType public required string MyProperty2 { get; set; } } +[MemoryPackable] +public partial class RequiredInitOnlyType +{ + public required int MyProperty1 { get; init; } + public required string MyProperty2 { get; init; } + public required string MyProperty3 { get; set; } + public string? MyProperty4 { get; set; } + public string? MyProperty5 { get; init; } +} + [MemoryPackable] public partial struct RequiredType2 { @@ -42,6 +52,21 @@ public void F() } } +[MemoryPackable] +public partial struct RequiredInitOnlyType2 +{ + public required int MyProperty1 { get; init; } + public required string MyProperty2 { get; init; } + public required string MyProperty3 { get; set; } + public string? MyProperty4 { get; set; } + public string? MyProperty5 { get; init; } + + public void F() + { + // new MyRecord() + } +} + #endif [MemoryPackable] diff --git a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs index b1234edd..789e9001 100644 --- a/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs +++ b/tests/MemoryPack.Tests/SourceGeneratorTests/IncrementalGeneratorTest.cs @@ -18,16 +18,18 @@ public void Run() using System.Collections.ObjectModel; using MemoryPack; - [MemoryPackable] - public partial class CollectionTest + + [MemoryPackable(GenerateType.CircularReference)] + public partial class CircularReferenceWithRequiredProperties { - public Collection Collection { get; } = new Collection(); - - [MemoryPackOnSerializing] - void OnSerializing2() - { - Console.WriteLine(nameof(OnSerializing2)); - } + [MemoryPackOrder(0)] + public required string FirstName { get; init; } + [MemoryPackOrder(1)] + public required string LastName { get; set; } + [MemoryPackOrder(2)] + public CircularReferenceWithRequiredProperties? Manager { get; init; } + [MemoryPackOrder(3)] + public required List DirectReports { get; set; } } """; From 36193b5b89412dde4c62c100a34434f2b4ec4a3e Mon Sep 17 00:00:00 2001 From: Alexandr Ivanov Date: Tue, 18 Feb 2025 22:47:59 +0200 Subject: [PATCH 3/3] Use CreateDelegate instead of Expression in SetInitOnlyProperty --- src/MemoryPack.Core/PropertyHelper.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/MemoryPack.Core/PropertyHelper.cs b/src/MemoryPack.Core/PropertyHelper.cs index 532fd65a..24e9aae9 100644 --- a/src/MemoryPack.Core/PropertyHelper.cs +++ b/src/MemoryPack.Core/PropertyHelper.cs @@ -23,14 +23,15 @@ public static void SetInitOnlyProperty(T instance, string propertyNam var setMethod = prop.GetSetMethod(true); if (setMethod is null) { - throw new ArgumentException($"Property \"{propertyName}\" does not have a setter."); + throw new ArgumentException($"Property \"{propertyName}\" does not have a setter."); } - var instanceParameter = Expression.Parameter(typeOfT, "instance"); - var valueParameter = Expression.Parameter(typeof(TValue), "value"); - var callExpr = Expression.Call(instanceParameter, setMethod, valueParameter); - var lambda = Expression.Lambda>(callExpr, instanceParameter, valueParameter); - setter = lambda.Compile(); + setter = setMethod.CreateDelegate(typeof(Action)); + //var instanceParameter = Expression.Parameter(typeOfT, "instance"); + //var valueParameter = Expression.Parameter(typeof(TValue), "value"); + //var callExpr = Expression.Call(instanceParameter, setMethod, valueParameter); + //var lambda = Expression.Lambda>(callExpr, instanceParameter, valueParameter); + //setter = lambda.Compile(); _cache.TryAdd(key, setter); }