// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using EpicGames.Core; namespace EpicGames.Serialization.Converters { class CbClassConverterMethods { class CbReflectedTypeInfo where T : class { public static Utf8String[]? Names = null; public static PropertyInfo[]? Properties = null; public static bool MatchName(CbField field, int idx) { return field.Name == Names![idx]; } } static readonly Utf8String s_discriminatorKey = new Utf8String("_t"); public Type ClassType { get; } public bool IsPolymorphic { get; } public DynamicMethod ReadMethod { get; } public DynamicMethod WriteMethod { get; } public DynamicMethod WriteNamedMethod { get; } public DynamicMethod WriteContentsMethod { get; } public DynamicMethod ReadConcreteMethod { get; } public DynamicMethod WriteConcreteContentsMethod { get; } static readonly Dictionary s_typeToMethods = []; public static CbClassConverterMethods Create(Type classType) { CbClassConverterMethods? methods; if (!s_typeToMethods.TryGetValue(classType, out methods)) { methods = new CbClassConverterMethods(classType); s_typeToMethods.Add(classType, methods); methods.GenerateBytecode(); } return methods; } private CbClassConverterMethods(Type classType) { ClassType = classType; IsPolymorphic = (classType.GetCustomAttribute(true) != null); ReadConcreteMethod = new DynamicMethod($"ReadConcrete_{classType.Name}", classType, [typeof(CbField)]); WriteConcreteContentsMethod = new DynamicMethod($"WriteConcreteContents_{classType.Name}", null, [typeof(CbWriter), classType]); WriteMethod = new DynamicMethod($"Write_{classType.Name}", null, [typeof(CbWriter), classType]); WriteNamedMethod = new DynamicMethod($"WriteNamed_{classType.Name}", null, [typeof(CbWriter), typeof(CbFieldName), classType]); if (IsPolymorphic) { ReadMethod = new DynamicMethod($"Read_{classType.Name}", classType, [typeof(CbField)]); WriteContentsMethod = new DynamicMethod($"WriteContents_{classType.Name}", null, [typeof(CbWriter), classType]); } else { ReadMethod = ReadConcreteMethod; WriteContentsMethod = WriteConcreteContentsMethod; } } private void GenerateBytecode() { // Create the regular methods CreateConcreteObjectReader(ClassType, ReadConcreteMethod.GetILGenerator()); CreateConcreteObjectContentsWriter(ClassType, WriteConcreteContentsMethod.GetILGenerator()); CreateObjectWriter(WriteMethod.GetILGenerator(), WriteContentsMethod); CreateNamedObjectWriter(WriteNamedMethod.GetILGenerator(), WriteContentsMethod); // Create the extra polymorphic methods if (IsPolymorphic) { // Create the dispatch type Type dispatchType = typeof(CbPolymorphicDispatch<>).MakeGenericType(ClassType); // Create the read dispatch method { ILGenerator generator = ReadMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Call, dispatchType.GetMethod(nameof(CbPolymorphicDispatch.Read))!); generator.Emit(OpCodes.Ret); } // Create the write dispatch method { ILGenerator generator = WriteContentsMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Call, dispatchType.GetMethod(nameof(CbPolymorphicDispatch.WriteContents))!); generator.Emit(OpCodes.Ret); } // Finally, update the dispatch type with all the methods. We should be safe on the recursive path now. PopulateDispatchType(ClassType, dispatchType); } } static void PopulateDispatchType(Type classType, Type dispatchType) { Dictionary discriminatorToKnownType = []; Type[] knownTypes = classType.Assembly.GetTypes(); foreach (Type knownType in knownTypes) { if (knownType.IsClass && !knownType.IsAbstract) { for (Type? baseType = knownType; baseType != null; baseType = baseType.BaseType) { if (baseType == classType) { CbDiscriminatorAttribute discriminator = knownType.GetCustomAttribute() ?? throw new NotSupportedException(); discriminatorToKnownType[new Utf8String(discriminator.Name)] = knownType; } } } } // Populate the dictionary Dictionary> nameToReadFunc = (Dictionary>)dispatchType.GetField(nameof(CbPolymorphicDispatch.NameToReadFunc))!.GetValue(null)!; Dictionary> typeToWriteContentsFunc = (Dictionary>)dispatchType.GetField(nameof(CbPolymorphicDispatch.TypeToWriteContentsFunc))!.GetValue(null)!; foreach ((Utf8String name, Type knownType) in discriminatorToKnownType) { CbClassConverterMethods methods = Create(knownType); { DynamicMethod dynamicMethod = new DynamicMethod("_", typeof(object), [typeof(CbField)]); ILGenerator generator = dynamicMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Call, methods.ReadConcreteMethod); generator.Emit(OpCodes.Castclass, typeof(object)); generator.Emit(OpCodes.Ret); nameToReadFunc[name] = CreateDelegate>(dynamicMethod); } { DynamicMethod dynamicMethod = new DynamicMethod("_", null, [typeof(CbWriter), typeof(object)]); ILGenerator generator = dynamicMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Castclass, knownType); generator.Emit(OpCodes.Call, methods.WriteConcreteContentsMethod); generator.Emit(OpCodes.Ret); typeToWriteContentsFunc[knownType] = CreateDelegate>(dynamicMethod); } } } static void CreateObjectWriter(ILGenerator generator, DynamicMethod contentsWriter) { generator.Emit(OpCodes.Ldarg_1); Label skipLabel = generator.DefineLabel(); generator.Emit(OpCodes.Brfalse, skipLabel); generator.Emit(OpCodes.Ldarg_0); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.BeginObject()), null); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.EmitCall(OpCodes.Call, contentsWriter, null); generator.Emit(OpCodes.Ldarg_0); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.EndObject()), null); generator.MarkLabel(skipLabel); generator.Emit(OpCodes.Ret); } static void CreateNamedObjectWriter(ILGenerator generator, DynamicMethod contentsWriter) { generator.Emit(OpCodes.Ldarg_2); Label skipLabel = generator.DefineLabel(); generator.Emit(OpCodes.Brfalse, skipLabel); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.BeginObject(default)), null); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_2); generator.EmitCall(OpCodes.Call, contentsWriter, null); generator.Emit(OpCodes.Ldarg_0); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.EndObject()), null); generator.MarkLabel(skipLabel); generator.Emit(OpCodes.Ret); } static void CreateConcreteObjectContentsWriter(Type type, ILGenerator generator) { // Find the reflected properties from this type (Utf8String Name, PropertyInfo Property)[] properties = GetProperties(type); // Create a static type with the required reflection data Type reflectedType = typeof(CbReflectedTypeInfo<>).MakeGenericType(type); FieldInfo namesField = reflectedType.GetField(nameof(CbReflectedTypeInfo.Names))!; namesField.SetValue(null, properties.Select(x => x.Name).ToArray()); // Write the discriminator CbDiscriminatorAttribute? discriminator = type.GetCustomAttribute(); if (discriminator != null) { FieldInfo discriminatorKeyField = GetFieldInfo(() => s_discriminatorKey); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldsfld, discriminatorKeyField); generator.Emit(OpCodes.Ldstr, discriminator.Name); generator.Emit(OpCodes.Call, GetMethodInfo(x => x.WriteString(default, null!))); } // Write all the remaining properties for (int idx = 0; idx < properties.Length; idx++) { PropertyInfo property = properties[idx].Property; Type propertyType = property.PropertyType; // Get the field value generator.Emit(OpCodes.Ldarg_1); generator.EmitCall(OpCodes.Call, property.GetMethod!, null); Label skipLabel = generator.DefineLabel(); MethodInfo writeMethod; if (s_typeToMethods.TryGetValue(propertyType, out CbClassConverterMethods? dynamicMethods)) { generator.Emit(OpCodes.Dup); generator.Emit(OpCodes.Brfalse, skipLabel); writeMethod = dynamicMethods.WriteNamedMethod; } else { ICbConverterMethods methods = CbConverterMethods.Get(property); writeMethod = methods.WriteNamedMethod; } // Store the variable in a local LocalBuilder local = generator.DeclareLocal(propertyType); generator.Emit(OpCodes.Dup); generator.Emit(OpCodes.Stloc, local); // Call the writer generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldsfld, namesField); generator.Emit(OpCodes.Ldc_I4, idx); generator.Emit(OpCodes.Ldelem, typeof(CbFieldName)); generator.Emit(OpCodes.Ldloc, local); generator.EmitCall(OpCodes.Call, writeMethod, null); // Remove the duplicated value from the top of the stack generator.MarkLabel(skipLabel); generator.Emit(OpCodes.Pop); } generator.Emit(OpCodes.Ret); } class CbPolymorphicDispatch { public static Dictionary> NameToReadFunc = []; public static Dictionary> TypeToWriteContentsFunc = []; public static object Read(CbField field) { Utf8String name = field.AsObject().Find(s_discriminatorKey).AsUtf8String(); return NameToReadFunc[name](field); } public static void WriteContents(CbWriter writer, object value) { Type type = value!.GetType(); TypeToWriteContentsFunc[type](writer, value); } } static T CreateDelegate(DynamicMethod method) where T : Delegate { return (T)method.CreateDelegate(typeof(T)); } static void CreateConcreteObjectReader(Type type, ILGenerator generator) { // Construct the object ConstructorInfo? constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) ?? throw new CbException($"Unable to find default constructor for {type}"); // Find the reflected properties from this type (Utf8String Name, PropertyInfo Property)[] properties = GetProperties(type); // Create a static type with the required reflection data Type reflectedType = typeof(CbReflectedTypeInfo<>).MakeGenericType(type); FieldInfo namesField = reflectedType.GetField(nameof(CbReflectedTypeInfo.Names))!; namesField.SetValue(null, properties.Select(x => x.Name).ToArray()); MethodInfo matchNameMethod = reflectedType.GetMethod(nameof(CbReflectedTypeInfo.MatchName))!; // NewObjectLocal = new Type() LocalBuilder newObjectLocal = generator.DeclareLocal(typeof(object)); generator.Emit(OpCodes.Newobj, constructor); generator.Emit(OpCodes.Stloc, newObjectLocal); // Stack(0) = CbField.CreateIterator() generator.Emit(OpCodes.Ldarg_0); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.CreateIterator()), null); // CbFieldIterator IteratorLocal = Stack(0) LocalBuilder iteratorLocal = generator.DeclareLocal(typeof(CbFieldIterator)); generator.Emit(OpCodes.Dup); generator.Emit(OpCodes.Stloc, iteratorLocal); // if(!Stack.Pop().IsValid()) goto ReturnLabel generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.IsValid()), null); Label returnLabel = generator.DefineLabel(); generator.Emit(OpCodes.Brfalse, returnLabel); // NamesLocal = CbReflectedTypeInfo.Names LocalBuilder namesLocal = generator.DeclareLocal(typeof(Utf8String[])); generator.Emit(OpCodes.Ldsfld, namesField); generator.Emit(OpCodes.Stloc, namesLocal); // IterationLoopLabel: Label iterationLoopLabel = generator.DefineLabel(); generator.MarkLabel(iterationLoopLabel); // bool MatchLocal = false LocalBuilder matchLocal = generator.DeclareLocal(typeof(bool)); generator.Emit(OpCodes.Ldc_I4_0); generator.Emit(OpCodes.Stloc, matchLocal); // Stack(0) = IteratorLocal.GetCurrent() generator.Emit(OpCodes.Ldloc, iteratorLocal); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.GetCurrent()), null); // Try to parse each of the properties in order. If fields are ordered correctly, we will parse the object in a single pass. Otherwise we can loop and start again. LocalBuilder fieldLocal = generator.DeclareLocal(typeof(CbField)); for (int idx = 0; idx < properties.Length; idx++) { PropertyInfo property = properties[idx].Property; // Get the read method for this property type MethodInfo readMethod; if (s_typeToMethods.TryGetValue(property.PropertyType, out CbClassConverterMethods? dynamicMethods)) { readMethod = dynamicMethods.ReadMethod; } else { readMethod = CbConverterMethods.Get(property).ReadMethod; } // if(!CbReflectedTypeInfo.MatchName(Stack(0), Idx)) goto SkipPropertyLabel Label skipPropertyLabel = generator.DefineLabel(); generator.Emit(OpCodes.Dup); // Current CbField generator.Emit(OpCodes.Ldc_I4, idx); generator.Emit(OpCodes.Call, matchNameMethod); generator.Emit(OpCodes.Brfalse, skipPropertyLabel); // FieldLocal = Stack.Pop() generator.Emit(OpCodes.Stloc, fieldLocal); // Copy the collection over if necessary if (property.SetMethod != null) { // Property.SetMethod(NewObjectLocal, ReadMethod(FieldLocal)) generator.Emit(OpCodes.Ldloc, newObjectLocal); generator.Emit(OpCodes.Ldloc, fieldLocal); generator.EmitCall(OpCodes.Call, readMethod, null); generator.EmitCall(OpCodes.Call, property.SetMethod!, null); } else if (TryGetCollectionCopyMethod(property.PropertyType, out MethodInfo? copyMethod)) { // CopyMethod(ReadMethod(FieldLocal), Property.GetMethod(NewObjectLocal)) generator.Emit(OpCodes.Ldloc, fieldLocal); generator.EmitCall(OpCodes.Call, readMethod, null); generator.Emit(OpCodes.Ldloc, newObjectLocal); generator.EmitCall(OpCodes.Call, property.GetMethod!, null); generator.EmitCall(OpCodes.Call, copyMethod, null); } else { throw new CbException($"Unable to write to property {property.Name}"); } // if(!IteratorLocal.MoveNext()) goto ReturnLabel generator.Emit(OpCodes.Ldloc, iteratorLocal); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.MoveNext()), null); generator.Emit(OpCodes.Brfalse, returnLabel); // MatchLocal = true generator.Emit(OpCodes.Ldc_I4_1); generator.Emit(OpCodes.Stloc, matchLocal); // Stack(0) = IteratorLocal.GetCurrent() generator.Emit(OpCodes.Ldloc, iteratorLocal); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.GetCurrent()), null); // SkipPropertyLabel: generator.MarkLabel(skipPropertyLabel); } // Stack.Pop() generator.Emit(OpCodes.Pop); // Current CbField // if(MatchLocal) goto IterationLoopLabel generator.Emit(OpCodes.Ldloc, matchLocal); generator.Emit(OpCodes.Brtrue, iterationLoopLabel); // if(IteratorLocal.MoveNext()) goto IterationLoopLabel generator.Emit(OpCodes.Ldloc, iteratorLocal); generator.EmitCall(OpCodes.Call, GetMethodInfo(x => x.MoveNext()), null); generator.Emit(OpCodes.Brtrue, iterationLoopLabel); // return NewObjectLocal generator.MarkLabel(returnLabel); generator.Emit(OpCodes.Ldloc, newObjectLocal); generator.Emit(OpCodes.Ret); } static bool TryGetCollectionCopyMethod(Type type, [NotNullWhen(true)] out MethodInfo? method) { foreach (Type interfaceType in type.GetInterfaces()) { if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) { MethodInfo genericMethod = typeof(CbClassConverterMethods).GetMethod(nameof(CopyCollection), BindingFlags.Static | BindingFlags.NonPublic)!; method = genericMethod.MakeGenericMethod(interfaceType, interfaceType.GetGenericArguments()[0]); return true; } } method = null; return false; } static void CopyCollection(TCollection source, TCollection target) where TCollection : ICollection { foreach (TElement element in source) { target.Add(element); } } static (Utf8String, PropertyInfo)[] GetProperties(Type type) { List<(Utf8String, PropertyInfo)> propertyList = []; foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { CbIgnoreAttribute? ignoreAttribute = property.GetCustomAttribute(); if (ignoreAttribute == null) { CbFieldAttribute? attribute = property.GetCustomAttribute(); if (attribute != null || (property.GetGetMethod()?.IsPublic ?? false)) { Utf8String name = new Utf8String(attribute?.Name ?? property.Name); propertyList.Add((name, property)); } } } if (propertyList.Count == 0 && type.GetCustomAttribute() == null) { throw new CbEmptyClassException(type); } return [.. propertyList]; } static FieldInfo GetFieldInfo(Expression> expr) { return (FieldInfo)((MemberExpression)expr.Body).Member; } static MethodInfo GetMethodInfo(Expression expr) { return ((MethodCallExpression)expr.Body).Method; } static MethodInfo GetMethodInfo(Expression> expr) { return ((MethodCallExpression)expr.Body).Method; } } class CbClassConverter : CbConverter, ICbConverterMethods where T : class { readonly CbClassConverterMethods _methods; readonly Func _readFunc; readonly Action _writeFunc; readonly Action _writeNamedFunc; public CbClassConverter() { _methods = CbClassConverterMethods.Create(typeof(T)); _readFunc = CreateDelegate>(_methods.ReadMethod); _writeFunc = CreateDelegate>(_methods.WriteMethod); _writeNamedFunc = CreateDelegate>(_methods.WriteNamedMethod); } public MethodInfo ReadMethod => _methods.ReadMethod; public MethodInfo WriteMethod => _methods.WriteMethod; public MethodInfo WriteNamedMethod => _methods.WriteNamedMethod; static TDelegate CreateDelegate(DynamicMethod method) where TDelegate : Delegate => (TDelegate)method.CreateDelegate(typeof(TDelegate)); public override T Read(CbField field) => _readFunc(field); public override void Write(CbWriter writer, T value) => _writeFunc(writer, value); public override void WriteNamed(CbWriter writer, CbFieldName name, T value) => _writeNamedFunc(writer, name, value); } class CbClassConverterFactory : CbConverterFactory { public override CbConverter? CreateConverter(Type type) { CbConverter? converter = null; if (type.IsClass) { Type converterType = typeof(CbClassConverter<>).MakeGenericType(type); try { converter = (CbConverter?)Activator.CreateInstance(converterType); } catch (TargetInvocationException ex) when (ex.InnerException is not null) { throw ex.InnerException; } } return converter; } } }