From b0fe18288a9742df80e5c4f7403796bc4e1282da Mon Sep 17 00:00:00 2001 From: jvbsl Date: Sun, 10 Dec 2023 15:59:30 +0100 Subject: [PATCH 1/2] Added runtime type serialization --- DEMO/DEMO.csproj | 6 +- DEMO/DynamicTypes.cs | 2 +- DEMO/Program.cs | 14 +- DEMO/RuntimeTypeTest.cs | 269 +++++++++++ DEMO/SUTMessage.cs | 31 +- .../NonSucking.Framework.Extension.csproj | 2 +- .../AssemblyNameCache.cs | 81 ++++ .../BaseGenerator.cs | 258 +++++++++++ .../BaseGeneratorContext.cs | 169 +++++++ .../Consts.cs | 14 + .../DeserializeGenerating.cs | 42 ++ .../DeserializeGenerator.cs | 69 +++ .../DeserializeGeneratorContext.cs | 26 ++ .../Helper.cs | 286 ++++++++++++ .../IgnoresAccessChecksToAttribute.cs | 15 + .../MethodResolver.cs | 227 ++++++++++ .../NonSucking.Advanced.props | 6 + ...ng.Framework.Serialization.Advanced.csproj | 5 +- .../NullabilityHelper.cs | 56 +++ .../SerializeGenerating.cs | 37 ++ .../SerializeGenerator.cs | 60 +++ .../SerializeGeneratorContext.cs | 25 ++ .../Serializers/ArraySerializer.cs | 259 +++++++++++ .../Serializers/CustomMethodCallSerializer.cs | 18 + .../Serializers/EnumSerializer.cs | 42 ++ .../Serializers/KnownSimpleTypeSerializer.cs | 363 +++++++++++++++ .../Serializers/ListSerializer.cs | 272 ++++++++++++ .../Serializers/MethodCallSerializer.cs | 51 +++ .../Serializers/NullableSerializer.cs | 131 ++++++ .../Serializers/PublicPropertySerializer.cs | 319 ++++++++++++++ .../Serializers/RecursiveCallSerializer.cs | 27 ++ .../Serializers/SpecialTypeSerializer.cs | 44 ++ .../Serializers/UnmanagedTypeSerializer.cs | 37 ++ .../TypeNameCache.cs | 86 ++++ .../TypeSerializerException.cs | 12 + .../Diagnostics.cs | 14 +- .../Extensions.cs | 19 + NonSucking.Framework.Serialization/Helper.cs | 83 +++- .../NonSucking.Framework.Serialization.csproj | 2 +- .../NoosonGenerator.cs | 15 +- .../Properties/launchSettings.json | 2 +- .../Serializers/CtorSerializer.cs | 44 +- .../Serializers/CustomMethodCallSerializer.cs | 2 +- .../Serializers/DynamicTypeSerializer.cs | 417 ++++++++++++++++-- .../Serializers/MethodCallSerializer.cs | 6 +- .../Serializers/NullableSerializer.cs | 10 +- .../Serializers/PublicPropertySerializer.cs | 10 +- .../Serializers/TypeConverterSerializer.cs | 7 +- .../NoosonRuntimeTypeResolver.cs | 325 ++++++++++++++ .../Attribute/NoosonDynamicTypeAttribute.cs | 58 ++- .../NoosonRuntimeTypeResolverTemplate.cs | 12 + .../Templates/Template.cs | 6 + 52 files changed, 4282 insertions(+), 111 deletions(-) create mode 100644 DEMO/RuntimeTypeTest.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/AssemblyNameCache.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/BaseGenerator.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/BaseGeneratorContext.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Consts.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/DeserializeGenerating.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/DeserializeGenerator.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/DeserializeGeneratorContext.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Helper.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/IgnoresAccessChecksToAttribute.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/MethodResolver.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/NonSucking.Advanced.props create mode 100644 NonSucking.Framework.Serialization.Advanced/NullabilityHelper.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/SerializeGenerating.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/SerializeGenerator.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/SerializeGeneratorContext.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/ArraySerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/CustomMethodCallSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/EnumSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/KnownSimpleTypeSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/ListSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/MethodCallSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/NullableSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/PublicPropertySerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/RecursiveCallSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/SpecialTypeSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/Serializers/UnmanagedTypeSerializer.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/TypeNameCache.cs create mode 100644 NonSucking.Framework.Serialization.Advanced/TypeSerializerException.cs create mode 100644 NonSucking.Framework.Serialization/Extensions.cs create mode 100644 NonSucking.Framework.Serialization/Templates/AdditionalSource/NoosonRuntimeTypeResolver.cs create mode 100644 NonSucking.Framework.Serialization/Templates/NoosonRuntimeTypeResolverTemplate.cs diff --git a/DEMO/DEMO.csproj b/DEMO/DEMO.csproj index 89a32d4..480b2a2 100644 --- a/DEMO/DEMO.csproj +++ b/DEMO/DEMO.csproj @@ -1,5 +1,6 @@  + Exe net7.0 @@ -16,11 +17,12 @@ - + + + - diff --git a/DEMO/DynamicTypes.cs b/DEMO/DynamicTypes.cs index 38f23c0..5ae71c7 100644 --- a/DEMO/DynamicTypes.cs +++ b/DEMO/DynamicTypes.cs @@ -33,7 +33,7 @@ public partial class D : B private int Bab { get; set; } [NoosonDynamicType(typeof(int), typeof(string))] - public object SomeOther { get; set; } + public object? SomeOther { get; set; } } // // void Test(System.IO.BinaryReader br) diff --git a/DEMO/Program.cs b/DEMO/Program.cs index 3ccd5d9..a438c98 100644 --- a/DEMO/Program.cs +++ b/DEMO/Program.cs @@ -5,7 +5,11 @@ using System.Collections.Generic; using System.Drawing; using System.IO; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; using NonSucking.Framework.Serialization; +using NonSucking.Framework.Serialization.Advanced; using static DEMO.SUTMessage; namespace DEMO @@ -28,6 +32,8 @@ static void Main(string[] args) bll.NameOfList = "ABC"; var conv = Newtonsoft.Json.JsonConvert.SerializeObject(bll, new JsonSerializerSettings { }); + var containerType = new ContainingClass() + { RuntimeValue = new RuntimeTypeTestA("huhu") { SomeValue = 1234 } }; var sutMessage = new SUTMessage() { AlternativUser = new User { Name = "Okay" }, @@ -58,7 +64,9 @@ static void Main(string[] args) Type = 897987414, UsersList = new List() { new User() { Name = "1User" }, new User() { Name = "2User" }, new User() { Name = "3User" }, new User() { Name = "4User" } }, X = 123123, - UnmanagedTypes = new() { SomePos = new Point(1, 2), SomeTime = DateTime.Parse("2022-02-22 12:34")} + UnmanagedTypes = new() { SomePos = new Point(1, 2), SomeTime = DateTime.Parse("2022-02-22 12:34")}, + RuntimeValue = containerType, + GenericRuntimeValue = new Generic() { Prop = new[] { containerType, new ContainingClass() { RuntimeValue = new RuntimeTypeTestB(12) { SomeValue = 12345} }} } }; //var sut = new SinglePropTest @@ -69,13 +77,13 @@ static void Main(string[] args) //}; using (var ms = new FileStream("sut.save", FileMode.OpenOrCreate)) { - using var bw = new BinaryWriter(ms); + using var bw = new NoosonBinaryWriter(ms); sutMessage.Serialize(bw); } using (var ms = new FileStream("sut.save", FileMode.Open)) { - using var br = new BinaryReader(ms); + using var br = new NoosonBinaryReader(ms); var sutMessageDes = SUTMessage.Deserialize(br); if (sutMessageDes == sutMessage) diff --git a/DEMO/RuntimeTypeTest.cs b/DEMO/RuntimeTypeTest.cs new file mode 100644 index 0000000..aec7fd7 --- /dev/null +++ b/DEMO/RuntimeTypeTest.cs @@ -0,0 +1,269 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.InteropServices; +using NonSucking.Framework.Serialization; + +namespace DEMO; + +[Nooson] +public partial class RuntimeTypeTestBase : IEquatable +{ + public int SomeValue { get; set; } + + public bool Equals(RuntimeTypeTestBase? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + return false; + + return SomeValue == other.SomeValue; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((RuntimeTypeTestBase)obj); + } + + public override int GetHashCode() + { + return SomeValue; + } +} + +[Nooson] +public partial class RuntimeTypeTestA : RuntimeTypeTestBase, IEquatable +{ + public RuntimeTypeTestA(string someOtherValue) + { + SomeOtherValue = someOtherValue; + } + + public string SomeOtherValue { get; } + + public bool Equals(RuntimeTypeTestA? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + if (ReferenceEquals(this, other)) + { + return true; + } + return SomeOtherValue == other.SomeOtherValue && SomeValue == other.SomeValue; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((RuntimeTypeTestA)obj); + } + + public override int GetHashCode() + { + return SomeOtherValue.GetHashCode(); + } +} + +[Nooson] +public partial class Generic : IEquatable> +{ + [NoosonDynamicType(Resolver = typeof(UnknownTypeResolver))] + public T[] Prop { get; set; } + + + public bool Equals(Generic? other) + { + if (other is null) + return false; + return Prop.SequenceEqual(other.Prop); + } +} + +[Nooson] +public partial class RuntimeTypeTestB : RuntimeTypeTestBase, IEquatable +{ + public RuntimeTypeTestB(int someOtherValue) + { + SomeOtherValue = someOtherValue; + } + + public int SomeOtherValue { get; } + + public bool Equals(RuntimeTypeTestB? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return SomeOtherValue == other.SomeOtherValue && SomeValue == other.SomeValue; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((RuntimeTypeTestB)obj); + } + + public override int GetHashCode() + { + return SomeOtherValue; + } +} + +[Nooson] +public partial class RuntimeTypeTestC : RuntimeTypeTestBase +{ + public RuntimeTypeTestC(string someOtherValue) + { + SomeOtherValue = someOtherValue; + } + + public string SomeOtherValue { get; } +} + +[Nooson] +public partial class ContainingClass : IEquatable +{ + [NoosonDynamicType(typeof(RuntimeTypeTestC), Resolver = typeof(Resolver))] + public RuntimeTypeTestBase RuntimeValue { get; set; } + + public bool Equals(ContainingClass? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return RuntimeValue.Equals(other.RuntimeValue); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((ContainingClass)obj); + } + + public override int GetHashCode() + { + return RuntimeValue.GetHashCode(); + } +} + +internal class UnknownTypeResolver : NoosonRuntimeTypeResolver +{ + public static UnknownTypeResolver Instance = new(); + protected override Type ResolveType(string identifier) + { + return Type.GetType(identifier) ?? throw new ArgumentException(); + } + + protected override string SolveType(Type type) + { + return type.AssemblyQualifiedName ?? throw new ArgumentException(); + } +} + + +partial struct SomeWrapperStruct +{ + public SomeWrapperStruct(T identifier) + { + Identifier = identifier; + } + + public T Identifier { get; } +} +internal class Resolver : NoosonRuntimeTypeResolver> +{ + public static Resolver Instance = new(); + protected override Type ResolveType(SomeWrapperStruct identifier) + { + return Type.GetType(identifier.Identifier?.ToString() ?? "") ?? throw new ArgumentOutOfRangeException(); + } + + protected override SomeWrapperStruct SolveType(Type type) + { + var val = type.AssemblyQualifiedName + ?? type.FullName + ?? throw new ArgumentOutOfRangeException(); + return new SomeWrapperStruct((T)(object)val); + } +} \ No newline at end of file diff --git a/DEMO/SUTMessage.cs b/DEMO/SUTMessage.cs index 561d987..2c707d3 100644 --- a/DEMO/SUTMessage.cs +++ b/DEMO/SUTMessage.cs @@ -56,17 +56,17 @@ public ComplainBaseWithCtor(string complain, string valueNever) this.valueNever = valueNever; } - public void FirstSerialize(BinaryWriter bw) + public void FirstSerialize(IBinaryWriter bw) { bw.Write(FirstCustom); } - public static void FirstSerialize(BinaryWriter bw, string first) + public static void FirstSerialize(IBinaryWriter bw, string first) { bw.Write(first); } - public static string FirstDeserialize(BinaryReader br) + public static string FirstDeserialize(IBinaryReader br) { return br.ReadString(); } @@ -111,6 +111,9 @@ public partial class SUTMessage : IEquatable public int X { get; set; } public int countPositions { get; set; } + + public ContainingClass RuntimeValue { get; set; } + public Generic GenericRuntimeValue { get; set; } [NoosonInclude] private int randomField = new Random().Next(); @@ -147,6 +150,7 @@ public static IUser DeserializeIUser(BinaryReader br) } public override bool Equals(object obj) => Equals(obj as SUTMessage); + public bool Equals(SUTMessage other) => other is not null && Positions.SequenceEqual(other.Positions) @@ -170,7 +174,9 @@ other is not null && countPositions == other.countPositions && randomField == other.randomField && forCtor == other.forCtor - && UnmanagedTypes == other.UnmanagedTypes; + && UnmanagedTypes == other.UnmanagedTypes + && RuntimeValue.Equals(other.RuntimeValue) + && GenericRuntimeValue.Equals(other.GenericRuntimeValue); /* EqualityComparer>.Default.Equals(UsersList, other.UsersList) && EqualityComparer>.Default.Equals(ComplainsBases, other.ComplainsBases) && EqualityComparer>.Default.Equals(Countings, other.Countings) && EqualityComparer>.Default.Equals(CountingDic, other.CountingDic) && EqualityComparer>.Default.Equals(ReadOnlyCountings, other.ReadOnlyCountings) && EqualityComparer>.Default.Equals(ReadOnlyCountingsButSetable, other.ReadOnlyCountingsButSetable) */ @@ -199,6 +205,8 @@ public override int GetHashCode() hash.Add(countPositions); hash.Add(randomField); hash.Add(forCtor); + hash.Add(RuntimeValue); + hash.Add(GenericRuntimeValue); return hash.ToHashCode(); } @@ -211,25 +219,30 @@ public class User : IUser, IEquatable public int DoSomething() => 12; - public void Serialize(BinaryWriter writer) + public void Serialize(T writer) + where T : IBinaryWriter { writer.Write(Name); } - public void SerializeMe(BinaryWriter bw) + public void SerializeMe(T bw) + where T : IBinaryWriter { bw.Write(Name); } - public static void SerializeMe(BinaryWriter bw, IUser user) + public static void SerializeMe(T bw, IUser user) + where T : IBinaryWriter { bw.Write(user.Name); } - public static User Deserialize(BinaryReader reader) + public static User Deserialize(T reader) + where T : IBinaryReader { return new User() { Name = reader.ReadString() }; } - public static User DeserializeMe(BinaryReader reader) + public static User DeserializeMe(T reader) + where T : IBinaryReader { return new User() { Name = reader.ReadString() }; } diff --git a/NonSucking.Framework.Extension/NonSucking.Framework.Extension.csproj b/NonSucking.Framework.Extension/NonSucking.Framework.Extension.csproj index 59c9e80..b94fc6c 100644 --- a/NonSucking.Framework.Extension/NonSucking.Framework.Extension.csproj +++ b/NonSucking.Framework.Extension/NonSucking.Framework.Extension.csproj @@ -1,6 +1,6 @@  - + netcoreapp3.1;net6.0 false diff --git a/NonSucking.Framework.Serialization.Advanced/AssemblyNameCache.cs b/NonSucking.Framework.Serialization.Advanced/AssemblyNameCache.cs new file mode 100644 index 0000000..1a7596b --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/AssemblyNameCache.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class AssemblyNameCache +{ + internal readonly struct Names + { + private readonly string? serializeName; + private readonly string? deserializeName; + private readonly string? deserializeOutName; + private const string DefaultSerializeName = "Serialize"; + private const string DefaultDeserializeName = "Deserialize"; + public Names(string? serializeName, string? deserializeName, string? deserializeOutName) + { + this.serializeName = serializeName; + this.deserializeName = deserializeName; + this.deserializeOutName = deserializeOutName; + } + + public static Names Combine(Names a, Names b) + { + return new Names(a.serializeName ?? b.serializeName, a.deserializeName ?? b.deserializeName, a.deserializeOutName ?? b.deserializeOutName); + } + + public string SerializeName => serializeName ?? DefaultSerializeName; + + public string DeserializeName => deserializeName ?? DefaultDeserializeName; + public string DeserializeOutName => deserializeOutName ?? DefaultDeserializeName; + } + private static readonly Dictionary AssemblyNameMap = new(); + + internal static T? FirstOrNull(IEnumerable enumerable, Func predicate) + where T : struct + { + foreach (var v in enumerable) + { + if (predicate(v)) + return v; + } + return null; + } + private static Names GetAssemblyConfig(Assembly assembly) + { + var assemblyAttr = assembly.GetCustomAttributesData().FirstOrDefault( + x => x.AttributeType.FullName == Consts.NoosonConfigurationAttribute); + if (assemblyAttr is null) + return new Names(null, null, null); + var deserializeName = FirstOrNull(assemblyAttr.NamedArguments, + argument => argument.MemberName == "NameOfStaticDeserializeWithCtor")?.TypedValue.Value?.ToString(); + var deserializeOutName = FirstOrNull(assemblyAttr.NamedArguments, + argument => argument.MemberName == "NameOfStaticDeserializeWithOutParams")?.TypedValue.Value?.ToString(); + var serializeName = FirstOrNull(assemblyAttr.NamedArguments, + argument => argument.MemberName == "NameOfSerialize")?.TypedValue.Value?.ToString(); + return new Names(serializeName, deserializeName, deserializeOutName); + } + public static Names ResolveAssemblyConfig(Assembly assembly) + { +#if NET6_0_OR_GREATER + ref var names = ref CollectionsMarshal.GetValueRefOrAddDefault(AssemblyNameMap, assembly, out var exists); + if (!exists) + { + names = GetAssemblyConfig(assembly); + } + return names; +#else + if (!AssemblyNameMap.TryGetValue(assembly, out var names)) + { + names = GetAssemblyConfig(assembly); + AssemblyNameMap.Add(assembly, names); + } + return names; +#endif + } + + +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/BaseGenerator.cs b/NonSucking.Framework.Serialization.Advanced/BaseGenerator.cs new file mode 100644 index 0000000..ff46630 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/BaseGenerator.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class BaseGenerator +{ + public BaseGenerator(BaseGeneratorContext currentContext) + { + CurrentContext = currentContext; + } + + public void PushContext(BaseGeneratorContext context) + { + Contexts.Push(context); + CurrentContext = context; + } + + public BaseGeneratorContext PopContext() + { + var popped = Contexts.Pop(); + CurrentContext = Contexts.Peek(); + return popped; + } + public void EmitCall(MethodInfo method) + { + Il.Emit(method.IsStatic ? OpCodes.Call : OpCodes.Callvirt, method); + } + public void EmitIfInverted(OpCode invertedBranch, Action? ifScope, Action? elseScope) + { + EmitIf(InvertBranch(invertedBranch), elseScope, ifScope); + } + + public void EmitIf(OpCode branchCode, Action? ifScope, + Action? elseScope) + { + var elseLabel = elseScope is null ? (Label?)null : Il.DefineLabel(); + var endLabel = Il.DefineLabel(); + Il.Emit(branchCode, elseLabel ?? endLabel); + ifScope?.Invoke(this); + if (elseLabel is not null) + { + if (ifScope != null) + Il.Emit(OpCodes.Br, endLabel); + Il.MarkLabel(elseLabel.Value); + elseScope?.Invoke(this); + } + Il.MarkLabel(endLabel); + } + public void EmitIf(Action? elseScope, params (OpCode branchCode, Action check, Action body)[] ifs) + { + if (ifs.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(ifs)); + } + + var nested = elseScope; + + for (int i = ifs.Length - 1; i >= 0; i--) + { + var (elifBranchCode, elifCheck, elifBody) = ifs[i]; + var nestedCopy = nested; + nested = gen => + { + elifCheck(gen); + gen.EmitIf(elifBranchCode, elifBody, nestedCopy); + }; + } + + nested!(this); + } + public void EmitIf(OpCode branchCode, Action? ifScope, + Action? elseScope, params (OpCode branchCode, Action check, Action body)[] elseIfs) + { + var nested = elseScope; + + for (int i = elseIfs.Length - 1; i >= 0; i--) + { + var (elifBranchCode, elifCheck, elifBody) = elseIfs[i]; + var nestedCopy = nested; + nested = gen => + { + elifCheck(gen); + gen.EmitIf(elifBranchCode, elifBody, nestedCopy); + }; + } + + EmitIf(branchCode, ifScope, + gen => + { + nested?.Invoke(gen); + } + ); + } + + public void EmitFor(Func init, Action check, Action increment, Action body) + { + var forLoopStart = Il.DefineLabel(); + var forLoopEnd = Il.DefineLabel(); + var variable = init(this); + Il.MarkLabel(forLoopStart); + check(this, variable); + Il.Emit(OpCodes.Brfalse, forLoopEnd); + body(this, forLoopStart, forLoopEnd, variable); + increment(this, variable); + Il.Emit(OpCodes.Br, forLoopStart); + Il.MarkLabel(forLoopEnd); + } + + public void EmitWhile(Action check, Action body) + { + var loopStart = Il.DefineLabel(); + var loopEnd = Il.DefineLabel(); + Il.MarkLabel(loopStart); + check(this); + Il.Emit(OpCodes.Brfalse, loopEnd); + body(this, loopStart, loopEnd); + Il.Emit(OpCodes.Br, loopStart); + Il.MarkLabel(loopEnd); + } + + public void EmitDo(Action check, Action body) + { + var loopStart = Il.DefineLabel(); + var loopEnd = Il.DefineLabel(); + Il.MarkLabel(loopStart); + body(this, loopStart, loopEnd); + check(this); + Il.Emit(OpCodes.Brtrue, loopStart); + Il.MarkLabel(loopEnd); + } + + public void EmitTryCatchFinally(Action body, Action? finalBlock, params (Type? filter, Action bodyGenerator)[]? exceptionBlocks) + { + var exBlock = Il.BeginExceptionBlock(); + var exitLabel = Il.DefineLabel(); + body(this, exBlock); + if (exceptionBlocks is not null && exceptionBlocks.Length > 0) + { + if (exceptionBlocks.Length > 1) + Il.BeginExceptFilterBlock(); + foreach (var (filter, bodyGenerator) in exceptionBlocks) + { + Il.BeginCatchBlock(filter); + bodyGenerator(this, exBlock); + } + if (finalBlock is not null) + Il.BeginFinallyBlock(); + } + else + Il.BeginFinallyBlock(); + + finalBlock?.Invoke(this, exitLabel); + + Il.MarkLabel(exitLabel); + Il.EndExceptionBlock(); + } + + public void EmitIncrement() + { + Il.Emit(OpCodes.Ldc_I4_1); + Il.Emit(OpCodes.Add); + } + + public void EmitIncrement(LocalBuilder var) + { + Il.Emit(OpCodes.Ldloc, var); + EmitIncrement(); + Il.Emit(OpCodes.Stloc, var); + } + + public void EmitForEach(Type enumerableType, Action getEnumerable, Action body) + { + var getEnumerator = Helper.GetMethodIncludingInterfaces(enumerableType, "GetEnumerator", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (getEnumerator is null) + throw new ArgumentException($"Not a valid enumerable type(no GetEnumerator found): '{enumerableType.FullName}'"); + var enumeratorType = getEnumerator.ReturnType; + var moveNext = Helper.GetMethodIncludingInterfaces(enumeratorType, "MoveNext", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (moveNext is null || moveNext.ReturnType != typeof(bool)) + throw new ArgumentException($"Not a valid enumerator type(no valid MoveNext returning bool found): '{enumeratorType.FullName}'"); + var getCurrent = Helper.GetPropertyIncludingInterfaces(enumeratorType, "Current", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)?.GetMethod; + if (getCurrent is null) + throw new ArgumentException( + $"Not a valid enumerator(no valid Current get method found): '{enumeratorType.FullName}'"); + var dispose = Helper.GetMethodIncludingInterfaces(enumeratorType, "Dispose", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (dispose is null) + throw new ArgumentException( + $"Not a valid enumerator(no valid Dispose method found): '{enumeratorType.FullName}'"); + var enumeratorVariable = Il.DeclareLocal(enumeratorType); + var currentVariable = Il.DeclareLocal(getCurrent.ReturnType); + getEnumerable(this); + Il.Emit(OpCodes.Callvirt, getEnumerator); + Il.Emit(OpCodes.Stloc, enumeratorVariable); + EmitTryCatchFinally( + (_, _) => + { + EmitWhile( + gen => + { + gen.EmitLoadLocRef(enumeratorVariable); + gen.Il.Emit(OpCodes.Callvirt, moveNext); + }, + (gen, loopStart, loopEnd) => + { + gen.EmitLoadLocRef(enumeratorVariable); + gen.Il.Emit(OpCodes.Callvirt, getCurrent); + gen.Il.Emit(OpCodes.Stloc, currentVariable); + body(gen, loopStart, loopEnd, currentVariable); + } + ); + }, + (gen, exitLabel) => + { + // if (!enumeratorVariable.LocalType.IsValueType) + // { + // gen.EmitLoadLocRef(enumeratorVariable); + // gen.IL.Emit(OpCodes.Brfalse, exitLabel); + // } + // + // gen.EmitLoadLocRef(enumeratorVariable); + // gen.IL.Emit(OpCodes.Callvirt, dispose); + }); + } + + private void EmitLoadLocRef(LocalBuilder var) + { + Il.Emit(var.LocalType.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc, var); + } + + public static MethodInfo GetDelegateInvoker(Type type) + { + var invoke = type.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); + if (invoke is null) + throw new ArgumentException($"No valid invoke method found for type: {type.FullName}"); + return invoke; + } + private static OpCode InvertBranch(OpCode code) + { + if (code == OpCodes.Brfalse_S) + return OpCodes.Brtrue_S; + if (code == OpCodes.Brfalse) + return OpCodes.Brtrue; + if (code == OpCodes.Brtrue_S) + return OpCodes.Brfalse_S; + if (code == OpCodes.Brtrue) + return OpCodes.Brfalse; + throw new ArgumentOutOfRangeException(nameof(code)); + } + + public Dictionary ContextMap { get; } = new(); + public BaseGeneratorContext CurrentContext { get; private set; } + public ILGenerator Il => CurrentContext.Il; + public Stack Contexts { get; } = new(); + public bool IsTopLevel => CurrentContext.IsTopLevel; + +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/BaseGeneratorContext.cs b/NonSucking.Framework.Serialization.Advanced/BaseGeneratorContext.cs new file mode 100644 index 0000000..f051066 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/BaseGeneratorContext.cs @@ -0,0 +1,169 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace NonSucking.Framework.Serialization.Advanced; + +public abstract class BaseGeneratorContext +{ + public enum ContextType + { + Serialize, + Deserialize + } + public readonly TypeBuilder? typeBuilder; + public class TypeContext : IEquatable + { + public TypeContext(Type type, NullabilityInfo nullabilityInfo) + { + Type = type; + NullabilityInfo = nullabilityInfo; + } + + public Type Type { get; } + public NullabilityInfo NullabilityInfo { get; } + + public bool Equals(TypeContext? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Type == other.Type && Equals(NullabilityInfo, other.NullabilityInfo); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((TypeContext)obj); + } + + private static bool Equals(NullabilityInfo a, NullabilityInfo b) + { + if (a.Type != b.Type || + a.ReadState != b.ReadState || + a.WriteState != b.WriteState) + return false; + + if (a.ElementType is not null && b.ElementType is not null) + { + if (!Equals(a.ElementType, b.ElementType)) + return false; + } + else if(a.ElementType != b.ElementType) + { + return false; + } + + if (a.GenericTypeArguments.Length != b.GenericTypeArguments.Length) + return false; + + for (int i = 0; i < a.GenericTypeArguments.Length; i++) + { + if (!Equals(a.GenericTypeArguments[i], b.GenericTypeArguments[i])) + return false; + } + + return true; + } + + private static int GetHashCode(NullabilityInfo a) + { + return HashCode.Combine(a.Type.GetHashCode(), a.ReadState.GetHashCode(), a.WriteState.GetHashCode()); + } + + public override int GetHashCode() + { + return HashCode.Combine(Type.GetHashCode(), GetHashCode(NullabilityInfo)); + } + } + public BaseGeneratorContext(TypeContext valueType, ContextType contextType, + Action getReaderWriter, bool isTopLevel, MethodBuilder method) + { + if (contextType != ContextType.Serialize && contextType != ContextType.Deserialize) + throw new ArgumentOutOfRangeException(nameof(contextType)); + ValueType = valueType; + Type = contextType; + GetReaderWriter = getReaderWriter; + IsTopLevel = isTopLevel; + Method = method; + Il = Method.GetILGenerator(); + } + public BaseGeneratorContext(Type readerWriterType, TypeContext valueType, ContextType contextType, Action getReaderWriter, bool isTopLevel) + { + if (contextType != ContextType.Serialize && contextType != ContextType.Deserialize) + throw new ArgumentOutOfRangeException(nameof(contextType)); + + ValueType = valueType; + Type = contextType; + GetReaderWriter = getReaderWriter; + IsTopLevel = isTopLevel; + var methodName = contextType == ContextType.Serialize ? "Serialize" : "Deserialize"; + var retType = contextType == ContextType.Serialize ? null : valueType.Type; + var paramTypes = contextType == ContextType.Serialize + ? new[] { readerWriterType, valueType.Type } + : new[] { readerWriterType }; + + var assemblyName = new AssemblyName($"{valueType.Type.ToAssemblyNameString()}{readerWriterType}"); + var assemblyBuilder = + AssemblyBuilder.DefineDynamicAssembly(assemblyName, + AssemblyBuilderAccess.Run); + // Add IgnoresAccessChecksTo attribute + + var ignoreAccessChecksToCtor = typeof(IgnoresAccessChecksToAttribute).GetConstructor(new[] { typeof(string) }) + ?? throw new InvalidProgramException( + $"{nameof(IgnoresAccessChecksToAttribute)} constructor with string argument is missing!"); + + var entryAssemblyName = Assembly.GetEntryAssembly()?.GetName().Name + ?? throw new InvalidOperationException("Could not resolve entry assembly name"); + var callingAssemblyName = Assembly.GetCallingAssembly().GetName().Name + ?? throw new InvalidOperationException("Could not resolve calling assembly name"); + + var ignoresAccessChecksTo = new CustomAttributeBuilder + ( + ignoreAccessChecksToCtor, + new object[] { entryAssemblyName } + ); + + assemblyBuilder.SetCustomAttribute(ignoresAccessChecksTo); + ignoresAccessChecksTo = new CustomAttributeBuilder + ( + ignoreAccessChecksToCtor, + new object[] { callingAssemblyName } + ); + + assemblyBuilder.SetCustomAttribute(ignoresAccessChecksTo); + var mod = assemblyBuilder.DefineDynamicModule(assemblyName.Name!); + var typeBuilderLocal = mod.DefineType("SerializeStuff"); + typeBuilder = typeBuilderLocal; + Method = typeBuilderLocal.DefineMethod(methodName, + MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, + retType, paramTypes); + Il = Method.GetILGenerator(); + AssemblyBuilder = assemblyBuilder; + } +public AssemblyBuilder AssemblyBuilder { get; } + public Type Build() + { + if (typeBuilder is null) + throw new InvalidOperationException("Cannot Build type for already existent method"); + return typeBuilder.CreateType(); + } + + public Action GetReaderWriter { get; } + public Action? GetValue { get; set; } + public Action? GetValueRef { get; set; } + public Action? SetValue { get; set; } + + public ILGenerator Il { get; } + + public MethodBuilder Method { get; } + + public TypeContext ValueType { get; } + public ContextType Type { get; } + public bool IsTopLevel { get; } + + public abstract BaseGeneratorContext SubContext(TypeContext valueType, bool? isTopLevel); +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Consts.cs b/NonSucking.Framework.Serialization.Advanced/Consts.cs new file mode 100644 index 0000000..3b45a9b --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Consts.cs @@ -0,0 +1,14 @@ +namespace NonSucking.Framework.Serialization.Advanced; + +public static class Consts +{ + internal const string NoosonNamespace = "NonSucking.Framework.Serialization"; + internal const string NoosonConfigurationAttribute = $"{NoosonNamespace}.NoosonConfigurationAttribute"; + internal const string NoosonCustomAttribute = $"{NoosonNamespace}.NoosonCustomAttribute"; + internal const string NoosonIgnoreAttribute = $"{NoosonNamespace}.NoosonIgnoreAttribute"; + internal const string NoosonIncludeAttribute = $"{NoosonNamespace}.NoosonIncludeAttribute"; + internal const string NoosonOrderAttribute = $"{NoosonNamespace}.NoosonOrderAttribute"; + internal const string NoosonParameterAttribute = $"{NoosonNamespace}.NoosonParameterAttribute"; + internal const string NoosonPreferredCtorAttribute = $"{NoosonNamespace}.NoosonPreferredCtorAttribute"; + internal const string LocalVariableSuffix = "_️"; //VARIATION SELECTOR-16 _, looks better and provides better uniqueness +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/DeserializeGenerating.cs b/NonSucking.Framework.Serialization.Advanced/DeserializeGenerating.cs new file mode 100644 index 0000000..53c8c42 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/DeserializeGenerating.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace NonSucking.Framework.Serialization.Advanced; + +public static class DeserializeGenerating +{ + public static MethodInfo GenerateDeserialize(FieldInfo prop) + { + return GenerateDeserialize(prop.FieldType, NullabilityHelper.Context.Create(prop)); + } + + public static MethodInfo GenerateDeserialize(PropertyInfo prop) + { + return GenerateDeserialize(prop.PropertyType, NullabilityHelper.Context.Create(prop)); + } + + + public static MethodInfo GenerateDeserialize(Type type, bool isNullable) + { + return GenerateDeserialize(type, NullabilityHelper.CreateNullable(type, isNullable)); + } + + private static MethodInfo GenerateDeserialize(Type type, NullabilityInfo nullability) + { + var gen = new DeserializeGenerator(typeof(TReader), TypeNameCache.GetTypeConfig(type), + new BaseGeneratorContext.TypeContext(type, nullability), + BaseGeneratorContext.ContextType.Deserialize); + var generatedContext = gen.CurrentContext; + + var generatedType = generatedContext.Build(); + + return generatedType.GetMethod("Deserialize", BindingFlags.Public | BindingFlags.Static)!; + } + public static Delegate GenerateDeserializeDelegate(Type type, bool isNullable) + { + var m = GenerateDeserialize(type, isNullable); + var reader = Expression.Parameter(typeof(TReader), "reader"); + return Expression.Lambda(Expression.Call(m, reader), reader).Compile(); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/DeserializeGenerator.cs b/NonSucking.Framework.Serialization.Advanced/DeserializeGenerator.cs new file mode 100644 index 0000000..0f7ccd5 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/DeserializeGenerator.cs @@ -0,0 +1,69 @@ +using System; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class DeserializeGenerator : BaseGenerator +{ + public DeserializeGenerator(Type readerType, TypeNameCache.TypeConfig typeConfig, BaseGeneratorContext.TypeContext valueType, BaseGeneratorContext.ContextType contextType) + : base(new DeserializeGeneratorContext(readerType, valueType, gen => gen.Emit(OpCodes.Ldarg_0), true)) + { + if (contextType == BaseGeneratorContext.ContextType.Serialize) + { + CurrentContext.GetValue = gen => gen.Emit(OpCodes.Ldarg_1); + CurrentContext.GetValueRef = valueType.Type.IsValueType + ? gen => gen.Emit(OpCodes.Ldarga, (short)1) + : CurrentContext.GetValue; + } + else + { + var resVal = CurrentContext.Il.DeclareLocal(valueType.Type); + CurrentContext.SetValue = gen => gen.Emit(OpCodes.Stloc, resVal); + CurrentContext.GetValue = gen => gen.Emit(OpCodes.Ldloc, resVal); + CurrentContext.GetValueRef = valueType.Type.IsValueType + ? gen => gen.Emit(OpCodes.Ldloca, resVal) + : CurrentContext.GetValue; + } + PushContext(CurrentContext); + if (!Create(typeConfig)) + { + throw new TypeSerializerException($"Unable to create dynamic deserializer for type config: {typeConfig}"); + } + } + + public bool GenerateDeserialize(DeserializeGeneratorContext context, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + ContextMap.TryAdd(context.ValueType, context); + PushContext(context); + ContextMap.TryAdd(context.ValueType, context); + + var res = CustomMethodCallSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + NullableSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + !IsTopLevel && RecursiveCallSerializer.Deserialize(this, typeConfig) || + SpecialTypeSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + EnumSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + KnownSimpleTypeSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + UnmanagedTypeSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + MethodCallSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + ArraySerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + ListSerializer.Deserialize(this, typeConfig, baseRecursionDepth) || + PublicPropertySerializer.Deserialize(this, typeConfig, baseRecursionDepth); + + _ = PopContext(); + return res; + } + + private bool Create(TypeNameCache.TypeConfig typeConfig) + { + var res = GenerateDeserialize(CurrentContext, typeConfig); + if (!res) + return false; + CurrentContext.GetValue!(Il); + Il.Emit(OpCodes.Ret); + while (Contexts.Count > 1) + _ = PopContext(); + return res; + } + + public new DeserializeGeneratorContext CurrentContext => (DeserializeGeneratorContext)base.CurrentContext; +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/DeserializeGeneratorContext.cs b/NonSucking.Framework.Serialization.Advanced/DeserializeGeneratorContext.cs new file mode 100644 index 0000000..4974cbf --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/DeserializeGeneratorContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +public class DeserializeGeneratorContext : BaseGeneratorContext +{ + public DeserializeGeneratorContext(Type readerType, TypeContext valueType, Action getReaderWriter, bool isTopLevel, MethodBuilder method) + : base(valueType, ContextType.Deserialize, getReaderWriter, isTopLevel, method) + { + ReaderType = readerType; + } + + public DeserializeGeneratorContext(Type readerType, TypeContext valueType, Action getReaderWriter, bool isTopLevel) + : base(readerType, valueType, ContextType.Deserialize, getReaderWriter, isTopLevel) + { + ReaderType = readerType; + } + + public Type ReaderType { get; } + + public override DeserializeGeneratorContext SubContext(TypeContext valueType, bool? isTopLevel) + { + return new DeserializeGeneratorContext(ReaderType, valueType, GetReaderWriter, isTopLevel ?? IsTopLevel, Method); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Helper.cs b/NonSucking.Framework.Serialization.Advanced/Helper.cs new file mode 100644 index 0000000..8a40fb6 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Helper.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace NonSucking.Framework.Serialization.Advanced; + +public static class Helper +{ + private static readonly MethodInfo GenericMethodUnmanagedCheck = + typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.IsReferenceOrContainsReferences), BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidProgramException($"{nameof(RuntimeHelpers)} does not contain a {nameof(RuntimeHelpers.IsReferenceOrContainsReferences)} method"); + public static bool IsUnmanaged(this Type t) + { + try + { + return !(bool)GenericMethodUnmanagedCheck.MakeGenericMethod(t).Invoke(null,null)!; + } + catch (Exception) + { + return false; + } + } + internal static bool ForAll(this IEnumerable list, IList second, Func check) + { + int index = 0; + foreach (var item in list) + { + if (second.Count > index) + { + if (!check(item, second[index])) + return false; + } + else + return false; + index++; + } + + return second.Count == index; + } + public static bool IsAssignableFromOpenGeneric(this Type type, Type assignTo) + { + return IsAssignableToOpenGeneric(assignTo, type); + } + public static bool IsAssignableToOpenGeneric(this Type type, Type assignTo) + { + var interfaces = type.GetInterfaces(); + + foreach (var i in interfaces) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == assignTo) + return true; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == assignTo) + return true; + + return type.BaseType is { } baseType && IsAssignableToOpenGeneric(baseType, assignTo); + } + + public static Type? GetOpenGenericType(this Type type, Type generic) + { + Type? currentType = type; + while (currentType is not null) + { + var interfaces = currentType.GetInterfaces(); + + foreach (var i in interfaces) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == generic) return i; + } + + if (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == generic) + return currentType; + + currentType = currentType.BaseType; + } + + return null; + } + + public static string ToAssemblyNameString(this Type type) + { + return type.ToString().Replace(',', '.'); + } + public static string GetSpecialTypeReadMethodName(TypeCode code) + { + switch (code) + { + case TypeCode.Boolean: + return "ReadBoolean"; + case TypeCode.Char: + return "ReadChar"; + case TypeCode.SByte: + return "ReadSByte"; + case TypeCode.Byte: + return "ReadByte"; + case TypeCode.Int16: + return "ReadInt16"; + case TypeCode.UInt16: + return "ReadUInt16"; + case TypeCode.Int32: + return "ReadInt32"; + case TypeCode.UInt32: + return "ReadUInt32"; + case TypeCode.Int64: + return "ReadInt64"; + case TypeCode.UInt64: + return "ReadUInt64"; + case TypeCode.Single: + return "ReadSingle"; + case TypeCode.Double: + return "ReadDouble"; + case TypeCode.Decimal: + return "ReadDecimal"; + case TypeCode.String: + return "ReadString"; + case TypeCode.Empty: + case TypeCode.Object: + case TypeCode.DBNull: + case TypeCode.DateTime: + default: + break; + } + + throw new ArgumentOutOfRangeException(nameof(code), code, null); + } + internal static MethodInfo? TryGetRead(DeserializeGenerator generator, Type type, string methodName) + { + var readerType = generator.CurrentContext.ReaderType; + return MethodResolver.GetBestMatch(readerType, + methodName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + Type.EmptyTypes, type); + } + internal static MethodInfo? TryGetRead(DeserializeGenerator generator, Type type, TypeCode typeCode, out string methodName) + { + methodName = GetSpecialTypeReadMethodName(typeCode); + return TryGetRead(generator, type, methodName); + } + internal static MethodInfo? TryGetRead(DeserializeGenerator generator, Type type, out string methodName) + { + return TryGetRead(generator, type, Type.GetTypeCode(type), out methodName); + } + + internal static MethodInfo? TryGetRead(DeserializeGenerator generator, Type type, TypeCode typeCode) + => TryGetRead(generator, type, typeCode, out _); + + internal static MethodInfo? TryGetRead(DeserializeGenerator generator, Type type) + => TryGetRead(generator, type, out _); + internal static MethodInfo GetRead(DeserializeGenerator generator, Type type, TypeCode typeCode) + { + var readerType = generator.CurrentContext.ReaderType; + return TryGetRead(generator, type, typeCode, out var methodName) + ?? throw new NotSupportedException($"Reader {readerType} is missing {methodName}({type}) overload."); + } + internal static MethodInfo GetRead(DeserializeGenerator generator, Type type) + { + var readerType = generator.CurrentContext.ReaderType; + return TryGetRead(generator, type, out var methodName) + ?? throw new NotSupportedException($"Reader {readerType} is missing {methodName}({type}) overload."); + } + internal static MethodInfo GetRead(DeserializeGenerator generator, Type type, string methodName) + { + var readerType = generator.CurrentContext.ReaderType; + return TryGetRead(generator, type, methodName) + ?? throw new NotSupportedException($"Reader {readerType} is missing {methodName}({type}) overload."); + } + + + internal static MethodInfo? TryGetWrite(SerializeGenerator generator, Type type, string methodName = "Write") + { + var writerType = generator.CurrentContext.WriterType; + return MethodResolver.GetBestMatch( + writerType, + methodName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, + new[] { type }, null); + } + internal static MethodInfo GetWrite(SerializeGenerator generator, Type type, string methodName = "Write") + { + var writerType = generator.CurrentContext.WriterType; + return TryGetWrite(generator, type, methodName) + ?? throw new NotSupportedException($"Writer {writerType} is missing {methodName}({type}) overload."); + } + internal static T? GetFirstMemberWithBase(Type? type, + Func predicate, + int maxRecursion = int.MaxValue, + int currentIteration = 0) + { + if (type is null) + return default; + if (currentIteration++ > maxRecursion) + return default; + + foreach (var member in type.GetMembers()) + { + if (member is T t && predicate(t)) + return t; + } + + return GetFirstMemberWithBase(type.BaseType, predicate, maxRecursion, + currentIteration); + } + internal static IEnumerable<(MemberInfo memberInfo, int depth)> GetMembersWithBase(Type? type, + int maxRecursion = int.MaxValue, int currentIteration = 0) + { + if (currentIteration++ > maxRecursion) + yield break; + + if (type is null) + yield break; + foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + if (member.DeclaringType != type) + continue; + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (member.MemberType) + { + // Exclude CompilerGenerated EqualityContract from serialization process + case MemberTypes.Property when member.Name == "EqualityContract" && member.GetCustomAttribute() is not null: + continue; + case MemberTypes.Property: + if (member.GetCustomAttributesData().FirstOrDefault(x => + x.AttributeType.FullName == Consts.NoosonIgnoreAttribute) is not null) + continue; + yield return (member, currentIteration); + break; + case MemberTypes.Field: + if (member.GetCustomAttributesData().FirstOrDefault(x => + x.AttributeType.FullName == Consts.NoosonIncludeAttribute) is null) + continue; + yield return (member, currentIteration); + break; + } + } + + foreach (var item in GetMembersWithBase(type.BaseType, maxRecursion, currentIteration)) + { + yield return item; + } + } + internal static bool MatchIdentifierWithPropName(string identifier, string parameterName) + { + var index = identifier.IndexOf(Consts.LocalVariableSuffix, StringComparison.Ordinal); + if (index > -1) + identifier = identifier.Remove(index); + index = parameterName.IndexOf(Consts.LocalVariableSuffix, StringComparison.Ordinal); + if (index > -1) + parameterName = parameterName.Remove(index); + + return char.ToLowerInvariant(identifier[0]) == char.ToLowerInvariant(parameterName[0]) + && string.Equals(identifier[1..], parameterName[1..]); + } + internal static MethodInfo? GetMethodIncludingInterfaces(Type type, string name, BindingFlags bindingFlags) + { + var res = type.GetMethod(name, bindingFlags); + if (res is not null) + return res; + foreach (var i in type.GetInterfaces()) + { + res = i.GetMethod(name, bindingFlags); + if (res is not null) + return res; + } + + return null; + } + + + internal static PropertyInfo? GetPropertyIncludingInterfaces(Type type, string name, BindingFlags bindingFlags) + { + var res = type.GetProperty(name, bindingFlags); + if (res is not null) + return res; + foreach (var i in type.GetInterfaces()) + { + res = i.GetProperty(name, bindingFlags); + if (res is not null) + return res; + } + + return null; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/IgnoresAccessChecksToAttribute.cs b/NonSucking.Framework.Serialization.Advanced/IgnoresAccessChecksToAttribute.cs new file mode 100644 index 0000000..204f598 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/IgnoresAccessChecksToAttribute.cs @@ -0,0 +1,15 @@ +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class IgnoresAccessChecksToAttribute : Attribute + { + public IgnoresAccessChecksToAttribute(string assemblyName) + { + AssemblyName = assemblyName; + } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public string AssemblyName { get; } + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/MethodResolver.cs b/NonSucking.Framework.Serialization.Advanced/MethodResolver.cs new file mode 100644 index 0000000..5c2912c --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/MethodResolver.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Loader; + +namespace NonSucking.Framework.Serialization.Advanced; + + +internal class MethodResolver +{ + private static readonly Dictionary> RegisteredExtensionMethods = new(); + private static readonly HashSet AlreadyRegistered = new(); + private static readonly HashSet AlreadyRegisteredAssemblies = new(); + + readonly struct AssemblyNameKey : IEquatable + { + public AssemblyNameKey(AssemblyName assemblyName) + { + AssemblyName = assemblyName; + } + + public AssemblyName AssemblyName { get; } + + public bool Equals(AssemblyNameKey other) + { + return AssemblyName.FullName.Equals(other.AssemblyName.FullName); + } + + public override bool Equals(object? obj) + { + return obj is AssemblyNameKey other && Equals(other); + } + + public override int GetHashCode() + { + return AssemblyName.FullName.GetHashCode(); + } + + public static implicit operator AssemblyNameKey(AssemblyName name) + { + return new(name); + } + } + + static MethodResolver() + { + // Default resolve extension methods + Stopwatch st = new(); + st.Start(); + AnalyzeExtensionMethodsRecurse(Assembly.GetCallingAssembly()); + AnalyzeExtensionMethodsRecurse(Assembly.GetEntryAssembly()); + st.Stop(); + Console.WriteLine($"Took {st.ElapsedMilliseconds}ms to load"); + } + internal static IEnumerable GetRegisteredExtensionMethods(string name) + { + return RegisteredExtensionMethods.TryGetValue(name, out var methods) + ? methods + : Array.Empty(); + } + + internal static void AnalyzeExtensionMethods(Type type) + { + if (!type.IsClass || !type.IsSealed || !type.IsAbstract || type.GetCustomAttribute() is null) + return; + + if (!AlreadyRegistered.Add(type)) + return; + + var methods = type.GetMethods(); + Array.Sort(methods, (a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + var previousMethodName = ""; + List resolvedMethods = null!; + foreach (var m in methods) + { + if (m.GetCustomAttribute() is null) + continue; + if (previousMethodName != m.Name) + { + ref var refList = ref CollectionsMarshal.GetValueRefOrAddDefault(RegisteredExtensionMethods, m.Name, out var exists); + if (exists) + resolvedMethods = refList!; + else + refList = resolvedMethods = new List(); + previousMethodName = m.Name; + } + + resolvedMethods.Add(m); + } + } + + internal static void AnalyzeExtensionMethodsRecurse(Assembly? assembly) + { + if (assembly is null) + return; + AnalyzeExtensionMethods(assembly); + foreach (var referencedName in assembly.GetReferencedAssemblies()) + { + if (AlreadyRegisteredAssemblies.Contains(referencedName)) + continue; + var referencedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(referencedName); + AnalyzeExtensionMethodsRecurse(referencedAssembly); + } + } + internal static void AnalyzeExtensionMethods(Assembly assembly) + { + if (!AlreadyRegisteredAssemblies.Add(assembly.GetName())) + return; + foreach (var t in assembly.GetTypes()) + { + AnalyzeExtensionMethods(t); + } + } + private static Type[] MatchGenericArguments(MethodInfo x, ParameterInfo[] parameters, Type[] parameterTypes, Type returnType) + { + // TODO: cannot match generic parameters that are not used as parameter types + var genArguments = x.GetGenericArguments(); + var matchedParameters = new Type[genArguments.Length]; + + void MatchNew(int index, Type newlyMatched) + { + var previouslyMatched = matchedParameters[index]; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (previouslyMatched is null || newlyMatched.IsAssignableFrom(previouslyMatched)) + { + matchedParameters[index] = newlyMatched; + } + } + + for (var index = 0; index < genArguments.Length; index++) + { + var genArg = genArguments[index]; + + if (x.ReturnType == genArg) + { + MatchNew(index, returnType); + } + + for (var pIndex = 0; pIndex < parameters.Length; pIndex++) + { + var genParam = parameters[pIndex]; + if (genParam.ParameterType != genArg) + continue; + var newlyMatched = parameterTypes[pIndex]; + MatchNew(index, newlyMatched); + } + } + + return matchedParameters; + } + + private static IEnumerable SelectMethods(IEnumerable methodInfos, Type[] parameterTypes, Type? returnType) + { + return methodInfos.Select(x => + { + try + { + var parameters = x.GetParameters(); + if (parameters.Length != parameterTypes.Length) + return null; + if (x.IsGenericMethodDefinition) + { + var matched = MatchGenericArguments(x, parameters, parameterTypes, returnType ?? typeof(void)); + var genMethod = x.MakeGenericMethod(matched); + if (genMethod.ReturnType != (returnType ?? typeof(void))) + return null; + return genMethod; + } + + return x; + } + catch (ArgumentException) + { + return null; + } + }).Where(x => x is not null).OfType(); + } + internal static MethodInfo? GetBestMatch(Type type, string methodName, BindingFlags bindingFlags, Type[] parameterTypes, Type? returnType) + { + var methods = SelectMethods(type.GetMethods(bindingFlags) + .Where(x => x.Name == methodName), parameterTypes, returnType).ToArray(); + + if (GetPerfectMatch(parameterTypes, methods) is { } perfectMatch) + return perfectMatch; + + // ReSharper disable once CoVariantArrayConversion + var bestInstanceMethod = methods.Length == 0 ? null : Type.DefaultBinder.SelectMethod(bindingFlags, methods, parameterTypes, null) as MethodInfo; + if (bestInstanceMethod is null) + { + var parameterTypesWithThis = parameterTypes.Prepend(type).ToArray(); + var extMethods = SelectMethods(GetRegisteredExtensionMethods(methodName), parameterTypesWithThis, returnType).ToArray(); + if (GetPerfectMatch(parameterTypesWithThis, extMethods) is { } perfectExtMatch) + return perfectExtMatch; + if (extMethods.Length == 0) + return null; + // ReSharper disable once CoVariantArrayConversion + return Type.DefaultBinder.SelectMethod(bindingFlags, extMethods, parameterTypesWithThis, null) as MethodInfo; + } + + return bestInstanceMethod; + } + + private static MethodInfo? GetPerfectMatch(Type[] parameterTypes, MethodInfo[] methods) + { + var perfectMatch = methods.FirstOrDefault( + x => + { + if (x.IsGenericMethod) + return false; + var parameters = x.GetParameters(); + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].ParameterType != parameterTypes[i]) + { + return false; + } + } + + return true; + }); + return perfectMatch; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/NonSucking.Advanced.props b/NonSucking.Framework.Serialization.Advanced/NonSucking.Advanced.props new file mode 100644 index 0000000..52d66a7 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/NonSucking.Advanced.props @@ -0,0 +1,6 @@ + + + + $(DefineConstants);__NOOSON_ADVANCED_SERIALIZATION__ + + \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/NonSucking.Framework.Serialization.Advanced.csproj b/NonSucking.Framework.Serialization.Advanced/NonSucking.Framework.Serialization.Advanced.csproj index 533e94e..deb0817 100644 --- a/NonSucking.Framework.Serialization.Advanced/NonSucking.Framework.Serialization.Advanced.csproj +++ b/NonSucking.Framework.Serialization.Advanced/NonSucking.Framework.Serialization.Advanced.csproj @@ -1,12 +1,13 @@ - netstandard2.0;netstandard2.1;net7.0 + net7.0 + enable NonSucking.Framework.Serialization 10.0 - + diff --git a/NonSucking.Framework.Serialization.Advanced/NullabilityHelper.cs b/NonSucking.Framework.Serialization.Advanced/NullabilityHelper.cs new file mode 100644 index 0000000..e2753d3 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/NullabilityHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace NonSucking.Framework.Serialization.Advanced; + +public static class NullabilityHelper +{ + public delegate NullabilityInfo NullabilityInfoCreationDelegate( + Type type, + NullabilityState readState, + NullabilityState writeState, + NullabilityInfo? elementType, + NullabilityInfo[] typeArguments); + + public static readonly NullabilityInfoCreationDelegate CreateNullability; + + static NullabilityHelper() + { + var ctorParamTypes = new[] + { + typeof(Type), + typeof(NullabilityState), + typeof(NullabilityState), + typeof(NullabilityInfo), + typeof(NullabilityInfo[]) + }; + var ctor = typeof(NullabilityInfo).GetConstructor( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, + ctorParamTypes) ?? throw new InvalidProgramException($"Could not find constructor for '{nameof(NullabilityInfo)}'"); + + var ctorParams = ctorParamTypes.Select(Expression.Parameter).ToArray(); + + CreateNullability = Expression.Lambda(Expression.New(ctor, ctorParams), ctorParams).Compile(); + } + public static readonly NullabilityInfoContext Context = new(); + internal static NullabilityInfo CreateNullable(Type type, bool isNullable) + { + if (type.IsGenericType && type.IsAssignableToOpenGeneric(typeof(Nullable<>))) + isNullable = true; + + var elementType = type.IsSZArray ? CreateNullable(type.GetElementType()!, false) : null; + var typeArguments = type.IsGenericType + ? type.GenericTypeArguments.Select(x => CreateNullable(x, false)).ToArray() + : Array.Empty(); + + return CreateNullability( + type, + isNullable ? NullabilityState.Nullable : NullabilityState.NotNull, + isNullable ? NullabilityState.Nullable : NullabilityState.NotNull, + elementType, + typeArguments); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/SerializeGenerating.cs b/NonSucking.Framework.Serialization.Advanced/SerializeGenerating.cs new file mode 100644 index 0000000..eb4e2ad --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/SerializeGenerating.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace NonSucking.Framework.Serialization.Advanced; + +public static class SerializeGenerating +{ + public static MethodInfo GenerateSerialize(FieldInfo prop) + { + return GenerateSerialize(prop.FieldType, NullabilityHelper.Context.Create(prop)); + } + public static MethodInfo GenerateSerialize(PropertyInfo prop) + { + return GenerateSerialize(prop.PropertyType, NullabilityHelper.Context.Create(prop)); + } + + public static MethodInfo GenerateSerialize(Type type, bool isNullable) + { + return GenerateSerialize(type, NullabilityHelper.CreateNullable(type, isNullable)); + } + + private static MethodInfo GenerateSerialize(Type type, NullabilityInfo nullability) + { + var gen = new SerializeGenerator(typeof(TWriter), TypeNameCache.GetTypeConfig(type), + new BaseGeneratorContext.TypeContext(type, nullability)); + return gen.CurrentContext.Build().GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static)!; + } + + public static Delegate GenerateSerializeDelegate(Type type, bool isNullable) + { + var m = GenerateSerialize(type, isNullable); + var writer = Expression.Parameter(typeof(TWriter), "writer"); + var value = Expression.Parameter(type, "value"); + return Expression.Lambda(Expression.Call(m, writer, value), writer, value).Compile(); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/SerializeGenerator.cs b/NonSucking.Framework.Serialization.Advanced/SerializeGenerator.cs new file mode 100644 index 0000000..0f6c8a0 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/SerializeGenerator.cs @@ -0,0 +1,60 @@ +using System; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class SerializeGenerator : BaseGenerator +{ + public SerializeGenerator(Type writerType, TypeNameCache.TypeConfig typeConfig, + BaseGeneratorContext.TypeContext valueType) + : base( new SerializeGeneratorContext(writerType, valueType, gen => gen.Emit(OpCodes.Ldarg_0), true)) + { + CurrentContext.GetValue = gen => gen.Emit(OpCodes.Ldarg_1); + CurrentContext.GetValueRef = valueType.Type.IsValueType + ? gen => gen.Emit(OpCodes.Ldarga, (short)1) + : CurrentContext.GetValue; + + PushContext(CurrentContext); + if (!Create(typeConfig)) + { + throw new TypeSerializerException($"Unable to create dynamic serializer for type config: {typeConfig}"); + } + } + + public bool GenerateSerialize(SerializeGeneratorContext context, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + PushContext(context); + + ContextMap.TryAdd(context.ValueType, context); + + var res = CustomMethodCallSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + NullableSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + !IsTopLevel && RecursiveCallSerializer.Serialize(this, typeConfig) || + SpecialTypeSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + EnumSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + KnownSimpleTypeSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + UnmanagedTypeSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + MethodCallSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + ArraySerializer.Serialize(this, typeConfig, baseRecursionDepth) || + ListSerializer.Serialize(this, typeConfig, baseRecursionDepth) || + PublicPropertySerializer.Serialize(this, typeConfig, baseRecursionDepth); + + _ = PopContext(); + return res; + } + + private bool Create(TypeNameCache.TypeConfig typeConfig) + { + var res = GenerateSerialize(CurrentContext, typeConfig); + if (!res) + return false; + Il.Emit(OpCodes.Ret); + + while (Contexts.Count > 1) + _ = PopContext(); + + return res; + } + + public new SerializeGeneratorContext CurrentContext => (SerializeGeneratorContext)base.CurrentContext; +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/SerializeGeneratorContext.cs b/NonSucking.Framework.Serialization.Advanced/SerializeGeneratorContext.cs new file mode 100644 index 0000000..db07455 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/SerializeGeneratorContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +public class SerializeGeneratorContext : BaseGeneratorContext +{ + public SerializeGeneratorContext(Type writerType, TypeContext valueType, Action getReaderWriter, bool isTopLevel, MethodBuilder method) + : base(valueType, ContextType.Serialize, getReaderWriter, isTopLevel, method) + { + WriterType = writerType; + } + + public SerializeGeneratorContext(Type writerType, TypeContext valueType, Action getReaderWriter, bool isTopLevel) + : base(writerType, valueType, ContextType.Serialize, getReaderWriter, isTopLevel) + { + WriterType = writerType; + } + public Type WriterType { get; } + + public override SerializeGeneratorContext SubContext(TypeContext valueType, bool? isTopLevel) + { + return new SerializeGeneratorContext(WriterType, valueType, GetReaderWriter, isTopLevel ?? IsTopLevel, Method); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/ArraySerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/ArraySerializer.cs new file mode 100644 index 0000000..b3e2983 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/ArraySerializer.cs @@ -0,0 +1,259 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class ArraySerializer +{ + private static readonly MethodInfo GetLength = MethodResolver.GetBestMatch(typeof(Array), "GetLength", + BindingFlags.Instance | BindingFlags.Public, + new[] { typeof(int) }, typeof(int)) + ?? throw new ArgumentNullException("Array.GetLength not found", (Exception?)null); + + private static void CreateNestedFor(int rank, T generator, LocalBuilder[] loopVariables, LocalBuilder[] countVariables, Action body) + where T : BaseGenerator + { + var previousMethod = body; + for (int i = rank - 1; i >= 0; i--) + { + var iCopyGen = i; + var previousMethodCopyGen = previousMethod; + previousMethod = gen => + { + var iCopyFor = iCopyGen; + var previousMethodCopyFor = previousMethodCopyGen; + gen.EmitFor( + g => + { + g.Il.Emit(OpCodes.Ldc_I4_0); + g.Il.Emit(OpCodes.Stloc, loopVariables[iCopyFor]); + return loopVariables[iCopyFor]; + }, + (g, var) => + { + g.Il.Emit(OpCodes.Ldloc, var!); + g.Il.Emit(OpCodes.Ldloc, countVariables[iCopyFor]); + g.Il.Emit(OpCodes.Clt); + }, + (g, var) => + { + g.EmitIncrement(var!); + }, + (g, _, _, _) => + { + previousMethodCopyFor((T)g); + }); + }; + } + + previousMethod(generator); + } + + + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType; + if (!type.Type.IsArray) + return false; + var rank = type.Type.GetArrayRank(); + if (rank < 1) + return false; + var elementType = type.Type.GetElementType() + ?? throw new InvalidOperationException($"{type.Type} is an array but array element type could not be resolved"); + + var getterMethod = MethodResolver.GetBestMatch(type.Type, "Get", BindingFlags.Instance | BindingFlags.Public, + Enumerable.Repeat(typeof(int), rank).ToArray(), elementType) + ?? throw new InvalidOperationException($"{type.Type} is an array but get method could not be resolved"); + + var loopVariables = new LocalBuilder[rank]; + var countVariables = new LocalBuilder[rank]; + void ForBody(SerializeGenerator gen) + { + gen.CurrentContext.GetValue!(gen.Il); + for (int i = 0; i < rank; i++) + { + gen.Il.Emit(OpCodes.Ldloc, loopVariables[i]); + } + if (rank == 1) + gen.Il.Emit(OpCodes.Ldelem, elementType); + else + gen.Il.Emit(OpCodes.Callvirt, getterMethod); + var elementVar = gen.Il.DeclareLocal(elementType); + gen.Il.Emit(OpCodes.Stloc, elementVar); + + var subContext = + gen.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(elementType, type.NullabilityInfo.ElementType!), + null); + subContext.GetValue = _ => gen.Il.Emit(OpCodes.Ldloc, elementVar); + subContext.GetValueRef = elementType.IsValueType + ? _ => gen.Il.Emit(OpCodes.Ldloca, elementVar) + : subContext.GetValue; + + var tpConfig = TypeNameCache.GetTypeConfig(elementType); + + _ = gen.GenerateSerialize(subContext, tpConfig); + } + + var writeInt = Helper.GetWrite(generator, typeof(int)); + + for (int i = 0; i < rank; i++) + { + var countVar = generator.Il.DeclareLocal(typeof(int)); + generator.CurrentContext.GetValue!(generator.Il); + if (rank == 1) + { + var length = type.Type.GetProperty("Length")!; + generator.Il.Emit(OpCodes.Callvirt, length.GetMethod!); + } + else + { + generator.Il.Emit(OpCodes.Ldc_I4, i); + generator.Il.Emit(OpCodes.Callvirt, GetLength); + } + generator.Il.Emit(OpCodes.Stloc, countVar); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, countVar); + generator.Il.Emit(OpCodes.Callvirt, writeInt); + + + countVariables[i] = countVar; + + loopVariables[i] = generator.Il.DeclareLocal(typeof(int)); + } + + CreateNestedFor(rank, generator, loopVariables, countVariables, ForBody); + + + return true; + } + + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType; + if (!type.Type.IsArray) + return false; + var rank = type.Type.GetArrayRank(); + if (rank < 1) + return false; + + var elementType = type.Type.GetElementType() + ?? throw new InvalidOperationException($"{type.Type} is an array but array element type could not be resolved"); + + var setterMethod = MethodResolver.GetBestMatch(type.Type, "Set", BindingFlags.Instance | BindingFlags.Public, + Enumerable.Repeat(typeof(int), rank).Append(elementType).ToArray(), null) + ?? throw new InvalidOperationException($"{type.Type} is an array but set method could not be resolved"); + + var loopVariables = new LocalBuilder[rank]; + var countVariables = new LocalBuilder[rank]; + + void ForBody(DeserializeGenerator gen) + { + var subContext = + gen.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(elementType, type.NullabilityInfo.ElementType!), + null); + + var bodyResVar = gen.Il.DeclareLocal(elementType); + subContext.SetValue = g => g.Emit(OpCodes.Stloc, bodyResVar); + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, bodyResVar); + subContext.GetValueRef = elementType.IsValueType + ? g => g.Emit(OpCodes.Ldloca, bodyResVar) + : subContext.GetValue; + + var tpConfig = TypeNameCache.GetTypeConfig(elementType); + _ = gen.GenerateDeserialize(subContext, tpConfig); + + gen.CurrentContext.GetValue!(gen.Il); + for (int i = 0; i < rank; i++) + { + gen.Il.Emit(OpCodes.Ldloc, loopVariables[i]); + } + + gen.Il.Emit(OpCodes.Ldloc, bodyResVar); + if (rank == 1) + gen.Il.Emit(GetStElem(elementType)); + else + gen.Il.Emit(OpCodes.Callvirt, setterMethod); + + } + + // var resArray = generator.IL.DeclareLocal(type); + // generator.CurrentContext.GetValue = (g) => g.Emit(OpCodes.Ldloc, resArray); + // + var readInt = Helper.GetRead(generator, typeof(int)); + + for (int i = 0; i < rank; i++) + { + var countVar = generator.Il.DeclareLocal(typeof(int)); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, readInt); + generator.Il.Emit(OpCodes.Stloc, countVar); + + countVariables[i] = countVar; + + loopVariables[i] = generator.Il.DeclareLocal(typeof(int)); + } + + for (int i = 0; i < rank; i++) + { + generator.Il.Emit(OpCodes.Ldloc, countVariables[i]); + } + + if (rank == 1) + { + generator.Il.Emit(OpCodes.Newarr, elementType); + } + else + { + var arrayCtor = type.Type.GetConstructor(Enumerable.Repeat(typeof(int), rank).ToArray()) + ?? throw new InvalidOperationException($"{type.Type} is an array but constructor could not be resolved"); + + generator.Il.Emit(OpCodes.Newobj, arrayCtor); + } + + generator.CurrentContext.SetValue!(generator.Il); + + + CreateNestedFor(rank, generator, loopVariables, countVariables, ForBody); + // + // generator.CurrentContext.GetValue = g => g.Emit(OpCodes.Ldloc, resArray); + // generator.CurrentContext.GetValue = g => g.Emit(OpCodes.Ldloc, resArray); + + return true; + } + + private static OpCode GetStElem(Type type) + { + if (!type.IsValueType) + return OpCodes.Stelem_Ref; + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (Type.GetTypeCode(type)) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Boolean: + return OpCodes.Stelem_I1; + case TypeCode.Char: + return OpCodes.Stelem_I2; + case TypeCode.Int32: + case TypeCode.UInt32: + return OpCodes.Stelem_I4; + case TypeCode.Int64: + case TypeCode.UInt64: + return OpCodes.Stelem_I8; + case TypeCode.Single: + return OpCodes.Stelem_R4; + case TypeCode.Double: + return OpCodes.Stelem_R8; + } + + if (type == typeof(IntPtr) || type == typeof(UIntPtr)) + return OpCodes.Stelem_I; + + return OpCodes.Stelem; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/CustomMethodCallSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/CustomMethodCallSerializer.cs new file mode 100644 index 0000000..7ece33d --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/CustomMethodCallSerializer.cs @@ -0,0 +1,18 @@ +namespace NonSucking.Framework.Serialization.Advanced; + +internal class CustomMethodCallSerializer +{ + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + if (!typeConfig.IsCustomMethodCall) + return false; + + return MethodCallSerializer.Serialize(generator, typeConfig); + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + if (!typeConfig.IsCustomMethodCall) + return false; + return MethodCallSerializer.Deserialize(generator, typeConfig); + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/EnumSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/EnumSerializer.cs new file mode 100644 index 0000000..07d6f38 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/EnumSerializer.cs @@ -0,0 +1,42 @@ +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class EnumSerializer +{ + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + if (!type.IsEnum) + return false; + var serializerMethod = Helper.TryGetWrite(generator, type.GetEnumUnderlyingType()); + if (serializerMethod is null) + return false; + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValue!(generator.Il); + + generator.Il.Emit(OpCodes.Callvirt, serializerMethod); + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + if (!type.IsEnum) + return false; + var underlyingType = type.GetEnumUnderlyingType(); + var deserializerMethod = Helper.TryGetRead(generator, underlyingType); + if (deserializerMethod is null) + { + return false; + } + + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Call, deserializerMethod); + + generator.CurrentContext.SetValue!(generator.Il); + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/KnownSimpleTypeSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/KnownSimpleTypeSerializer.cs new file mode 100644 index 0000000..c2ec3c3 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/KnownSimpleTypeSerializer.cs @@ -0,0 +1,363 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class KnownSimpleTypeSerializer +{ + private enum KnownTypes + { + None, + IpAddress, + Guid, + BigInteger + } + private static KnownTypes GetKnownType(Type type) + { + if (type == typeof(IPAddress)) + { + return KnownTypes.IpAddress; + } + if (type == typeof(Guid)) + { + return KnownTypes.Guid; + } + if (type == typeof(BigInteger)) + { + return KnownTypes.BigInteger; + } + return KnownTypes.None; + } + + private static void GetIpAddressSize(BaseGenerator generator, LocalBuilder addressFamilyVar) + { + generator.EmitIf(gen => + { + var throwNotSupported = typeof(NotSupportedException).GetConstructor( + BindingFlags.Instance | BindingFlags.Public, + Type.EmptyTypes) + ?? throw new InvalidProgramException("NotSupportedException is missing an empty constructor"); + gen.Il.Emit(OpCodes.Newobj, throwNotSupported); + gen.Il.Emit(OpCodes.Throw); + }, + ( + OpCodes.Bne_Un, + gen => + { + gen.Il.Emit(OpCodes.Ldloc, addressFamilyVar); + gen.Il.Emit(OpCodes.Ldc_I4, (int)AddressFamily.InterNetworkV6); + }, + gen => + { + gen.Il.Emit(OpCodes.Ldc_I4, 16); + } + ), + ( + OpCodes.Bne_Un, + gen => + { + gen.Il.Emit(OpCodes.Ldloc, addressFamilyVar); + gen.Il.Emit(OpCodes.Ldc_I4, (int)AddressFamily.InterNetwork); + }, + gen => + { + gen.Il.Emit(OpCodes.Ldc_I4, 4); + } + ) + ); + } + + private static MethodInfo? GetTryWriteBytes(Type? discardType = null) + { + var paramTypes = discardType is not null ? new[] { typeof(Span), discardType.MakeByRefType() } : new[] { typeof(Span) }; + return typeof(T).GetMethod( + "TryWriteBytes", + BindingFlags.Instance | BindingFlags.Public, + paramTypes); + } + private static MethodInfo? GetTryWriteBytesBigInt(Type discardType) + { + var paramTypes = new[] { typeof(Span), discardType.MakeByRefType(), typeof(bool), typeof(bool)}; + return typeof(BigInteger).GetMethod( + "TryWriteBytes", + BindingFlags.Instance | BindingFlags.Public, + paramTypes); + } + private static void TryWriteBytes(BaseGenerator generator, MethodInfo tryWriteBytes, LocalBuilder buffer, LocalBuilder? discard = null) + { + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, buffer); + if (discard is not null) + { + generator.Il.Emit(OpCodes.Ldloca, discard); + } + + foreach (var _ in tryWriteBytes.GetParameters().Skip(discard is not null ? 2 : 1)) + { + generator.Il.Emit(OpCodes.Ldc_I4_0); + } + + generator.Il.Emit(OpCodes.Callvirt, tryWriteBytes); + generator.Il.Emit(OpCodes.Pop); + } + + private static LocalBuilder GetSerializableBuffer(SerializeGenerator generator, Type type, KnownTypes knownType) + { + var writeInt = Helper.GetWrite(generator, typeof(int)); + + switch (knownType) + { + case KnownTypes.IpAddress: + { + var addressFamilyProp = type.GetProperty("AddressFamily", BindingFlags.Instance | BindingFlags.Public)!; + var addressFamilyVar = generator.Il.DeclareLocal(typeof(AddressFamily)); + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, addressFamilyProp.GetMethod!); + generator.Il.Emit(OpCodes.Stloc, addressFamilyVar); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, addressFamilyVar); + generator.Il.Emit(OpCodes.Callvirt, writeInt); + + var tryWriteBytes = GetTryWriteBytes(typeof(int)); + + LocalBuilder bufferVar; + if (tryWriteBytes is null) + { + bufferVar = GetSimpleByteBuffer(generator, type, "GetAddressBytes"); + } + else + { + GetIpAddressSize(generator, addressFamilyVar); + + var bufferSizeVar = generator.Il.DeclareLocal(typeof(int)); + + generator.Il.Emit(OpCodes.Dup); + generator.Il.Emit(OpCodes.Stloc, bufferSizeVar); + generator.Il.Emit(OpCodes.Localloc); + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + + bufferVar = CreateSpanByteBuffer(generator); + + var discardInt = bufferSizeVar; // Reuse bufferSizeVar for out discard + + TryWriteBytes(generator, tryWriteBytes, bufferVar, discardInt); + } + + return bufferVar; + } + case KnownTypes.Guid: + { + var tryWriteBytes = GetTryWriteBytes(); + + LocalBuilder bufferVar; + if (tryWriteBytes is null) + { + bufferVar = GetSimpleByteBuffer(generator, type, "ToByteArray"); + } + else + { + generator.Il.Emit(OpCodes.Ldc_I4, 16); + generator.Il.Emit(OpCodes.Localloc); + generator.Il.Emit(OpCodes.Ldc_I4, 16); + + bufferVar = CreateSpanByteBuffer(generator); + TryWriteBytes(generator, tryWriteBytes, bufferVar); + } + + return bufferVar; + } + case KnownTypes.BigInteger: + { + var getByteCount = type.GetMethod("GetByteCount", BindingFlags.Instance | BindingFlags.Public)!; + generator.CurrentContext.GetValueRef!(generator.Il); + if (getByteCount.GetParameters().Length == 1) + generator.Il.Emit(OpCodes.Ldc_I4_0); + generator.Il.Emit(OpCodes.Callvirt, getByteCount); + var bufferSizeVar = generator.Il.DeclareLocal(typeof(int)); + generator.Il.Emit(OpCodes.Stloc, bufferSizeVar); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + generator.Il.Emit(OpCodes.Callvirt, writeInt); + + var tryWriteBytes = GetTryWriteBytesBigInt(bufferSizeVar.LocalType); + + LocalBuilder bufferVar; + if (tryWriteBytes is null) + { + bufferVar = GetSimpleByteBuffer(generator, type, "ToByteArray"); + } + else + { + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + generator.Il.Emit(OpCodes.Localloc); + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + + bufferVar = CreateSpanByteBuffer(generator); + TryWriteBytes(generator, tryWriteBytes, bufferVar, bufferSizeVar); + } + + return bufferVar; + } + default: + throw new NotSupportedException($"{knownType} is not a valid type to be serialized."); + } + } + + private static LocalBuilder CreateSpanByteBuffer(BaseGenerator generator) + { + var byteSpanCtor = typeof(Span).GetConstructor(BindingFlags.Instance | BindingFlags.Public, + new[] { typeof(void*), typeof(int) }) + ?? throw new InvalidProgramException("Span is missing constructor(void*, int)."); + + var bufferVar = generator.Il.DeclareLocal(typeof(Span)); + generator.Il.Emit(OpCodes.Newobj, byteSpanCtor); + + generator.Il.Emit(OpCodes.Stloc, bufferVar); + return bufferVar; + } + + private static LocalBuilder GetSimpleByteBuffer(BaseGenerator generator, Type type, string methodName) + { + var getAddressBytes = type.GetMethod(methodName, + BindingFlags.Instance | BindingFlags.Public, Type.EmptyTypes); + if (getAddressBytes is null) + throw new NotSupportedException( + $"{type} type is missing 'TryWriteBytes(Span, out int)' or '{methodName}' method"); + + var bufferVar = generator.Il.DeclareLocal(getAddressBytes.ReturnType); + + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, getAddressBytes); + + generator.Il.Emit(OpCodes.Stloc, bufferVar); + return bufferVar; + } + + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + + var knownType = GetKnownType(type); + + if (knownType == KnownTypes.None) + return false; + + var buffer = GetSerializableBuffer(generator, type, knownType); + + var writeMethod = generator.CurrentContext.WriterType.GetMethod("Write", + BindingFlags.Public | BindingFlags.Instance, new[] { buffer.LocalType }); + + if (writeMethod is null && buffer.LocalType != typeof(ReadOnlySpan)) + { + writeMethod ??= generator.CurrentContext.WriterType.GetMethod("Write", + BindingFlags.Public | BindingFlags.Instance, new[] { typeof(ReadOnlySpan) }); + } + + if (writeMethod is null) + throw new NotImplementedException($"Missing Write implementation on writer for type {buffer.LocalType}"); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, buffer); + generator.Il.Emit(OpCodes.Callvirt, writeMethod); + + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + var readInt = Helper.GetRead(generator, typeof(int)); + + var knownType = GetKnownType(type); + + if (knownType == KnownTypes.None) + return false; + + var ctor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, + new[] { typeof(ReadOnlySpan), typeof(bool), typeof(bool) }); + bool useSpans = true; + if (ctor is null) + { + useSpans = false; + ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, + new[] { typeof(byte[]) }); + } + + if (ctor is null) + { + throw new NotSupportedException($"No matching {knownType} ctor found taking either 'byte[]' or 'ReadOnlySpan'"); + } + + switch (knownType) + { + case KnownTypes.IpAddress: + var addressFamilyVar = generator.Il.DeclareLocal(typeof(AddressFamily)); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, readInt); + generator.Il.Emit(OpCodes.Stloc, addressFamilyVar); + + GetIpAddressSize(generator, addressFamilyVar); + + break; + case KnownTypes.Guid: + generator.Il.Emit(OpCodes.Ldc_I4, 16); + break; + case KnownTypes.BigInteger: + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, readInt); + break; + default: + throw new NotSupportedException($"{knownType} is not supported to be serialized."); + } + + LocalBuilder bufferVar; + var bufferSizeVar = generator.Il.DeclareLocal(typeof(int)); + if (useSpans) + { + generator.Il.Emit(OpCodes.Dup); + generator.Il.Emit(OpCodes.Stloc, bufferSizeVar); + generator.Il.Emit(OpCodes.Localloc); + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + + bufferVar = CreateSpanByteBuffer(generator); + var readBytes = MethodResolver.GetBestMatch(generator.CurrentContext.ReaderType, "ReadBytes", + BindingFlags.Public | BindingFlags.Instance, + new[] { typeof(Span) }, null) + ?? throw new InvalidOperationException($"{generator.CurrentContext.ReaderType} does not have a valid void ReadBytes(Span) overload."); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, bufferVar); + generator.EmitCall(readBytes); + } + else + { + var readBytes = MethodResolver.GetBestMatch(generator.CurrentContext.ReaderType, "ReadBytes", + BindingFlags.Public | BindingFlags.Instance, + new[] { typeof(int) }, typeof(byte[])) + ?? throw new InvalidOperationException($"{generator.CurrentContext.ReaderType} does not have a valid byte[] ReadBytes(int) overload."); + generator.Il.Emit(OpCodes.Stloc, bufferSizeVar); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, bufferSizeVar); + generator.EmitCall(readBytes); + + bufferVar = generator.Il.DeclareLocal(typeof(byte[])); + generator.Il.Emit(OpCodes.Stloc, bufferVar); + } + + generator.Il.Emit(OpCodes.Ldloc, bufferVar); + foreach (var _ in ctor.GetParameters().Skip(1)) + { + generator.Il.Emit(OpCodes.Ldc_I4_0); + } + generator.Il.Emit(OpCodes.Newobj, ctor); + + generator.CurrentContext.SetValue!(generator.Il); + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/ListSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/ListSerializer.cs new file mode 100644 index 0000000..d3e79fb --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/ListSerializer.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal static class ListSerializer +{ + private static NullabilityInfo CreateListNullability(NullabilityInfo info) + { + return NullabilityHelper.CreateNullability(info.Type, info.ReadState, + info.ReadState, info.ElementType, info.GenericTypeArguments); + } + + private static NullabilityInfo GetItemNullability(Type type) + { + var info = NullabilityHelper.Context.Create( + Helper.GetMethodIncludingInterfaces(type, "GetEnumerator", BindingFlags.Instance | BindingFlags.Public)!.ReturnType.GetProperty("Current")!); + return CreateListNullability(info); + } + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + + var readonlyCollection = type.GetOpenGenericType(typeof(IReadOnlyCollection<>)); + if (readonlyCollection is null) + return false; + var elementType = readonlyCollection.GenericTypeArguments[0]; + + var getCountProp = Helper.GetPropertyIncludingInterfaces(type, "Count", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy) + ?? throw new InvalidOperationException($"{type} is a List but does not have a valid Count property."); + + if (getCountProp.GetMethod is null) + throw new InvalidOperationException($"{type} is a List but does not have a valid Count getter."); + + var inheritanceDepth = GetInheritanceDepth(type); + + if (inheritanceDepth > -1) + { + var subContext = + generator.CurrentContext.SubContext(generator.CurrentContext.ValueType, + null); + subContext.SetValue = generator.CurrentContext.SetValue; + subContext.GetValue = generator.CurrentContext.GetValue; + subContext.GetValueRef = generator.CurrentContext.GetValueRef; + + generator.PushContext(subContext); + + _ = PublicPropertySerializer.Serialize(generator, typeConfig, inheritanceDepth); + + _ = generator.PopContext(); + } + + var writeInt = Helper.GetWrite(generator, typeof(int)); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, getCountProp.GetMethod); + generator.Il.Emit(OpCodes.Callvirt, writeInt); + + generator.EmitForEach(type, gen => gen.CurrentContext.GetValue!(gen.Il), + (gen, cont, br, item) => + { + var subContext = + generator.CurrentContext.SubContext(new BaseGeneratorContext.TypeContext(elementType, GetItemNullability(type)), + null); + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, item); + subContext.GetValueRef = elementType.IsValueType + ? g => g.Emit(OpCodes.Ldloca, item) + : subContext.GetValue; + + var tpConf = TypeNameCache.GetTypeConfig(elementType); + + _ = generator.GenerateSerialize(subContext, tpConf); + }); + + return true; + } + + private static ConstructorInfo? GetCtor(Type type) + { + return type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, new[] { typeof(int) }) + ?? type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, Type.EmptyTypes); + } + + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + + var readonlyCollection = type.GetOpenGenericType(typeof(IReadOnlyCollection<>)); + if (readonlyCollection is null) + return false; + var elementType = readonlyCollection.GenericTypeArguments[0]; + + + var inheritanceDepth = GetInheritanceDepth(type); + + if (inheritanceDepth > -1) + { + var subContext = + generator.CurrentContext.SubContext(generator.CurrentContext.ValueType, + null); + subContext.SetValue = generator.CurrentContext.SetValue; + subContext.GetValue = generator.CurrentContext.GetValue; + subContext.GetValueRef = generator.CurrentContext.GetValueRef; + + generator.PushContext(subContext); + + _ = PublicPropertySerializer.Deserialize(generator, typeConfig, inheritanceDepth); + + _ = generator.PopContext(); + } + + var readInt = Helper.GetRead(generator, typeof(int)); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, readInt); + + var countVar = generator.Il.DeclareLocal(typeof(int)); + generator.Il.Emit(OpCodes.Stloc, countVar); + + ConstructorInfo? ctor = null; + Type? baseInterfaceType = null; + + var elementTypeGenParams = new[] { elementType }; + if (type is { IsAbstract: false, IsInterface: false }) + { + ctor = GetCtor(type); + baseInterfaceType = type; + } + if (typeof(IReadOnlySet<>).MakeGenericType(elementTypeGenParams) is {} setType && setType.IsAssignableFrom(type)) + { + ctor ??= GetCtor(setType); + baseInterfaceType = setType; + } + else + { + if (elementType.IsGenericType && elementType.IsAssignableToOpenGeneric(typeof(KeyValuePair<,>))) + { + var keyType = elementType.GenericTypeArguments[0]; + var valueType = elementType.GenericTypeArguments[1]; + var genericParams = new[] { keyType, valueType }; + if (typeof(IDictionary).MakeGenericType(genericParams) is {} dictType && dictType.IsAssignableFrom(type) + || typeof(IReadOnlyDictionary<,>).MakeGenericType(genericParams) is {} readOnlyDictType && readOnlyDictType.IsAssignableFrom(type)) + { + ctor ??= GetCtor(typeof(Dictionary<,>).MakeGenericType(genericParams)); + + baseInterfaceType = typeof(ICollection).MakeGenericType(elementTypeGenParams); + } + } + + if (typeof(IList).MakeGenericType(elementTypeGenParams) is {} listType && listType.IsAssignableFrom(type) + || typeof(IReadOnlyCollection<>).MakeGenericType(elementTypeGenParams) is {} readOnlyListType && readOnlyListType.IsAssignableFrom(type)) + { + ctor ??= GetCtor(typeof(List<>).MakeGenericType(elementTypeGenParams)); + baseInterfaceType = typeof(ICollection).MakeGenericType(elementTypeGenParams); + } + } + + if (ctor is null) + { + throw new NotSupportedException(); + } + + var tempListVar = generator.Il.DeclareLocal(ctor.DeclaringType!); + if (ctor.GetParameters().Length == 1) + { + generator.Il.Emit(OpCodes.Ldloc, countVar); + } + generator.Il.Emit(OpCodes.Newobj, ctor); + + generator.Il.Emit(OpCodes.Stloc, tempListVar); + + MethodInfo? GetAddMethod(Type? t) + { + if (t is null) + return null; + return t.GetMethod(nameof(IList.Add), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic, elementTypeGenParams) + ?? t.GetMethod(nameof(Stack.Push), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic, elementTypeGenParams) + ?? t.GetMethod(nameof(Queue.Enqueue), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic, elementTypeGenParams); + } + + var addMethod = GetAddMethod(tempListVar.LocalType) + ?? GetAddMethod(baseInterfaceType); + + if (addMethod is null) + throw new NotSupportedException(); + + generator.EmitFor( + g => + { + var counter = g.Il.DeclareLocal(typeof(int)); + g.Il.Emit(OpCodes.Ldc_I4_0); + g.Il.Emit(OpCodes.Stloc, counter); + return counter; + }, + (g, counter) => + { + g.Il.Emit(OpCodes.Ldloc, counter!); + g.Il.Emit(OpCodes.Ldloc, countVar); + g.Il.Emit(OpCodes.Clt); + }, + (g, counter) => + { + g.EmitIncrement(counter!); + }, + (g, _, _, _) => + { + + var item = g.Il.DeclareLocal(elementType); + + var subContext = + generator.CurrentContext.SubContext(new BaseGeneratorContext.TypeContext(elementType, GetItemNullability(type)), + null); + subContext.GetValue = gen => gen.Emit(OpCodes.Ldloc, item); + subContext.GetValueRef = elementType.IsValueType + ? gen => gen.Emit(OpCodes.Ldloca, item) + : subContext.GetValue; + subContext.SetValue = gen => gen.Emit(OpCodes.Stloc, item); + + if (elementType.IsPrimitive || elementType.IsPointer) + { + g.Il.Emit(OpCodes.Ldc_I4_0); + subContext.SetValue(g.Il); + } + else if (elementType.IsValueType) + { + subContext.GetValueRef(g.Il); + g.Il.Emit(OpCodes.Initobj, elementType); + } + else + { + g.Il.Emit(OpCodes.Ldnull); + subContext.SetValue(g.Il); + } + + var tpConf = TypeNameCache.GetTypeConfig(elementType); + + _ = generator.GenerateDeserialize(subContext, tpConf); + + g.Il.Emit(OpCodes.Ldloc, tempListVar); + subContext.GetValue!(g.Il); + g.EmitCall(addMethod); + if (addMethod.ReturnType != typeof(void)) + g.Il.Emit(OpCodes.Pop); + + }); + + generator.Il.Emit(OpCodes.Ldloc, tempListVar); + generator.CurrentContext.SetValue!(generator.Il); + + return true; + } + + private static int GetInheritanceDepth(Type? type, int lastAmount = -1) + { + while (type is not null && !type.IsArray) + { + if (type.GetInterfaces().Any(x => x == typeof(IEnumerable))) + return lastAmount; + type = type.BaseType; + lastAmount = ++lastAmount; + } + + return lastAmount; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/MethodCallSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/MethodCallSerializer.cs new file mode 100644 index 0000000..bfee245 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/MethodCallSerializer.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class MethodCallSerializer +{ + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var useStaticSerialize = typeConfig.SerializeType != generator.CurrentContext.ValueType.Type; + var serializerMethod = MethodResolver.GetBestMatch(typeConfig.SerializeType, typeConfig.Names.SerializeName, + BindingFlags.Public | BindingFlags.NonPublic | + (useStaticSerialize ? BindingFlags.Static : BindingFlags.Instance), + useStaticSerialize ? new[] { generator.CurrentContext.WriterType, generator.CurrentContext.ValueType.Type } : new[] { generator.CurrentContext.WriterType }, null); + if (serializerMethod is null) + return false; + if (serializerMethod.IsStatic) + { + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValue!(generator.Il); + } + else + { + generator.CurrentContext.GetValue!(generator.Il); + generator.CurrentContext.GetReaderWriter(generator.Il); + } + generator.EmitCall(serializerMethod); + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var deserializerMethod = MethodResolver.GetBestMatch(typeConfig.DeserializeType, + typeConfig.Names.DeserializeName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, + new[] { generator.CurrentContext.ReaderType }, generator.CurrentContext.ValueType.Type); + if (deserializerMethod is null) + { + return false; + } + + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Call, deserializerMethod); + + generator.CurrentContext.SetValue!(generator.Il); + // generator.CurrentContext.GetValue = ilGenerator => ilGenerator.Emit(OpCodes.Ldloc, serializerVar); + // generator.CurrentContext.SetValue = ilGenerator => ilGenerator.Emit(OpCodes.Stloc, serializerVar); + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/NullableSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/NullableSerializer.cs new file mode 100644 index 0000000..ec90fd3 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/NullableSerializer.cs @@ -0,0 +1,131 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal static class NullableSerializer +{ + private static NullabilityInfo RemoveNullability(NullabilityInfo info) + { + return NullabilityHelper.CreateNullability(info.Type, NullabilityState.NotNull, + NullabilityState.NotNull, info.ElementType, info.GenericTypeArguments); + } + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType; + if (type.NullabilityInfo.ReadState == NullabilityState.NotNull) + return false; + + var writeBool = Helper.GetWrite(generator, typeof(bool)); + var nullableType = type.Type; + + if (type.Type.IsValueType) + { + nullableType = type.Type.GetGenericArguments()[0]; + generator.CurrentContext.GetValueRef!(generator.Il); + var getHasValue = type.Type.GetProperty("HasValue")!.GetMethod!; + generator.EmitCall(getHasValue); + } + else + { + generator.CurrentContext.GetValue!(generator.Il); + generator.Il.Emit(OpCodes.Ldnull); + generator.Il.Emit(OpCodes.Cgt_Un); + } + + var isNotNullVar = generator.Il.DeclareLocal(typeof(bool)); + generator.Il.Emit(OpCodes.Stloc, isNotNullVar); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, isNotNullVar); + + generator.Il.Emit(OpCodes.Callvirt, writeBool); + generator.Il.Emit(OpCodes.Ldloc, isNotNullVar); + + + + generator.EmitIf(OpCodes.Brfalse, gen => + { + var subContext = + gen.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(nullableType, RemoveNullability(type.NullabilityInfo)), + null); + if (type.Type.IsValueType) + { + var v = gen.Il.DeclareLocal(nullableType); + var getValue = type.Type.GetProperty("Value")!.GetMethod!; + + gen.CurrentContext.GetValueRef!(gen.Il); + gen.EmitCall(getValue); + gen.Il.Emit(OpCodes.Stloc, v); + + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, v); + subContext.GetValueRef = g => g.Emit(OpCodes.Ldloca, v); + } + else + { + subContext.GetValue = gen.CurrentContext.GetValue; + subContext.GetValueRef = gen.CurrentContext.GetValueRef; + } + + + _ = ((SerializeGenerator)gen).GenerateSerialize((SerializeGeneratorContext)subContext, typeConfig, baseRecursionDepth); + }, null); + + + + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType; + if (type.NullabilityInfo.WriteState == NullabilityState.NotNull) + return false; + + var nullableType = type.Type; + if (type.Type.IsValueType) + { + nullableType = type.Type.GetGenericArguments()[0]; + } + + var readBool = Helper.GetRead(generator, typeof(bool)); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Callvirt, readBool); + + + generator.EmitIf(OpCodes.Brfalse, gen => + { + var subContext = + gen.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(nullableType, RemoveNullability(type.NullabilityInfo)), + null); + + if (type.Type.IsValueType) + { + var v = gen.Il.DeclareLocal(nullableType); + + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, v); + subContext.GetValueRef = g => g.Emit(OpCodes.Ldloca, v); + subContext.SetValue = g => g.Emit(OpCodes.Stloc, v); + } + else + { + subContext.SetValue = gen.CurrentContext.SetValue; + subContext.GetValue = gen.CurrentContext.GetValue; + subContext.GetValueRef = gen.CurrentContext.GetValueRef; + } + + _ = ((DeserializeGenerator)gen).GenerateDeserialize((DeserializeGeneratorContext)subContext, typeConfig); + + if (type.Type.IsValueType) + { + subContext.GetValue!(gen.Il); + gen.Il.Emit(OpCodes.Newobj, type.Type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, new []{ nullableType })!); + gen.CurrentContext.SetValue!(gen.Il); + } + }, null); + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/PublicPropertySerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/PublicPropertySerializer.cs new file mode 100644 index 0000000..2674438 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/PublicPropertySerializer.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal static class PublicPropertySerializer +{ + private static IEnumerable<(MemberInfo memberInfo, int depth)> OrderProps(IEnumerable<(MemberInfo memberInfo, int depth)> props) + { + return props.OrderBy(x => + { + var attr = x.memberInfo.GetCustomAttributesData() + .FirstOrDefault(y => y.AttributeType.FullName == Consts.NoosonOrderAttribute); + return attr == null ? int.MaxValue : (int)attr.ConstructorArguments[0].Value!; + }).ThenByDescending(x => x.depth); + } + private static IEnumerable<(MemberInfo memberInfo, int depth)> FilterPropsForNotWriteOnly(IEnumerable<(MemberInfo memberInfo, int)> props) + { + return props.Where(x => + x.memberInfo is PropertyInfo { GetMethod: not null } || + x.memberInfo.MemberType == MemberTypes.Field); + } + + private static NullabilityInfo FixReadability(NullabilityInfo info, bool canRead, bool canWrite) + { + return NullabilityHelper.CreateNullability(info.Type, + canRead || !canWrite ? info.ReadState : info.WriteState, + !canRead || canWrite ? info.WriteState : info.ReadState, + info.ElementType, info.GenericTypeArguments); + } + private static (Type, NullabilityInfo, PropertyInfo?, FieldInfo?) ExtractInfo(MemberInfo memberInfo) + { + return memberInfo switch + { + PropertyInfo pi => (pi.PropertyType, + FixReadability(NullabilityHelper.Context.Create(pi), pi.CanRead, pi.CanWrite), pi, null), + FieldInfo fi => (fi.FieldType, NullabilityHelper.Context.Create(fi), null, fi), + _ => throw new ArgumentOutOfRangeException(nameof(memberInfo)) + }; + } + + internal static MethodInfo? GetBaseDeserialize(DeserializeGenerator generator, Type type, bool compareWithOwnSignature, AssemblyNameCache.Names names, List? requiredParameter = null) + { + return Helper.GetFirstMemberWithBase( + type, + im => + { + var parameters = im.GetParameters(); + return parameters.Length > 1 + && (requiredParameter is null + || parameters.Length == requiredParameter.Count) + && !compareWithOwnSignature + && im.Name == names.DeserializeOutName + && parameters[0].ParameterType.IsAssignableFrom(generator.CurrentContext.ReaderType) + && parameters.Skip(1) + .All(x => x.IsOut) + && (requiredParameter is null + || parameters.ForAll(requiredParameter, + (a, b) => a.ParameterType == b)); + }); + } + + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + + var baseSerialize = MethodResolver.GetBestMatch(type, typeConfig.Names.SerializeName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy, + new[] { generator.CurrentContext.WriterType }, null); + + var props = Helper.GetMembersWithBase(type, baseSerialize is null ? baseRecursionDepth : 0) + .Where(x => x.memberInfo is not PropertyInfo p || p.GetIndexParameters().Length == 0); + + props = FilterPropsForNotWriteOnly(props).ToList(); + + if (baseSerialize is not null && !baseSerialize.IsAbstract) // Do not generate when done for a different type + { + generator.CurrentContext.GetValue!(generator.Il); + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.EmitCall(baseSerialize); + } + + foreach ((var prop, int _) in OrderProps(props)) + { + var (subType, nullability, pi, fi) = ExtractInfo(prop); + var subContext = + generator.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(subType, nullability), + false); + var v = generator.Il.DeclareLocal(subType); + + var tpConfig = typeConfig; + + if (fi is not null) + { + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(fi.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, fi); + generator.Il.Emit(OpCodes.Stloc, v); + tpConfig = TypeNameCache.GetTypeConfig(fi); + } + else if (pi is not null) + { + if (!pi.GetMethod!.IsStatic) + generator.CurrentContext.GetValueRef!(generator.Il); + generator.EmitCall(pi.GetMethod!); + generator.Il.Emit(OpCodes.Stloc, v); + tpConfig = TypeNameCache.GetTypeConfig(pi); + } + + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, v); + subContext.GetValueRef = subType.IsValueType + ? g => g.Emit(OpCodes.Ldloca, v) + : subContext.GetValue; + + _ = generator.GenerateSerialize(subContext, tpConfig, baseRecursionDepth - 1); + } + + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + + var baseDeserialize = GetBaseDeserialize(generator, type, false, typeConfig.Names); + + var props = Helper.GetMembersWithBase(type, baseDeserialize is null ? baseRecursionDepth : 0) + .Where(x => x.memberInfo is not PropertyInfo p || p.GetIndexParameters().Length == 0); + + props = FilterPropsForNotWriteOnly(props).ToList(); + + var parametersToMatch = baseDeserialize is not null + ? new List(baseDeserialize.GetParameters().Skip(1)) + : new List(); + var outVariables = baseDeserialize is not null + ? new LocalBuilder[baseDeserialize.GetParameters().Length - 1] + : Array.Empty(); + + var otherVariables = new List(); + + var allVariables = new Dictionary(); + + foreach (var p in props) + { + if (baseDeserialize is not null) + { + bool foundParameter = false; + for (int i = parametersToMatch.Count - 1; i >= 0; i--) + { + var outParam = parametersToMatch[i]; + var t = p.memberInfo switch + { + FieldInfo fi => fi.FieldType, + PropertyInfo pi => pi.PropertyType, + _ => null + }; + if (outParam.ParameterType != t || outParam.Name is null || !Helper.MatchIdentifierWithPropName(p.memberInfo.Name, outParam.Name)) + continue; + var v = outVariables[i] = generator.Il.DeclareLocal(outParam.ParameterType.GetElementType()!); + parametersToMatch.RemoveAt(i); + foundParameter = true; + allVariables.Add(p.memberInfo.Name, (p.memberInfo, v)); + break; + } + + if (foundParameter) + continue; + } + + otherVariables.Add(p.memberInfo); + } + + if (baseDeserialize is not null) + { + generator.CurrentContext.GetReaderWriter(generator.Il); + foreach (var p in outVariables) + { + generator.Il.Emit(OpCodes.Ldloca, p); + } + generator.EmitCall(baseDeserialize); + } + + foreach (var prop in otherVariables) + { + var (subType, nullability, pi, fi) = ExtractInfo(prop); + var subContext = + generator.CurrentContext.SubContext( + new BaseGeneratorContext.TypeContext(subType, nullability), + false); + var v = generator.Il.DeclareLocal(subType); + + + + subContext.GetValue = g => g.Emit(OpCodes.Ldloc, v); + subContext.GetValueRef = subType.IsValueType + ? g => g.Emit(OpCodes.Ldloca, v) + : subContext.GetValue; + subContext.SetValue = g => g.Emit(OpCodes.Stloc, v); + + var tpConfig = typeConfig; + if (fi is not null) + { + tpConfig = TypeNameCache.GetTypeConfig(fi); + } + else if (pi is not null) + { + tpConfig = TypeNameCache.GetTypeConfig(pi); + } + + _ = generator.GenerateDeserialize(subContext, tpConfig, baseRecursionDepth - 1); + + + allVariables.Add(prop.Name, (prop, v)); + } + + var ctorToCall = GetCtorArguments(type, allVariables, out var ctorArgs); + + if (ctorToCall is not null) + { + foreach (var (key, arg) in ctorArgs) + { + generator.Il.Emit(OpCodes.Ldloc, arg); + + allVariables.Remove(key); + } + + + generator.Il.Emit(OpCodes.Newobj, ctorToCall); + + generator.CurrentContext.SetValue!(generator.Il); + } + + foreach (var (_, (m, v)) in allVariables) + { + if (m is FieldInfo fi) + { + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, v); + generator.Il.Emit(OpCodes.Stfld, fi); + } + else if (m is PropertyInfo { SetMethod: not null } pi) + { + generator.CurrentContext.GetValueRef!(generator.Il); + generator.Il.Emit(OpCodes.Ldloc, v); + generator.EmitCall(pi.SetMethod); + } + } + + + return true; + } + + private static IEnumerable OrderCtors(IEnumerable ctors) + { + return ctors.OrderByDescending(constructor => + (constructor.GetCustomAttributesData() + .FirstOrDefault(x => x.AttributeType.FullName == + Consts.NoosonPreferredCtorAttribute) is not null + ? 0xFFFF1 + : 0) //0xFFFF is the maximum amount of Parameters, so we add an additional one + + constructor.GetParameters().Length); + } + + internal static ConstructorInfo? GetCtorArguments(Type type, Dictionary localDeclarations, out List<(string, LocalBuilder)> ctorArguments) + { + ctorArguments = new List<(string, LocalBuilder)>(); + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length == 0) + { + return null; + } + + foreach (var constructor in OrderCtors(constructors)) + { + bool constructorMatch = true; + + foreach (var parameter in constructor.GetParameters()) + { + var parameterName = parameter.Name; + if (parameterName is null) + break; + + if (parameter.GetCustomAttributesData() + .FirstOrDefault(x => x.AttributeType.FullName == Consts.NoosonParameterAttribute) is + { } newNameAttribute) + { + parameterName = (string)newNameAttribute.ConstructorArguments[0].Value!; + } + + var matchedDeclarationKey + = localDeclarations + .FirstOrDefault(x => Helper.MatchIdentifierWithPropName(parameterName, x.Value.member.Name)).Key; + + if (matchedDeclarationKey is null) + { + constructorMatch = false; + break; + } + + ctorArguments.Add((matchedDeclarationKey, localDeclarations[matchedDeclarationKey].var)); + } + + + if (constructorMatch) + { + + + return constructor; + } + + ctorArguments.Clear(); + } + + throw new NotSupportedException(); + } + +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/RecursiveCallSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/RecursiveCallSerializer.cs new file mode 100644 index 0000000..fe306ad --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/RecursiveCallSerializer.cs @@ -0,0 +1,27 @@ +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class RecursiveCallSerializer +{ + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var serializerMethod = generator.CurrentContext.Method; + if (!generator.ContextMap.TryGetValue(generator.CurrentContext.ValueType, out var context) || context == generator.CurrentContext || !context.IsTopLevel) + return false; + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValue!(generator.Il); + generator.Il.Emit(OpCodes.Call, serializerMethod); + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var deserializerMethod = generator.CurrentContext.Method; + if (!generator.ContextMap.TryGetValue(generator.CurrentContext.ValueType, out var context) || context == generator.CurrentContext || !context.IsTopLevel) + return false; + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Call, deserializerMethod); + generator.CurrentContext.SetValue!(generator.Il); + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/SpecialTypeSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/SpecialTypeSerializer.cs new file mode 100644 index 0000000..cfd7c8b --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/SpecialTypeSerializer.cs @@ -0,0 +1,44 @@ +using System; +using System.Reflection.Emit; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class SpecialTypeSerializer +{ + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + var typeCode = Type.GetTypeCode(type); + if (typeCode != TypeCode.String && typeCode is < TypeCode.Boolean or > TypeCode.Double) + return false; + var serializerMethod = Helper.TryGetWrite(generator, type); + if (serializerMethod is null) + return false; + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValue!(generator.Il); + + generator.Il.Emit(OpCodes.Callvirt, serializerMethod); + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + var typeCode = Type.GetTypeCode(type); + if (typeCode != TypeCode.String && typeCode is < TypeCode.Boolean or > TypeCode.Double) + return false; + var deserializerMethod = Helper.TryGetRead(generator, type, typeCode); + if (deserializerMethod is null) + { + return false; + } + + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.Il.Emit(OpCodes.Call, deserializerMethod); + + generator.CurrentContext.SetValue!(generator.Il); + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/Serializers/UnmanagedTypeSerializer.cs b/NonSucking.Framework.Serialization.Advanced/Serializers/UnmanagedTypeSerializer.cs new file mode 100644 index 0000000..8f5efd0 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/Serializers/UnmanagedTypeSerializer.cs @@ -0,0 +1,37 @@ +namespace NonSucking.Framework.Serialization.Advanced; + +internal class UnmanagedTypeSerializer +{ + + public static bool Serialize(SerializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + if (!type.IsUnmanaged()) + return false; + + var serializerMethod = Helper.GetWrite(generator, type, "WriteUnmanaged"); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.CurrentContext.GetValue!(generator.Il); + + generator.EmitCall(serializerMethod); + + return true; + } + public static bool Deserialize(DeserializeGenerator generator, TypeNameCache.TypeConfig typeConfig, int baseRecursionDepth = int.MaxValue) + { + var type = generator.CurrentContext.ValueType.Type; + if (!type.IsUnmanaged()) + return false; + + + var deserializerMethod = Helper.GetRead(generator, type, "ReadUnmanaged"); + + generator.CurrentContext.GetReaderWriter(generator.Il); + generator.EmitCall(deserializerMethod); + generator.CurrentContext.SetValue!(generator.Il); + + + return true; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization.Advanced/TypeNameCache.cs b/NonSucking.Framework.Serialization.Advanced/TypeNameCache.cs new file mode 100644 index 0000000..b85d800 --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/TypeNameCache.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace NonSucking.Framework.Serialization.Advanced; + +internal class TypeNameCache +{ + internal struct TypeConfig + { + public TypeConfig(bool isCustomMethodCall, Type serializeType, Type deserializeType, AssemblyNameCache.Names names) + { + IsCustomMethodCall = isCustomMethodCall; + SerializeType = serializeType; + DeserializeType = deserializeType; + Names = names; + } + + public bool IsCustomMethodCall { get; } + public Type SerializeType { get; } + public Type DeserializeType { get; } + public AssemblyNameCache.Names Names { get; } + + public override string ToString() + { + return $"{SerializeType.Name}.{Names.SerializeName}/{DeserializeType.Name}.{Names.DeserializeName}"; + } + } + + internal static CustomAttributeData? GetFirstNoosonCustom(IList data) + { + return data.FirstOrDefault( + x => x.AttributeType.FullName == Consts.NoosonCustomAttribute); + } + + private static TypeConfig GetTypeConfig(CustomAttributeData attr, AssemblyNameCache.Names names, Type type) + { + var deserializeName = AssemblyNameCache.FirstOrNull(attr.NamedArguments, + argument => argument.MemberName == "DeserializeMethodName")?.TypedValue.Value?.ToString(); + var serializeName = AssemblyNameCache.FirstOrNull(attr.NamedArguments, + argument => argument.MemberName == "SerializeMethodName")?.TypedValue.Value?.ToString(); + names = AssemblyNameCache.Names.Combine(names, new AssemblyNameCache.Names(serializeName, deserializeName, null)); + + var serializeType = AssemblyNameCache.FirstOrNull(attr.NamedArguments, + argument => argument.MemberName == "SerializeImplementationType")?.TypedValue.Value as Type ?? type; + var deserializeType = AssemblyNameCache.FirstOrNull(attr.NamedArguments, + argument => argument.MemberName == "DeserializeImplementationType")?.TypedValue.Value as Type ?? type; + + return new TypeConfig(true, serializeType, deserializeType, names); + } + + private static TypeConfig GetTypeConfig(Type type, AssemblyNameCache.Names names) + { + var typeAttr = GetFirstNoosonCustom(type.GetCustomAttributesData()); + if (typeAttr is null) + return new TypeConfig(false, type, type, names); + return GetTypeConfig(typeAttr, names, type); + } + public static TypeConfig GetTypeConfig(Type type) + { + var names = AssemblyNameCache.ResolveAssemblyConfig(type.Assembly); + return GetTypeConfig(type, names); + } + + public static TypeConfig GetTypeConfig(PropertyInfo prop) + { + var type = prop.PropertyType; + var names = AssemblyNameCache.ResolveAssemblyConfig(type.Assembly); + var propAttr = GetFirstNoosonCustom(prop.GetCustomAttributesData()); + if (propAttr is null) + return GetTypeConfig(type, names); + return GetTypeConfig(propAttr, names, type); + } + + public static TypeConfig GetTypeConfig(FieldInfo prop) + { + var type = prop.FieldType; + var names = AssemblyNameCache.ResolveAssemblyConfig(type.Assembly); + var propAttr = GetFirstNoosonCustom(prop.GetCustomAttributesData()); + if (propAttr is null) + return GetTypeConfig(type, names); + return GetTypeConfig(propAttr, names, type); + } + +} diff --git a/NonSucking.Framework.Serialization.Advanced/TypeSerializerException.cs b/NonSucking.Framework.Serialization.Advanced/TypeSerializerException.cs new file mode 100644 index 0000000..0ba8cbe --- /dev/null +++ b/NonSucking.Framework.Serialization.Advanced/TypeSerializerException.cs @@ -0,0 +1,12 @@ +using System; + +namespace NonSucking.Framework.Serialization; + +public class TypeSerializerException : Exception +{ + public TypeSerializerException(string message) + : base(message) + { + + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization/Diagnostics.cs b/NonSucking.Framework.Serialization/Diagnostics.cs index 9b46ad7..86f2628 100644 --- a/NonSucking.Framework.Serialization/Diagnostics.cs +++ b/NonSucking.Framework.Serialization/Diagnostics.cs @@ -63,7 +63,7 @@ public static class Diagnostics public static readonly DiagnosticInfo SingletonImplementationRequired = new( 0021, "", - "Singleton property or field 'Instance' required for type converters." + "Singleton property or field 'Instance' required for {0}." ); public static readonly DiagnosticInfo NoValidConverter = new( @@ -83,6 +83,18 @@ public static class Diagnostics "", "Custom method call is not compatible with serializer of type '{0}'" ); + + public static readonly DiagnosticInfo InvalidDynamicResolver = new( + 0024, + "", + "Resolver of '{0}' does not implement necessary {1} interface and is not a valid dynamic resolver." + ); + + public static readonly DiagnosticInfo InvalidOpenGenericResolver = new( + 0025, + "", + "Resolver of '{0}' is a open generic type{1} which is not supported for resolvers." + ); public record DiagnosticInfo(int Id, string Title, string FormatString) { diff --git a/NonSucking.Framework.Serialization/Extensions.cs b/NonSucking.Framework.Serialization/Extensions.cs new file mode 100644 index 0000000..43d6c8e --- /dev/null +++ b/NonSucking.Framework.Serialization/Extensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace NonSucking.Framework.Serialization; + +public static class Extensions +{ + public static T? FirstOrNull(this IEnumerable enumerable, Func predicate) + where T : struct + { + foreach (var v in enumerable) + { + if (predicate(v)) + return v; + } + return null; + } +} \ No newline at end of file diff --git a/NonSucking.Framework.Serialization/Helper.cs b/NonSucking.Framework.Serialization/Helper.cs index 334e193..eed4b37 100644 --- a/NonSucking.Framework.Serialization/Helper.cs +++ b/NonSucking.Framework.Serialization/Helper.cs @@ -14,6 +14,7 @@ using VaVare.Generators.Common.Arguments.ArgumentTypes; using VaVare.Models; +using VaVare.Statements; namespace NonSucking.Framework.Serialization { @@ -38,11 +39,11 @@ internal static void Reset() internal static ITypeParameterSymbol? GetTypeParameter(IParameterSymbol parameter) { - return parameter as ITypeParameterSymbol; + return parameter.Type as ITypeParameterSymbol; } - internal static INamedTypeSymbol? GetParameterTypeConstraint(IParameterSymbol parameter) + internal static IEnumerable? GetParameterTypeConstraints(IParameterSymbol parameter) { - return GetTypeParameter(parameter)?.ConstraintTypes.OfType().FirstOrDefault(); + return GetTypeParameter(parameter)?.ConstraintTypes.OfType(); } internal static List? GetTypeParameterConstraints(GeneratedMethodParameter parameter, GeneratedMethod method) @@ -50,31 +51,41 @@ internal static void Reset() return method.TypeParameterConstraints.FirstOrDefault(y => y.Identifier == parameter.Type)?.Constraints; } - internal static TypeParameterConstraint? GetParameterTypeConstraint(GeneratedMethodParameter parameter, + internal static IEnumerable? GetParameterTypeConstraints(GeneratedMethodParameter parameter, GeneratedMethod method) { - return GetTypeParameterConstraints(parameter, method) - ?.FirstOrDefault(x => x.Type == TypeParameterConstraint.ConstraintType.Type); + return GetTypeParameterConstraints(parameter, method)? + .Where(x => x.Type == TypeParameterConstraint.ConstraintType.Type); } internal static bool MatchReaderWriterParameter(NoosonGeneratorContext context, IParameterSymbol readerParam) { return MatchReaderWriterParameter(context, readerParam.Type.ToDisplayString(), - GetParameterTypeConstraint(readerParam)?.ToDisplayString()); + readerParam.ContainingSymbol is IMethodSymbol { IsGenericMethod: true }, + GetParameterTypeConstraints(readerParam)?.Select(x => x.ToDisplayString())); } internal static bool MatchReaderWriterParameter(NoosonGeneratorContext context, GeneratedMethodParameter readerParam, GeneratedMethod method) { - return MatchReaderWriterParameter(context, readerParam.Type, - GetParameterTypeConstraint(readerParam, method)?.ConstraintIdentifier); + return MatchReaderWriterParameter(context, readerParam.Type, method.IsGenericMethod, + GetParameterTypeConstraints(readerParam, method)?.Select(x => x.ConstraintIdentifier)); } - internal static bool MatchReaderWriterParameter(NoosonGeneratorContext context, string baseParameterTypeName, string? typeConstraint) + internal static bool MatchReaderWriterParameter(NoosonGeneratorContext context, string baseParameterTypeName, bool isGeneric, IEnumerable? typeConstraints) { - var readerOrWriter = context.ReaderTypeName ?? context.WriterTypeName; + var readerOrWriter = context.MethodType == MethodType.Serialize ? context.WriterTypeName : context.ReaderTypeName; var matchGenericReader = readerOrWriter is null; if (matchGenericReader) - return typeConstraint is not null && - typeConstraint == Consts.GenericParameterReaderInterfaceFull; + { + readerOrWriter = (context.MethodType == MethodType.Serialize + ? Consts.GenericParameterWriterInterfaceFull + : Consts.GenericParameterReaderInterfaceFull); + if (isGeneric) + return typeConstraints is null || typeConstraints.Any(x => x == readerOrWriter); + + } + // TODO: match generic other way round + // Serialize(IBinaryWriter) should also be applicable for Serialize(T) where T : IBinaryWriter + return baseParameterTypeName == readerOrWriter; } @@ -517,10 +528,56 @@ internal static bool IsAssignable(ITypeSymbol typeToAssignTo, ITypeSymbol? typeT return false; if (SymbolEqualityComparer.Default.Equals(typeToAssignFrom, typeToAssignTo)) return true; + if (typeToAssignTo is ITypeParameterSymbol typeParameterSymbol) + { + if (typeParameterSymbol.HasValueTypeConstraint && !typeToAssignFrom.IsValueType + || typeParameterSymbol.HasReferenceTypeConstraint && !typeToAssignFrom.IsReferenceType + || typeParameterSymbol.HasUnmanagedTypeConstraint && !typeToAssignFrom.IsUnmanagedType + || typeParameterSymbol.HasNotNullConstraint && typeToAssignFrom.NullableAnnotation == NullableAnnotation.Annotated) + return false; + foreach (var constraintType in typeParameterSymbol.ConstraintTypes) + { + if (!IsAssignable(constraintType, typeToAssignFrom)) + return false; + } + + if (typeParameterSymbol.HasConstructorConstraint && !typeToAssignFrom.GetMembers() + .Any(x => x is IMethodSymbol { Parameters.Length: 0, Name: ".ctor" })) + return false; + return true; + } var assignable = IsAssignable(typeToAssignTo, typeToAssignFrom.BaseType); if (!assignable && typeToAssignTo.IsAbstract) return typeToAssignFrom.Interfaces.Any(x => IsAssignable(typeToAssignTo, x)); return assignable; } + + internal static (MemberInfo tempVariable, string tempVariableAccessorName) CreateTempIfNeeded(MemberInfo property, GeneratedSerializerCode statements) + { + var needsTemp = property.Parent != ""; + var tempVariable = needsTemp + ? property with { Name = Helper.GetRandomNameFor(property.Name, property.Parent), Parent = "" } + : property; + var tempVariableAccessorName = Helper.GetMemberAccessString(tempVariable); + var setTempVariable = Statement.Declaration.DeclareAndAssign( + tempVariableAccessorName, + SyntaxFactory.IdentifierName(Helper.GetMemberAccessString(property))); + if (needsTemp) + statements.Statements.Add(setTempVariable); + return (tempVariable, tempVariableAccessorName); + } + + internal static bool CheckSingleton(NoosonGeneratorContext context, ITypeSymbol type) + { + var instanceAccessor = Helper.GetFirstMemberWithBase(type, + (x) => x is IPropertySymbol or IFieldSymbol { Name: "Instance", IsStatic: true } + && x.DeclaredAccessibility is Accessibility.Internal or Accessibility.Public or Accessibility.ProtectedOrInternal, context); + if (instanceAccessor is null) + { + return false; + } + + return true; + } } } diff --git a/NonSucking.Framework.Serialization/NonSucking.Framework.Serialization.csproj b/NonSucking.Framework.Serialization/NonSucking.Framework.Serialization.csproj index 61af0c9..1672e7f 100644 --- a/NonSucking.Framework.Serialization/NonSucking.Framework.Serialization.csproj +++ b/NonSucking.Framework.Serialization/NonSucking.Framework.Serialization.csproj @@ -1,6 +1,6 @@  - + netstandard2.0 11.0 diff --git a/NonSucking.Framework.Serialization/NoosonGenerator.cs b/NonSucking.Framework.Serialization/NoosonGenerator.cs index 319492b..64e82f1 100644 --- a/NonSucking.Framework.Serialization/NoosonGenerator.cs +++ b/NonSucking.Framework.Serialization/NoosonGenerator.cs @@ -223,7 +223,7 @@ private static string TypeNameToSummaryName(string typeName) private static void InternalExecute(SourceProductionContext sourceProductionContext, (Compilation Compilation, ImmutableArray VisitInfos) source, List