diff --git a/samples/LiveTesting/MicroBenchmark.cs b/samples/LiveTesting/MicroBenchmark.cs index 0e07a4e..7be69a7 100644 --- a/samples/LiveTesting/MicroBenchmark.cs +++ b/samples/LiveTesting/MicroBenchmark.cs @@ -136,11 +136,13 @@ public void Warmup() } } - public static implicit operator BenchJob((string name, Action action) data) => new BenchJob + public BenchJob(string name, Action action) { - Name = data.name, - Action = data.action, - }; + Name = name; + Action = action; + } + + public static implicit operator BenchJob((string name, Action action) data) => new BenchJob(data.name, data.action); } struct Timer : IDisposable diff --git a/samples/LiveTesting/Program.cs b/samples/LiveTesting/Program.cs index dbaeb9f..eef135e 100644 --- a/samples/LiveTesting/Program.cs +++ b/samples/LiveTesting/Program.cs @@ -23,48 +23,300 @@ namespace LiveTesting using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; + using System.Threading; using Tutorial; using Xunit; using Encoding = System.Text.Encoding; - class Program + partial class Program { static Guid staticGuid = Guid.Parse("39b29409-880f-42a4-a4ae-2752d97886fa"); - public sealed class CustomProperty + + + + public class NullableWrapper + { + public Test? TestStruct; + + public NullableWrapper(Test? nullableStruct) + { + TestStruct = nullableStruct; + } + } + + public class NormalWrapper + { + public readonly Test Struct; + + public NormalWrapper(Test nullableStruct) + { + Struct = nullableStruct; + } + } + + public struct Test : IEquatable + { + public decimal Value; + public SubStruct SubStruct; + + public override bool Equals(object obj) + { + return obj is Test test && Equals(test); + } + + public bool Equals(Test other) + { + return Value == other.Value && + SubStruct.Equals(other.SubStruct); + } + } + + public readonly struct SubStruct : IEquatable { - public CustomProperty(string key, decimal value) + public readonly NameAge NameAge; + public readonly int? Count; + public readonly decimal? Num; + + public SubStruct(NameAge nameAge, int? count, decimal? num) + { + NameAge = nameAge; + Count = count; + Num = num; + } + + public override bool Equals(object obj) + { + return obj is SubStruct @struct && Equals(@struct); + } + + public bool Equals(SubStruct other) + { + return NameAge.Equals(other.NameAge) && + EqualityComparer.Default.Equals(Count, other.Count) && + EqualityComparer.Default.Equals(Num, other.Num); + } + } + + public struct NameAge : IEquatable + { + public readonly string Name; + public readonly int? Age; + + public NameAge(string name, int? age) + { + Name = name; + Age = age; + } + + public override bool Equals(object obj) + { + return obj is NameAge age && Equals(age); + } + + public bool Equals(NameAge other) + { + return Name == other.Name && + EqualityComparer.Default.Equals(Age, other.Age); + } + } + + + + public delegate ref TField GetFieldRefDelegate(ref TStruct targetStruct); + public delegate void ReadonlySetterDelegate(ref TStruct targetStruct, in TField value); + static class ReadonlySetter + { + static Dictionary getFieldRefMethods; + + public static GetFieldRefDelegate CreateGetFieldRef(FieldInfo field) + { + var fieldType = field.FieldType; + + MethodInfo asRef = typeof(Unsafe) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => m.Name == nameof(Unsafe.AsRef)) + .First(m => m.ReturnType == m.GetParameters()[0].ParameterType) + .MakeGenericMethod(fieldType); + + var targetStruct = Expression.Parameter(typeof(TStruct).MakeByRefType(), "targetStruct"); + + // (ref MyStruct targetStruct) => { return ref targetStruct.field; } + var lambda = Expression.Lambda( + delegateType: typeof(GetFieldRefDelegate), + body: Expression.Call(asRef, + Expression.MakeMemberAccess(targetStruct, field)), + parameters: targetStruct + ); + + return (GetFieldRefDelegate)lambda.Compile(); + } + + public static LambdaExpression CreateGetFieldRef(Type structType, FieldInfo field) + { + var fieldType = field.FieldType; + + MethodInfo asRef = typeof(Unsafe) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => m.Name == nameof(Unsafe.AsRef)) + .First(m => m.ReturnType == m.GetParameters()[0].ParameterType) + .MakeGenericMethod(fieldType); + + var targetStruct = Expression.Parameter(structType.MakeByRefType(), "targetStruct"); + + // (ref MyStruct targetStruct) => { return ref targetStruct.field; } + var lambda = Expression.Lambda( + delegateType: typeof(GetFieldRefDelegate<,>).MakeGenericType(structType, fieldType), + body: Expression.Call(asRef, + Expression.MakeMemberAccess(targetStruct, field)), + parameters: targetStruct + ); + + return lambda; + } + + public static (ReadonlySetterDelegate, LambdaExpression) GenerateSetter(FieldInfo targetField) { - Key = key; - Value = value; + var (del, lambda) = GenerateSetter(targetField); + return ((ReadonlySetterDelegate)del, lambda); } - public string Key { get; } - public decimal Value { get; } + public static (Delegate, LambdaExpression) GenerateSetter(FieldInfo targetField) + { + var structType = targetField.DeclaringType; + var fieldType = targetField.FieldType; + + MethodInfo asRef = typeof(Unsafe) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => m.Name == nameof(Unsafe.AsRef)) + .First(m => m.ReturnType == m.GetParameters()[0].ParameterType) + .MakeGenericMethod(fieldType); + + MethodInfo write = typeof(Unsafe) + .GetMethod(nameof(Unsafe.Write), BindingFlags.Static | BindingFlags.Public) + .MakeGenericMethod(fieldType); + + MethodInfo assign = typeof(Program) + .GetMethod(nameof(Program.Assign), BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .MakeGenericMethod(fieldType); + + + + var targetStruct = Expression.Parameter(structType.MakeByRefType(), "targetStruct"); + var value = Expression.Parameter(fieldType.MakeByRefType(), "value"); + + var body = new List(); + + // void* ptr = AsPointer(ref myStruct.MyReadonlyField); + // Write(ptr, value); + + + var getFieldRefExp = CreateGetFieldRef(structType, targetField); + //var getFieldRef = getFieldRefExp.Compile(); + // (ref getFieldRef(ref targetStruct)) = value; + //var refTarget = Expression.Invoke(getFieldRefExp, targetStruct); + var refTarget = Expression.MakeMemberAccess(targetStruct, targetField); + + body.Add( + Expression.Call(assign, + refTarget, + value + ) + ); + + + + var lambda = Expression.Lambda( + delegateType: typeof(ReadonlySetterDelegate<,>).MakeGenericType(structType, fieldType), + body: Expression.Block(body), + targetStruct, value); + + var del = lambda.Compile(); + + return (del, lambda); + } } + static void Assign(ref T target, in T value) + { + target = value; + + // ldarg.0 + // ldflda string [LiveTesting]LiveTesting.Program/NameAge::Name + // ldarg.1 + // call void [LiveTesting]LiveTesting.Program::Assign(!!0&, !!0&) + // ret + } static unsafe void Main(string[] args) { - var config = new SerializerConfig(); + /* + var nameAge = new NameAge("riki", null); + + var (nameSetter, nameSetterLambda) = ReadonlySetter.GenerateSetter(GetField(() => nameAge.Name)); + var (ageSetter, ageSetterLambda) = ReadonlySetter.GenerateSetter(GetField(() => nameAge.Age)); + + + SaveDelegateIL(nameSetterLambda); // operation can destabilize the runtime + + + var newName = "changed!"; + int? newAge = 12; + + nameSetter(ref nameAge, newName); + ageSetter(ref nameAge, newAge); + + var ageField = GetField(()=>nameAge.Age); + object boxedNameAge = nameAge; + object boxedNewAgeValue = newAge; + MicroBenchmark.Run(2, + new BenchJob("setValue(box, box)", () => ageField.SetValue(boxedNameAge, boxedNewAgeValue)), + new BenchJob("ageSetter", () => ageSetter(ref nameAge, newAge)) + ); + + Console.ReadLine(); + */ + + + + var eqOpNullableTest = StructEquality.EqualFunction; + + + var name = "abc"; + var a = new NullableWrapper(new Test { Value = 2, SubStruct = new SubStruct(new NameAge(name, 8), 1111, 3) }); + var b = new NullableWrapper(new Test { Value = 2, SubStruct = new SubStruct(new NameAge(name, 8), 5, 3) }); + var c = new NullableWrapper(new Test { Value = 2, SubStruct = new SubStruct(new NameAge(name, 8), 5, 3) }); + + // 1: Does it work? + var ab = eqOpNullableTest(ref a.TestStruct, ref b.TestStruct); + var bc = eqOpNullableTest(ref b.TestStruct, ref c.TestStruct); + + + MicroBenchmark.Run(2, + new BenchJob("StructEquality.AreEqual()", () => StructEquality.AreEqual(ref b.TestStruct, ref c.TestStruct)), + new BenchJob("object.Equals(left, right)", () => object.Equals(b, c) ) + //new BenchJob("left.Equals(right)", () => b.TestStruct.Equals(c.TestStruct) ), + ); + Console.ReadLine(); + + + SaveDelegateIL(StructEquality.Lambda); + + + var config = new SerializerConfig { DefaultTargets = TargetMember.AllFields, PreserveReferences = false }; config.Advanced.ReadonlyFieldHandling = ReadonlyFieldHandling.ForcedOverwrite; config.Advanced.SkipCompilerGeneratedFields = false; - config.DefaultTargets = TargetMember.PrivateFields; + config.OnConfigNewType = tc => tc.TypeConstruction = TypeConstruction.ByUninitialized(); - // Next two lines do exactly the same - config.ConfigType(typeof(CustomProperty)).TypeConstruction = TypeConstruction.ByUninitialized(); - config.ConfigType().ConstructByUninitialized(); - var ceras = new CerasSerializer(config); - var obj = new CustomProperty("abc", 123.456M); - - var report = ceras.GenerateSerializationDebugReport(typeof(CustomProperty)); + var obj = new NullableWrapper(new Test { Value = 123.456M }); var data = ceras.Serialize(obj); - var clone = ceras.Deserialize(data); + var clone = ceras.Deserialize(data); + new Ceras.Test.BuiltInTypes().MultidimensionalArrays(); @@ -1575,6 +1827,36 @@ static ConstructorInfo GetCtor(Expression> e) throw new ArgumentException(); } + + static FieldInfo GetField(Expression> e) + { + var b = e.Body; + + if (b is MemberExpression m) + if (m.Member is FieldInfo f) + return f; + + throw new ArgumentException(); + } + + [Conditional("NET47")] + static void SaveDelegateIL(LambdaExpression lambdaExp, string fileName = "dyn.dll", string methodName = "Method") + { +#if NET47 + var da = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("dyn"), AssemblyBuilderAccess.Save); + + var dm = da.DefineDynamicModule("dyn_mod", "dyn.dll"); + var dt = dm.DefineType("dyn_type"); + var method = dt.DefineMethod( + methodName, + MethodAttributes.Public | MethodAttributes.Static); + + lambdaExp.CompileToMethod(method); + dt.CreateType(); + + da.Save(fileName); +#endif + } } diff --git a/src/Ceras.AotGenerator/Helpers/TypeNameHelper.cs b/src/Ceras.AotGenerator/Helpers/TypeNameHelper.cs index ad0990e..b0b2780 100644 --- a/src/Ceras.AotGenerator/Helpers/TypeNameHelper.cs +++ b/src/Ceras.AotGenerator/Helpers/TypeNameHelper.cs @@ -36,25 +36,10 @@ public static string TryGetPrimitiveName(Type type) } // returns something like - // "List" + // List public static string ToFriendlyName(this Type type, bool fullName = false) { - var primitiveName = TryGetPrimitiveName(type); - if(primitiveName != null) - return primitiveName; - - var name = fullName ? type.FullName : type.Name; - if (type.IsGenericType) - { - return name.Split('`')[0] // base name - + "<" - + string.Join(", ", type.GetGenericArguments().Select(t => t.ToFriendlyName(fullName)).ToArray()) - + ">"; - } - else - { - return name; - } + return Ceras.Helpers.ReflectionHelper.FriendlyName(type, fullName); } // returns something like diff --git a/src/Ceras/Helpers/ReflectionHelper.cs b/src/Ceras/Helpers/ReflectionHelper.cs index e851af2..d2022be 100644 --- a/src/Ceras/Helpers/ReflectionHelper.cs +++ b/src/Ceras/Helpers/ReflectionHelper.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; - static class ReflectionHelper + public static class ReflectionHelper { static readonly Dictionary _typeToBlittableSize = new Dictionary(); static readonly Dictionary _typeToUnsafeSize = new Dictionary(); @@ -634,8 +634,10 @@ public static string FriendlyName(this Type type, bool fullName = false) if (type.IsGenericType) { - var n = fullName ? type.FullName : type.Name; - return n.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(t => t.FriendlyName(fullName)).ToArray()) + ">"; + var name = fullName ? type.FullName : type.Name; + var baseName = name.Split('`')[0].Replace("+", "."); + + return baseName + "<" + string.Join(", ", type.GetGenericArguments().Select(t => t.FriendlyName(fullName)).ToArray()) + ">"; } else { diff --git a/src/Ceras/Helpers/StructEquality.cs b/src/Ceras/Helpers/StructEquality.cs new file mode 100644 index 0000000..715b02b --- /dev/null +++ b/src/Ceras/Helpers/StructEquality.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + + +namespace Ceras.Helpers +{ + using static Expression; + + public delegate bool EqualsDelegate(ref TStruct left, ref TStruct right); + static class StructEquality + { + public static EqualsDelegate EqualFunction { get; } + public static LambdaExpression Lambda { get; } + + + public static bool AreEqual(ref T left, ref T right) => EqualFunction(ref left, ref right); + + + static StructEquality() + { + if (!typeof(T).IsValueType || typeof(T).IsPrimitive) + throw new InvalidOperationException("T must be a non-primitive value type (a struct)"); + + (EqualFunction, Lambda) = GenerateEq(); + } + + static (EqualsDelegate, LambdaExpression) GenerateEq() + { + var type = typeof(T); + + var left = Parameter(type.MakeByRefType(), "left"); + var right = Parameter(type.MakeByRefType(), "right"); + var methodEnd = Label(typeof(bool), "methodEnd"); + var body = new List(); + + foreach (var f in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var fLeft = Field(left, f); + var fRight = Field(right, f); + + // "if( left.f != right.f ) return false;" + body.Add(IfThen( + Not(AreFieldsEqual(f.FieldType, fLeft, fRight)), + Return(methodEnd, Constant(false)) + )); + } + + // "return true;" + body.Add(Label(methodEnd, Constant(true))); + + var lambda = Expression.Lambda( + delegateType: typeof(EqualsDelegate<>).MakeGenericType(type), + body: Block(body), + left, right); + var del = lambda.Compile(); + + return ((EqualsDelegate)del, lambda); + } + + static Expression AreFieldsEqual(Type fieldType, MemberExpression leftField, MemberExpression rightField) + { + // ReferenceTypes: ReferenceEqual() + if (!fieldType.IsValueType) + return ReferenceEqual(leftField, rightField); + + // Primitives: Equal() + if (fieldType.IsPrimitive) + return Equal(leftField, rightField); + + // Try custom equality operator if it exists + try + { + var customEq = Equal(leftField, rightField); + return customEq; + } + catch { } + + // Structs: Recurse into AreEqual() + + const bool resolveLambda = true; + + if (resolveLambda) + { + var lambdaProp = typeof(StructEquality<>).MakeGenericType(fieldType).GetProperty(nameof(Lambda)); + var eqLambda = (LambdaExpression)lambdaProp.GetValue(null); + return Invoke(eqLambda, leftField, rightField); + } + else + { + var eqMethod = typeof(StructEquality<>).MakeGenericType(fieldType).GetMethod(nameof(AreEqual), BindingFlags.Static | BindingFlags.Public); + return Call(method: eqMethod, arg0: leftField, arg1: rightField); + } + } + + } + +}