Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Serialization/Converters/CbClassConverter.cs
2025-05-18 13:04:45 +08:00

553 lines
20 KiB
C#

// 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<T> 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<Type, CbClassConverterMethods> 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<CbPolymorphicAttribute>(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<object>.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<object>.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<Utf8String, Type> 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<CbDiscriminatorAttribute>() ?? throw new NotSupportedException();
discriminatorToKnownType[new Utf8String(discriminator.Name)] = knownType;
}
}
}
}
// Populate the dictionary
Dictionary<Utf8String, Func<CbField, object>> nameToReadFunc = (Dictionary<Utf8String, Func<CbField, object>>)dispatchType.GetField(nameof(CbPolymorphicDispatch<object>.NameToReadFunc))!.GetValue(null)!;
Dictionary<Type, Action<CbWriter, object>> typeToWriteContentsFunc = (Dictionary<Type, Action<CbWriter, object>>)dispatchType.GetField(nameof(CbPolymorphicDispatch<object>.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<Func<CbField, object>>(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<Action<CbWriter, object>>(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<CbWriter>(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<CbWriter>(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<CbWriter>(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<CbWriter>(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<object>.Names))!;
namesField.SetValue(null, properties.Select(x => x.Name).ToArray());
// Write the discriminator
CbDiscriminatorAttribute? discriminator = type.GetCustomAttribute<CbDiscriminatorAttribute>();
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<CbWriter>(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<T>
{
public static Dictionary<Utf8String, Func<CbField, object>> NameToReadFunc = [];
public static Dictionary<Type, Action<CbWriter, object>> 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<T>(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<object>.Names))!;
namesField.SetValue(null, properties.Select(x => x.Name).ToArray());
MethodInfo matchNameMethod = reflectedType.GetMethod(nameof(CbReflectedTypeInfo<object>.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<CbField>(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<CbFieldIterator>(x => x.IsValid()), null);
Label returnLabel = generator.DefineLabel();
generator.Emit(OpCodes.Brfalse, returnLabel);
// NamesLocal = CbReflectedTypeInfo<Type>.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<CbFieldIterator>(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<Type>.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<CbFieldIterator>(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<CbFieldIterator>(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<CbFieldIterator>(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, TElement>(TCollection source, TCollection target) where TCollection : ICollection<TElement>
{
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<CbIgnoreAttribute>();
if (ignoreAttribute == null)
{
CbFieldAttribute? attribute = property.GetCustomAttribute<CbFieldAttribute>();
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<CbObjectAttribute>() == null)
{
throw new CbEmptyClassException(type);
}
return [.. propertyList];
}
static FieldInfo GetFieldInfo<T>(Expression<Func<T>> expr)
{
return (FieldInfo)((MemberExpression)expr.Body).Member;
}
static MethodInfo GetMethodInfo(Expression<Action> expr)
{
return ((MethodCallExpression)expr.Body).Method;
}
static MethodInfo GetMethodInfo<T>(Expression<Action<T>> expr)
{
return ((MethodCallExpression)expr.Body).Method;
}
}
class CbClassConverter<T> : CbConverter<T>, ICbConverterMethods where T : class
{
readonly CbClassConverterMethods _methods;
readonly Func<CbField, T> _readFunc;
readonly Action<CbWriter, T> _writeFunc;
readonly Action<CbWriter, CbFieldName, T> _writeNamedFunc;
public CbClassConverter()
{
_methods = CbClassConverterMethods.Create(typeof(T));
_readFunc = CreateDelegate<Func<CbField, T>>(_methods.ReadMethod);
_writeFunc = CreateDelegate<Action<CbWriter, T>>(_methods.WriteMethod);
_writeNamedFunc = CreateDelegate<Action<CbWriter, CbFieldName, T>>(_methods.WriteNamedMethod);
}
public MethodInfo ReadMethod => _methods.ReadMethod;
public MethodInfo WriteMethod => _methods.WriteMethod;
public MethodInfo WriteNamedMethod => _methods.WriteNamedMethod;
static TDelegate CreateDelegate<TDelegate>(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;
}
}
}