using IPA.Config.Data; using IPA.Config.Stores.Attributes; using IPA.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.IO; using Boolean = IPA.Config.Data.Boolean; using System.Collections; using IPA.Utilities; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; #endif [assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)] namespace IPA.Config.Stores { /// /// A class providing an extension for to make it easy to use generated /// config stores. /// public static class GeneratedExtension { /// /// The name of the assembly that internals must be visible to to allow internal protection. /// public const string AssemblyVisibilityTarget = GeneratedStore.GeneratedAssemblyName; /// /// Creates a generated of type , registers it to /// the object, and returns it. This also forces a synchronous config load via /// if is . /// /// /// /// must be a public non- class. /// It can also be internal, but in that case, then your assembly must have the following attribute /// to allow the generated code to reference it. /// /// [assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)] /// /// /// /// Only fields and properties that are public or protected will be considered, and only properties /// where both the getter and setter are public or protected are considered. Any fields or properties /// with an applied to them are also ignored. Having properties be is not strictly /// necessary, however it allows the generated type to keep track of changes and lock around them so that the config will auto-save. /// /// /// All of the attributes in the namespace are handled as described by them. /// /// /// If the declares a public or protected, /// method Changed(), then that method may be called to artificially signal to the runtime that the content of the object /// has changed. That method will also be called after the write locks are released when a property is set anywhere in the owning /// tree. This will only be called on the outermost generated object of the config structure, even if the change being signaled /// is somewhere deep into the tree. TODO: is this a good idea? /// /// /// Similarly, can declare a public or protected, /// method OnReload(), which will be called on the filesystem reader thread after the object has been repopulated with new data /// values. It will be called after the write lock for this object is released. This will only be called on the outermost generated /// object of the config structure. /// /// /// TODO: describe details of generated stores /// /// /// the type to wrap /// the to register to /// whether to synchronously load the content, or trigger an async load /// a generated instance of as a special public static T Generated(this Config cfg, bool loadSync = true) where T : class { var ret = GeneratedStore.Create(); cfg.SetStore(ret as IConfigStore); if (loadSync) cfg.LoadSync(); else cfg.LoadAsync(); return ret; } } internal static class GeneratedStore { internal interface IGeneratedStore { Type Type { get; } IGeneratedStore Parent { get; } Impl Impl { get; } void OnReload(); void Changed(); Value Serialize(); void Deserialize(Value val); } internal class Impl : IConfigStore { private IGeneratedStore generated; internal static ConstructorInfo Ctor = typeof(Impl).GetConstructor(new[] { typeof(IGeneratedStore) }); public Impl(IGeneratedStore store) => generated = store; private readonly AutoResetEvent resetEvent = new AutoResetEvent(false); public WaitHandle SyncObject => resetEvent; internal static MethodInfo SyncObjectGetMethod = typeof(Impl).GetProperty(nameof(SyncObject)).GetGetMethod(); public ReaderWriterLockSlim WriteSyncObject { get; } = new ReaderWriterLockSlim(); internal static MethodInfo WriteSyncObjectGetMethod = typeof(Impl).GetProperty(nameof(WriteSyncObject)).GetGetMethod(); internal static MethodInfo ImplSignalChangedMethod = typeof(Impl).GetMethod(nameof(ImplSignalChanged)); public static void ImplSignalChanged(IGeneratedStore s) => FindImpl(s).SignalChanged(); public void SignalChanged() => resetEvent.Set(); internal static MethodInfo ImplInvokeChangedMethod = typeof(Impl).GetMethod(nameof(ImplInvokeChanged)); public static void ImplInvokeChanged(IGeneratedStore s) => FindImpl(s).InvokeChanged(); public void InvokeChanged() => generated.Changed(); internal static MethodInfo ImplTakeReadMethod = typeof(Impl).GetMethod(nameof(ImplTakeRead)); public static void ImplTakeRead(IGeneratedStore s) => FindImpl(s).TakeRead(); public void TakeRead() => WriteSyncObject.EnterReadLock(); internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead)); public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead(); public void ReleaseRead() => WriteSyncObject.ExitReadLock(); internal static MethodInfo ImplTakeWriteMethod = typeof(Impl).GetMethod(nameof(ImplTakeWrite)); public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s).TakeWrite(); public void TakeWrite() => WriteSyncObject.EnterWriteLock(); internal static MethodInfo ImplReleaseWriteMethod = typeof(Impl).GetMethod(nameof(ImplReleaseWrite)); public static void ImplReleaseWrite(IGeneratedStore s) => FindImpl(s).ReleaseWrite(); public void ReleaseWrite() => WriteSyncObject.ExitWriteLock(); internal static MethodInfo FindImplMethod = typeof(Impl).GetMethod(nameof(FindImpl)); public static Impl FindImpl(IGeneratedStore store) { while (store?.Parent != null) store = store.Parent; // walk to the top of the tree return store?.Impl; } internal static MethodInfo ReadFromMethod = typeof(Impl).GetMethod(nameof(ReadFrom)); public void ReadFrom(IConfigProvider provider) { var values = provider.Load(); Logger.config.Debug("Generated impl ReadFrom"); Logger.config.Debug($"Read {values}"); generated.Deserialize(values); ReleaseWrite(); generated.OnReload(); TakeWrite(); // must take again for runtime to be happy (which is unfortunate) } internal static MethodInfo WriteToMethod = typeof(Impl).GetMethod(nameof(WriteTo)); public void WriteTo(IConfigProvider provider) { var values = generated.Serialize(); Logger.config.Debug("Generated impl WriteTo"); Logger.config.Debug($"Serialized {values}"); provider.Store(values); } } private static readonly Dictionary generatedCreators = new Dictionary(); public static T Create() where T : class => (T)Create(typeof(T)); public static IConfigStore Create(Type type) => Create(type, null); private static readonly MethodInfo CreateGParent = typeof(GeneratedStore).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.Any, new[] { typeof(IGeneratedStore) }, Array.Empty()); internal static T Create(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent); private static IConfigStore Create(Type type, IGeneratedStore parent) => GetCreator(type)(parent); internal static GeneratedStoreCreator GetCreator(Type t) { if (generatedCreators.TryGetValue(t, out var gen)) return gen.ctor; else { gen = MakeCreator(t); generatedCreators.Add(t, gen); return gen.ctor; } } internal static Type GetGeneratedType(Type t) { if (generatedCreators.TryGetValue(t, out var gen)) return gen.type; else { gen = MakeCreator(t); generatedCreators.Add(t, gen); return gen.type; } } internal const string GeneratedAssemblyName = "IPA.Config.Generated"; private static AssemblyBuilder assembly = null; private static AssemblyBuilder Assembly { get { if (assembly == null) { var name = new AssemblyName(GeneratedAssemblyName); assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave); } return assembly; } } internal static void DebugSaveAssembly(string file) { Assembly.Save(file); } private static ModuleBuilder module = null; private static ModuleBuilder Module { get { if (module == null) module = Assembly.DefineDynamicModule(Assembly.GetName().Name, Assembly.GetName().Name + ".dll"); return module; } } private class SerializedMemberInfo { public string Name; public MemberInfo Member; public Type Type; public bool AllowNull; public bool IsVirtual; public bool IsField; public bool IsNullable; // signifies whether this is a Nullable public bool HasConverter; public bool IsGenericConverter; // used so we can call directly to the generic version if it is public Type Converter; public Type ConverterTarget; public FieldInfo ConverterField; // invalid for objects with IsNullabe false public Type NullableWrappedType => Nullable.GetUnderlyingType(Type); // invalid for objects with IsNullabe false public PropertyInfo Nullable_HasValue => Type.GetProperty(nameof(Nullable.HasValue)); // invalid for objects with IsNullabe false public PropertyInfo Nullable_Value => Type.GetProperty(nameof(Nullable.Value)); // invalid for objects with IsNullabe false public ConstructorInfo Nullable_Construct => Type.GetConstructor(new[] { NullableWrappedType }); } internal delegate IConfigStore GeneratedStoreCreator(IGeneratedStore parent); private static (GeneratedStoreCreator ctor, Type type) MakeCreator(Type type) { // note that this does not and should not use converters by default for everything if (!type.IsClass) throw new ArgumentException("Config type is not a class"); var baseCtor = type.GetConstructor(Type.EmptyTypes); // get a default constructor if (baseCtor == null) throw new ArgumentException("Config type does not have a public parameterless constructor"); var typeBuilder = Module.DefineType($"{type.FullName}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, type); var typeField = typeBuilder.DefineField("<>_type", typeof(Type), FieldAttributes.Private | FieldAttributes.InitOnly); var implField = typeBuilder.DefineField("<>_impl", typeof(Impl), FieldAttributes.Private | FieldAttributes.InitOnly); var parentField = typeBuilder.DefineField("<>_parent", typeof(IGeneratedStore), FieldAttributes.Private | FieldAttributes.InitOnly); // none of this can be Expressions because CompileToMethod requires a static target method for some dumbass reason #region Parse base object structure var baseChanged = type.GetMethod("Changed", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty()); if (baseChanged != null && !baseChanged.IsVirtual) baseChanged = null; // limit this to just the one thing var baseOnReload = type.GetMethod("OnReload", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty()); if (baseOnReload != null && !baseOnReload.IsVirtual) baseOnReload = null; // limit this to just the one thing var structure = new List(); // TODO: support converters bool ProcessAttributesFor(ref SerializedMemberInfo member) { var attrs = member.Member.GetCustomAttributes(true); var ignores = attrs.Select(o => o as IgnoreAttribute).NonNull(); if (ignores.Any()) // we ignore { return false; } var nonNullables = attrs.Select(o => o as NonNullableAttribute).NonNull(); member.Name = member.Member.Name; member.IsNullable = member.Type.IsGenericType && member.Type.GetGenericTypeDefinition() == typeof(Nullable<>); member.AllowNull = !nonNullables.Any() && (!member.Type.IsValueType || member.IsNullable); var nameAttr = attrs.Select(o => o as SerializedNameAttribute).NonNull().FirstOrDefault(); if (nameAttr != null) member.Name = nameAttr.Name; member.HasConverter = false; var converterAttr = attrs.Select(o => o as UseConverterAttribute).NonNull().FirstOrDefault(); if (converterAttr != null) { member.Converter = converterAttr.ConverterType; member.IsGenericConverter = converterAttr.IsGenericConverter; if (member.Converter.GetConstructor(Type.EmptyTypes) == null) { Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not default-constructible"); goto endConverterAttr; // is there a better control flow structure to do this? } if (member.Converter.ContainsGenericParameters) { Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that has unfilled type parameters"); goto endConverterAttr; } if (member.Converter.IsInterface || member.Converter.IsAbstract) { Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not constructible"); goto endConverterAttr; } var targetType = converterAttr.ConverterTargetType; if (!member.IsGenericConverter) { try { var conv = Activator.CreateInstance(converterAttr.ConverterType) as IValueConverter; targetType = conv.Type; } catch { Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter who's target type could not be determined"); goto endConverterAttr; } } if (targetType != member.Type) { Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not of the member's type"); goto endConverterAttr; } member.ConverterTarget = targetType; member.HasConverter = true; } endConverterAttr: return true; } // only looks at public/protected properties foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (prop.GetSetMethod(true)?.IsPrivate ?? true) { // we enter this block if the setter is inacessible or doesn't exist continue; // ignore props without setter } if (prop.GetGetMethod(true)?.IsPrivate ?? true) { // we enter this block if the getter is inacessible or doesn't exist continue; // ignore props without getter } var smi = new SerializedMemberInfo { Member = prop, IsVirtual = (prop.GetGetMethod(true)?.IsVirtual ?? false) || (prop.GetSetMethod(true)?.IsVirtual ?? false), IsField = false, Type = prop.PropertyType }; if (!ProcessAttributesFor(ref smi)) continue; structure.Add(smi); } // only look at public/protected fields foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (field.IsPrivate) continue; var smi = new SerializedMemberInfo { Member = field, IsVirtual = false, IsField = true, Type = field.FieldType }; if (!ProcessAttributesFor(ref smi)) continue; structure.Add(smi); } #endregion #region Converter fields var uniqueConverterTypes = structure.Where(m => m.HasConverter).Select(m => m.Converter).Distinct().ToArray(); var converterFields = new Dictionary(uniqueConverterTypes.Length); foreach (var convType in uniqueConverterTypes) { var field = typeBuilder.DefineField($"_{convType}", convType, FieldAttributes.Private | FieldAttributes.InitOnly | FieldAttributes.Static); converterFields.Add(convType, field); foreach (var member in structure.Where(m => m.HasConverter && m.Converter == convType)) member.ConverterField = field; } #endregion #region Static constructor var cctor = typeBuilder.DefineConstructor(MethodAttributes.Static, CallingConventions.Standard, Type.EmptyTypes); { var il = cctor.GetILGenerator(); foreach (var kvp in converterFields) { var typeCtor = kvp.Key.GetConstructor(Type.EmptyTypes); il.Emit(OpCodes.Newobj, typeCtor); il.Emit(OpCodes.Stsfld, kvp.Value); } il.Emit(OpCodes.Ret); } #endregion #region Constructor var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGeneratedStore) }); { var il = ctor.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // keep this at bottom of stack il.Emit(OpCodes.Dup); il.Emit(OpCodes.Call, baseCtor); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldarg_1); // load parent il.Emit(OpCodes.Stfld, parentField); il.Emit(OpCodes.Dup); EmitTypeof(il, type); il.Emit(OpCodes.Stfld, typeField); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Newobj, Impl.Ctor); il.Emit(OpCodes.Stfld, implField); var GetLocal = MakeGetLocal(il); foreach (var member in structure) { EmitMemberFix(il, member, GetLocal); } il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ret); } #endregion const MethodAttributes propertyMethodAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; const MethodAttributes virtualPropertyMethodAttr = propertyMethodAttr | MethodAttributes.Virtual | MethodAttributes.Final; const MethodAttributes virtualMemberMethod = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.Final; #region IGeneratedStore typeBuilder.AddInterfaceImplementation(typeof(IGeneratedStore)); var IGeneratedStore_t = typeof(IGeneratedStore); var IGeneratedStore_GetImpl = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Impl)).GetGetMethod(); var IGeneratedStore_GetType = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Type)).GetGetMethod(); var IGeneratedStore_GetParent = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Parent)).GetGetMethod(); var IGeneratedStore_Serialize = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Serialize)); var IGeneratedStore_Deserialize = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Deserialize)); var IGeneratedStore_OnReload = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.OnReload)); var IGeneratedStore_Changed = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Changed)); #region IGeneratedStore.OnReload var onReload = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.OnReload)}", virtualMemberMethod, null, Type.EmptyTypes); typeBuilder.DefineMethodOverride(onReload, IGeneratedStore_OnReload); if (baseOnReload != null) typeBuilder.DefineMethodOverride(onReload, baseOnReload); { var il = onReload.GetILGenerator(); if (baseOnReload != null) { il.Emit(OpCodes.Ldarg_0); // load this il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, baseOnReload); // load impl field } il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore.Impl var implProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Impl), PropertyAttributes.None, typeof(Impl), null); var implPropGet = typeBuilder.DefineMethod($"{nameof(IGeneratedStore.Impl)}", virtualPropertyMethodAttr, implProp.PropertyType, Type.EmptyTypes); implProp.SetGetMethod(implPropGet); typeBuilder.DefineMethodOverride(implPropGet, IGeneratedStore_GetImpl); { var il = implPropGet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // load this il.Emit(OpCodes.Ldfld, implField); // load impl field il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore.Type var typeProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Type), PropertyAttributes.None, typeof(Type), null); var typePropGet = typeBuilder.DefineMethod($"{nameof(IGeneratedStore.Type)}", virtualPropertyMethodAttr, typeProp.PropertyType, Type.EmptyTypes); typeProp.SetGetMethod(typePropGet); typeBuilder.DefineMethodOverride(typePropGet, IGeneratedStore_GetType); { var il = typePropGet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // load this il.Emit(OpCodes.Ldfld, typeField); // load impl field il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore.Parent var parentProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Parent), PropertyAttributes.None, typeof(IGeneratedStore), null); var parentPropGet = typeBuilder.DefineMethod($"{nameof(IGeneratedStore.Parent)}", virtualPropertyMethodAttr, parentProp.PropertyType, Type.EmptyTypes); parentProp.SetGetMethod(parentPropGet); typeBuilder.DefineMethodOverride(parentPropGet, IGeneratedStore_GetParent); { var il = parentPropGet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // load this il.Emit(OpCodes.Ldfld, parentField); // load impl field il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore.Serialize var serializeGen = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.Serialize)}", virtualPropertyMethodAttr, IGeneratedStore_Serialize.ReturnType, Type.EmptyTypes); typeBuilder.DefineMethodOverride(serializeGen, IGeneratedStore_Serialize); { // this is non-locking because the only code that will call this will already own the correct lock var il = serializeGen.GetILGenerator(); var Map_Add = typeof(Map).GetMethod(nameof(Map.Add)); il.Emit(OpCodes.Call, typeof(Value).GetMethod(nameof(Value.Map))); // the map is now at the top of the stack var GetLocal = MakeGetLocal(il); foreach (var member in structure) { il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldstr, member.Name); // TODO: make this behave with annotations EmitSerializeMember(il, member, GetLocal); il.Emit(OpCodes.Call, Map_Add); } // the map is still at the top of the stack, return it il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore.Deserialize var deserializeGen = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.Deserialize)}", virtualPropertyMethodAttr, null, new[] { IGeneratedStore_Deserialize.GetParameters()[0].ParameterType }); typeBuilder.DefineMethodOverride(deserializeGen, IGeneratedStore_Deserialize); { // this is non-locking because the only code that will call this will already own the correct lock var il = deserializeGen.GetILGenerator(); var Map_t = typeof(Map); var Map_TryGetValue = Map_t.GetMethod(nameof(Map.TryGetValue)); var Object_GetType = typeof(object).GetMethod(nameof(Object.GetType)); var valueLocal = il.DeclareLocal(typeof(Value)); var nonNull = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Brtrue, nonNull); EmitLogError(il, "Attempting to deserialize null", tailcall: true); il.Emit(OpCodes.Ret); il.MarkLabel(nonNull); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Isinst, Map_t); il.Emit(OpCodes.Dup); // duplicate cloned value var notMapError = il.DefineLabel(); il.Emit(OpCodes.Brtrue, notMapError); // handle error il.Emit(OpCodes.Pop); // removes the duplicate value EmitLogError(il, $"Invalid root for deserializing {type.FullName}", tailcall: true, expected: il => EmitTypeof(il, Map_t), found: il => { il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Callvirt, Object_GetType); }); il.Emit(OpCodes.Ret); var nextLabel = notMapError; var GetLocal = MakeGetLocal(il); // head of stack is Map instance foreach (var member in structure) { il.MarkLabel(nextLabel); nextLabel = il.DefineLabel(); var endErrorLabel = il.DefineLabel(); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldstr, member.Name); il.Emit(OpCodes.Ldloca_S, valueLocal); il.Emit(OpCodes.Call, Map_TryGetValue); il.Emit(OpCodes.Brtrue_S, endErrorLabel); EmitLogError(il, $"Missing key {member.Name}", tailcall: false); il.Emit(OpCodes.Br, nextLabel); il.MarkLabel(endErrorLabel); il.Emit(OpCodes.Ldloc_S, valueLocal); EmitDeserializeMember(il, member, nextLabel, il => il.Emit(OpCodes.Ldloc_S, valueLocal), GetLocal); } il.MarkLabel(nextLabel); il.Emit(OpCodes.Pop); // removes the duplicate value il.Emit(OpCodes.Ret); } #endregion #endregion #region IConfigStore typeBuilder.AddInterfaceImplementation(typeof(IConfigStore)); var IConfigStore_t = typeof(IConfigStore); var IConfigStore_GetSyncObject = IConfigStore_t.GetProperty(nameof(IConfigStore.SyncObject)).GetGetMethod(); var IConfigStore_GetWriteSyncObject = IConfigStore_t.GetProperty(nameof(IConfigStore.WriteSyncObject)).GetGetMethod(); var IConfigStore_WriteTo = IConfigStore_t.GetMethod(nameof(IConfigStore.WriteTo)); var IConfigStore_ReadFrom = IConfigStore_t.GetMethod(nameof(IConfigStore.ReadFrom)); #region IConfigStore.SyncObject var syncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.SyncObject), PropertyAttributes.None, typeof(WaitHandle), null); var syncObjPropGet = typeBuilder.DefineMethod($"{nameof(IConfigStore.SyncObject)}", virtualPropertyMethodAttr, syncObjProp.PropertyType, Type.EmptyTypes); syncObjProp.SetGetMethod(syncObjPropGet); typeBuilder.DefineMethodOverride(syncObjPropGet, IConfigStore_GetSyncObject); { var il = syncObjPropGet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.FindImplMethod); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.SyncObjectGetMethod); il.Emit(OpCodes.Ret); } #endregion #region IConfigStore.WriteSyncObject var writeSyncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.WriteSyncObject), PropertyAttributes.None, typeof(WaitHandle), null); var writeSyncObjPropGet = typeBuilder.DefineMethod($"{nameof(IConfigStore.WriteSyncObject)}", virtualPropertyMethodAttr, writeSyncObjProp.PropertyType, Type.EmptyTypes); writeSyncObjProp.SetGetMethod(writeSyncObjPropGet); typeBuilder.DefineMethodOverride(writeSyncObjPropGet, IConfigStore_GetWriteSyncObject); { var il = writeSyncObjPropGet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.FindImplMethod); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.WriteSyncObjectGetMethod); il.Emit(OpCodes.Ret); } #endregion #region IConfigStore.WriteTo var writeTo = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.WriteTo)}", virtualMemberMethod, null, new[] { typeof(IConfigProvider) }); typeBuilder.DefineMethodOverride(writeTo, IConfigStore_WriteTo); { var il = writeTo.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.FindImplMethod); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.WriteToMethod); il.Emit(OpCodes.Ret); } #endregion #region IConfigStore.ReadFrom var readFrom = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.ReadFrom)}", virtualMemberMethod, null, new[] { typeof(IConfigProvider) }); typeBuilder.DefineMethodOverride(readFrom, IConfigStore_ReadFrom); { var il = readFrom.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.FindImplMethod); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.ReadFromMethod); il.Emit(OpCodes.Ret); } #endregion #endregion #region Changed var coreChanged = typeBuilder.DefineMethod( "<>Changed", virtualMemberMethod, null, Type.EmptyTypes); { var il = coreChanged.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplSignalChangedMethod); il.Emit(OpCodes.Ret); // simply call our impl's SignalChanged method and return } if (baseChanged != null) { var changedMethod = typeBuilder.DefineMethod( // copy to override baseChanged baseChanged.Name, virtualMemberMethod, null, Type.EmptyTypes); typeBuilder.DefineMethodOverride(changedMethod, baseChanged); { var il = changedMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, baseChanged); // call base il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, coreChanged); // call back to the core change method il.Emit(OpCodes.Ret); } coreChanged = changedMethod; // switch to calling this version instead of just the default } typeBuilder.DefineMethodOverride(coreChanged, IGeneratedStore_Changed); #endregion #region Members foreach (var member in structure.Where(m => m.IsVirtual)) { // IsVirtual implies !IsField var prop = member.Member as PropertyInfo; var get = prop.GetGetMethod(true); var set = prop.GetSetMethod(true); var propBuilder = typeBuilder.DefineProperty($"{member.Name}#", PropertyAttributes.None, member.Type, null); var propGet = typeBuilder.DefineMethod($"{propBuilder.Name}", virtualPropertyMethodAttr, member.Type, Type.EmptyTypes); propBuilder.SetGetMethod(propGet); typeBuilder.DefineMethodOverride(propGet, get); { var il = propGet.GetILGenerator(); var local = il.DeclareLocal(member.Type); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplTakeReadMethod); // take the read lock il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, get); // call base getter il.Emit(OpCodes.Stloc, local); il.BeginFinallyBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplReleaseReadMethod); // release the read lock il.EndExceptionBlock(); il.Emit(OpCodes.Ldloc, local); il.Emit(OpCodes.Ret); } var propSet = typeBuilder.DefineMethod($"{propBuilder.Name}", virtualPropertyMethodAttr, null, new[] { member.Type }); propBuilder.SetSetMethod(propSet); typeBuilder.DefineMethodOverride(propSet, set); { // TODO: decide if i want to correct the value before or after i take the write lock var il = propSet.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplTakeWriteMethod); // take the write lock il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); EmitCorrectMember(il, member); il.Emit(OpCodes.Call, set); il.BeginFinallyBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplReleaseWriteMethod); // release the write lock il.EndExceptionBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, Impl.ImplInvokeChangedMethod); il.Emit(OpCodes.Ret); } } #endregion var genType = typeBuilder.CreateType(); var parentParam = Expression.Parameter(typeof(IGeneratedStore), "parent"); var creatorDel = Expression.Lambda( Expression.New(ctor, parentParam), parentParam ).Compile(); return (creatorDel, genType); } private delegate LocalBuilder GetLocal(Type type, int idx = 0); private static GetLocal MakeGetLocal(ILGenerator il) { // TODO: improve this shit a bit so that i can release a hold of a variable and do more auto managing var locals = new List(); LocalBuilder GetLocal(Type ty, int i = 0) { // TODO: pull this garbage out somewhere sane somehow var builder = locals.Where(b => b.LocalType == ty).Skip(i).FirstOrDefault(); if (builder == null) { builder = il.DeclareLocal(ty); locals.Add(builder); } return builder; } return GetLocal; } private static readonly MethodInfo LogErrorMethod = typeof(GeneratedStore).GetMethod(nameof(LogError), BindingFlags.NonPublic | BindingFlags.Static); internal static void LogError(Type expected, Type found, string message) { Logger.config.Notice($"{message}{(expected == null ? "" : $" (expected {expected}, found {found?.ToString() ?? "null"})")}"); } private static bool NeedsCorrection(SerializedMemberInfo member) { return false; } // expects start value on stack, exits with final value on stack private static void EmitCorrectMember(ILGenerator il, SerializedMemberInfo member) { if (!NeedsCorrection(member)) return; // TODO: impl } // expects the this param to be on the stack private static void EmitMemberFix(ILGenerator il, SerializedMemberInfo member, GetLocal GetLocal) { if (!NeedsCorrection(member)) return; var local = GetLocal(member.Type); EmitLoad(il, member); // load the member EmitCorrectMember(il, member); // correct it il.Emit(OpCodes.Stloc, local); EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, local)); } #region Utility private static void EmitLoad(ILGenerator il, SerializedMemberInfo member) { il.Emit(OpCodes.Ldarg_0); // load this if (member.IsField) il.Emit(OpCodes.Ldfld, member.Member as FieldInfo); else { // member is a property var prop = member.Member as PropertyInfo; var getter = prop.GetGetMethod(); if (getter == null) throw new InvalidOperationException($"Property {member.Name} does not have a getter and is not ignored"); il.Emit(OpCodes.Call, getter); } } private static void EmitStore(ILGenerator il, SerializedMemberInfo member, Action value) { il.Emit(OpCodes.Ldarg_0); // load this value(il); if (member.IsField) il.Emit(OpCodes.Stfld, member.Member as FieldInfo); else { // member is a property var prop = member.Member as PropertyInfo; var setter = prop.GetSetMethod(); if (setter == null) throw new InvalidOperationException($"Property {member.Name} does not have a setter and is not ignored"); il.Emit(OpCodes.Call, setter); } } private static void EmitLogError(ILGenerator il, string message, bool tailcall = false, Action expected = null, Action found = null) { if (expected == null) expected = il => il.Emit(OpCodes.Ldnull); if (found == null) found = il => il.Emit(OpCodes.Ldnull); expected(il); found(il); il.Emit(OpCodes.Ldstr, message); if (tailcall) il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, LogErrorMethod); } private static readonly MethodInfo Type_GetTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)); private static void EmitTypeof(ILGenerator il, Type type) { il.Emit(OpCodes.Ldtoken, type); il.Emit(OpCodes.Call, Type_GetTypeFromHandle); } private static Type Decimal_t = typeof(decimal); private static ConstructorInfo Decimal_FromFloat = Decimal_t.GetConstructor(new[] { typeof(float) }); private static ConstructorInfo Decimal_FromDouble = Decimal_t.GetConstructor(new[] { typeof(double) }); private static ConstructorInfo Decimal_FromInt = Decimal_t.GetConstructor(new[] { typeof(int) }); private static ConstructorInfo Decimal_FromUInt = Decimal_t.GetConstructor(new[] { typeof(uint) }); private static ConstructorInfo Decimal_FromLong = Decimal_t.GetConstructor(new[] { typeof(long) }); private static ConstructorInfo Decimal_FromULong = Decimal_t.GetConstructor(new[] { typeof(ulong) }); private static void EmitNumberConvertTo(ILGenerator il, Type to, Type from) { // WARNING: THIS USES THE NO-OVERFLOW OPCODES if (to == from) return; if (to == Decimal_t) { if (from == typeof(float)) il.Emit(OpCodes.Newobj, Decimal_FromFloat); else if (from == typeof(double)) il.Emit(OpCodes.Newobj, Decimal_FromDouble); else if (from == typeof(long)) il.Emit(OpCodes.Newobj, Decimal_FromLong); else if (from == typeof(ulong)) il.Emit(OpCodes.Newobj, Decimal_FromULong); else if (from == typeof(int)) il.Emit(OpCodes.Newobj, Decimal_FromInt); else if (from == typeof(uint)) il.Emit(OpCodes.Newobj, Decimal_FromUInt); else if (from == typeof(IntPtr)) { EmitNumberConvertTo(il, typeof(long), from); EmitNumberConvertTo(il, to, typeof(long)); } else if (from == typeof(UIntPtr)) { EmitNumberConvertTo(il, typeof(ulong), from); EmitNumberConvertTo(il, to, typeof(ulong)); } else { // if the source is anything else, we first convert to int because that can contain all other values EmitNumberConvertTo(il, typeof(int), from); EmitNumberConvertTo(il, to, typeof(int)); }; } else if (from == Decimal_t) { if (to == typeof(IntPtr)) { EmitNumberConvertTo(il, typeof(long), from); EmitNumberConvertTo(il, to, typeof(long)); } else if (to == typeof(UIntPtr)) { EmitNumberConvertTo(il, typeof(ulong), from); EmitNumberConvertTo(il, to, typeof(ulong)); } else { var method = Decimal_t.GetMethod($"To{to.Name}"); // conveniently, this is the pattern of the to* names il.Emit(OpCodes.Call, method); } } else if (to == typeof(IntPtr)) il.Emit(OpCodes.Conv_I); else if (to == typeof(UIntPtr)) il.Emit(OpCodes.Conv_U); else if (to == typeof(sbyte)) il.Emit(OpCodes.Conv_I1); else if (to == typeof(byte)) il.Emit(OpCodes.Conv_U1); else if (to == typeof(short)) il.Emit(OpCodes.Conv_I2); else if (to == typeof(ushort)) il.Emit(OpCodes.Conv_U2); else if (to == typeof(int)) il.Emit(OpCodes.Conv_I4); else if (to == typeof(uint)) il.Emit(OpCodes.Conv_U4); else if (to == typeof(long)) il.Emit(OpCodes.Conv_I8); else if (to == typeof(ulong)) il.Emit(OpCodes.Conv_U8); else if (to == typeof(float)) { if (from == typeof(byte) || from == typeof(ushort) || from == typeof(uint) || from == typeof(ulong) || from == typeof(UIntPtr)) il.Emit(OpCodes.Conv_R_Un); il.Emit(OpCodes.Conv_R4); } else if (to == typeof(double)) { if (from == typeof(byte) || from == typeof(ushort) || from == typeof(uint) || from == typeof(ulong) || from == typeof(UIntPtr)) il.Emit(OpCodes.Conv_R_Un); il.Emit(OpCodes.Conv_R8); } } private static void EmitCreateChildGenerated(ILGenerator il, Type childType) { var method = CreateGParent.MakeGenericMethod(childType); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, method); } #endregion #region Serialize // emit takes no args, leaves Value at top of stack private static void EmitSerializeMember(ILGenerator il, SerializedMemberInfo member, GetLocal GetLocal) { EmitLoad(il, member); var endSerialize = il.DefineLabel(); if (member.AllowNull) { var passedNull = il.DefineLabel(); il.Emit(OpCodes.Dup); if (member.IsNullable) il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod()); il.Emit(OpCodes.Brtrue, passedNull); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Br, endSerialize); il.MarkLabel(passedNull); } if (member.IsNullable) il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); var targetType = GetExpectedValueTypeForType(member.Type); if (targetType == typeof(Text)) { // only happens when arg is a string or char var TextCreate = typeof(Value).GetMethod(nameof(Value.Text)); if (member.Type == typeof(char)) { var strFromChar = typeof(char).GetMethod(nameof(char.ToString), new[] { typeof(char) }); il.Emit(OpCodes.Call, strFromChar); } il.Emit(OpCodes.Call, TextCreate); } else if (targetType == typeof(Boolean)) { var BoolCreate = typeof(Value).GetMethod(nameof(Value.Bool)); il.Emit(OpCodes.Call, BoolCreate); } else if (targetType == typeof(Integer)) { var IntCreate = typeof(Value).GetMethod(nameof(Value.Integer)); EmitNumberConvertTo(il, IntCreate.GetParameters()[0].ParameterType, member.Type); il.Emit(OpCodes.Call, IntCreate); } else if (targetType == typeof(FloatingPoint)) { var FloatCreate = typeof(Value).GetMethod(nameof(Value.Float)); EmitNumberConvertTo(il, FloatCreate.GetParameters()[0].ParameterType, member.Type); il.Emit(OpCodes.Call, FloatCreate); } else if (targetType == typeof(List)) { // TODO: impl this (enumerables) il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); } else if (targetType == typeof(Map)) { // TODO: support other aggregate types // for now, we assume that its a generated type implementing IGeneratedStore var IGeneratedStore_Serialize = typeof(IGeneratedStore).GetMethod(nameof(IGeneratedStore.Serialize)); il.Emit(OpCodes.Callvirt, IGeneratedStore_Serialize); } il.MarkLabel(endSerialize); // TODO: implement converters } #endregion #region Deserialize private static Type GetExpectedValueTypeForType(Type valT) { if (typeof(Value).IsAssignableFrom(valT)) // this is a Value subtype return valT; if (valT == typeof(string) || valT == typeof(char)) return typeof(Text); if (valT == typeof(bool)) return typeof(Boolean); if (valT == typeof(byte) || valT == typeof(sbyte) || valT == typeof(short) || valT == typeof(ushort) || valT == typeof(int) || valT == typeof(uint) || valT == typeof(long) || valT == typeof(IntPtr)) return typeof(Integer); if (valT == typeof(float) || valT == typeof(double) || valT == typeof(decimal) || valT == typeof(ulong) // ulong gets put into this, because decimal can hold it || valT == typeof(UIntPtr)) return typeof(FloatingPoint); if (typeof(IEnumerable).IsAssignableFrom(valT)) return typeof(List); // TODO: fill this out the rest of the way // TODO: support converters return typeof(Map); // default for various objects } private static void EmitDeserializeGeneratedValue(ILGenerator il, Type targetType, Type srcType, GetLocal GetLocal) { var IGeneratedStore_Deserialize = typeof(IGeneratedStore).GetMethod(nameof(IGeneratedStore.Deserialize)); var valuel = GetLocal(srcType, 0); il.Emit(OpCodes.Stloc, valuel); EmitCreateChildGenerated(il, targetType); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldloc, valuel); il.Emit(OpCodes.Callvirt, IGeneratedStore_Deserialize); } private static void EmitDeserializeNullable(ILGenerator il, SerializedMemberInfo member, Type expected, GetLocal GetLocal) { EmitDeserializeValue(il, member.NullableWrappedType, expected, GetLocal); il.Emit(OpCodes.Newobj, member.Nullable_Construct); } // top of stack is the Value to deserialize; the type will be as returned from GetExpectedValueTypeForType // after, top of stack will be thing to write to field private static void EmitDeserializeValue(ILGenerator il, Type targetType, Type expected, GetLocal GetLocal) { if (typeof(Value).IsAssignableFrom(targetType)) return; // do nothing if (expected == typeof(Text)) { var getter = expected.GetProperty(nameof(Text.Value)).GetGetMethod(); il.Emit(OpCodes.Call, getter); if (targetType == typeof(char)) { var strIndex = typeof(string).GetProperty("Chars").GetGetMethod(); // string's indexer is specially named Chars il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Call, strIndex); } } else if (expected == typeof(Boolean)) { var getter = expected.GetProperty(nameof(Boolean.Value)).GetGetMethod(); il.Emit(OpCodes.Call, getter); } else if (expected == typeof(Integer)) { var getter = expected.GetProperty(nameof(Integer.Value)).GetGetMethod(); il.Emit(OpCodes.Call, getter); EmitNumberConvertTo(il, targetType, getter.ReturnType); } else if (expected == typeof(FloatingPoint)) { var getter = expected.GetProperty(nameof(FloatingPoint.Value)).GetGetMethod(); il.Emit(OpCodes.Call, getter); EmitNumberConvertTo(il, targetType, getter.ReturnType); } // TODO: implement stuff for lists and maps of various types (probably call out somewhere else to figure out what to do) else if (expected == typeof(Map)) { EmitDeserializeGeneratedValue(il, targetType, expected, GetLocal); } else // TODO: support converters { il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); } } // emit takes the value being deserialized, logs on error, leaves nothing on stack private static void EmitDeserializeMember(ILGenerator il, SerializedMemberInfo member, Label nextLabel, Action getValue, GetLocal GetLocal) { var Object_GetType = typeof(object).GetMethod(nameof(Object.GetType)); var implLabel = il.DefineLabel(); var passedTypeCheck = il.DefineLabel(); var expectType = GetExpectedValueTypeForType(member.IsNullable ? member.NullableWrappedType : member.Type); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Brtrue_S, implLabel); // null check if (!member.AllowNull) { il.Emit(OpCodes.Pop); EmitLogError(il, $"Member {member.Name} ({member.Type}) not nullable", tailcall: false, expected: il => EmitTypeof(il, expectType)); il.Emit(OpCodes.Br, nextLabel); } else if (member.IsNullable) { il.Emit(OpCodes.Pop); var valTLocal = GetLocal(member.Type, 0); il.Emit(OpCodes.Ldloca, valTLocal); il.Emit(OpCodes.Initobj, member.Type); EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, valTLocal)); il.Emit(OpCodes.Br, nextLabel); } else { il.Emit(OpCodes.Pop); EmitStore(il, member, il => il.Emit(OpCodes.Ldnull)); il.Emit(OpCodes.Br, nextLabel); } il.MarkLabel(implLabel); il.Emit(OpCodes.Isinst, expectType); //replaces on stack il.Emit(OpCodes.Dup); // duplicate cloned value il.Emit(OpCodes.Brtrue, passedTypeCheck); // null check var errorHandle = il.DefineLabel(); // special cases to handle coersion between Float and Int if (expectType == typeof(FloatingPoint)) { var specialTypeCheck = il.DefineLabel(); il.Emit(OpCodes.Pop); getValue(il); il.Emit(OpCodes.Isinst, typeof(Integer)); //replaces on stack il.Emit(OpCodes.Dup); // duplicate cloned value il.Emit(OpCodes.Brfalse, errorHandle); // null check var Integer_CoerceToFloat = typeof(Integer).GetMethod(nameof(Integer.AsFloat)); il.Emit(OpCodes.Call, Integer_CoerceToFloat); il.Emit(OpCodes.Br, passedTypeCheck); } else if (expectType == typeof(Integer)) { var specialTypeCheck = il.DefineLabel(); il.Emit(OpCodes.Pop); getValue(il); il.Emit(OpCodes.Isinst, typeof(FloatingPoint)); //replaces on stack il.Emit(OpCodes.Dup); // duplicate cloned value il.Emit(OpCodes.Brfalse, errorHandle); // null check var Float_CoerceToInt = typeof(FloatingPoint).GetMethod(nameof(FloatingPoint.AsInteger)); il.Emit(OpCodes.Call, Float_CoerceToInt); il.Emit(OpCodes.Br, passedTypeCheck); } il.MarkLabel(errorHandle); il.Emit(OpCodes.Pop); EmitLogError(il, $"Unexpected type deserializing {member.Name}", tailcall: false, expected: il => EmitTypeof(il, expectType), found: il => { getValue(il); il.Emit(OpCodes.Callvirt, Object_GetType); }); il.Emit(OpCodes.Br, nextLabel); il.MarkLabel(passedTypeCheck); var local = GetLocal(member.Type, 0); if (member.IsNullable) EmitDeserializeNullable(il, member, expectType, GetLocal); else EmitDeserializeValue(il, member.Type, expectType, GetLocal); il.Emit(OpCodes.Stloc, local); EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, local)); } #endregion } }