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; using System.ComponentModel; #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. /// /// /// Similarly, can declare a public or protected, /// method CopyFrom(ConfigType) (the first parameter is the type it is defined on), which may be called to copy the properties from /// another object of its type easily, and more importantly, as only one change. Its body will be executed after the values have been copied. /// /// /// Similarly, can declare a public or protected, /// method ChangeTransaction() returning , which may be called to get an object representing a transactional /// change. This may be used to change a lot of properties at once without triggering a save multiple times. Ideally, this is used in a /// block or declaration. The returned from your implementation will have its /// called after Changed() is called, but before the write lock is released. /// Unless you have a very good reason to use the nested , avoid it. /// /// /// If is marked with , the resulting object will implement /// . Similarly, if implements , /// the resulting object will implement it and notify it too. /// /// /// 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(); IDisposable ChangeTransaction(); Value Serialize(); void Deserialize(Value val); } internal interface IGeneratedStore : IGeneratedStore where T : class { void CopyFrom(T source, bool useLock); } internal interface IGeneratedPropertyChanged : INotifyPropertyChanged { PropertyChangedEventHandler PropertyChangedEvent { get; } } internal class Impl : IConfigStore { private readonly IGeneratedStore generated; private bool inChangeTransaction = false; private bool changedInTransaction = false; 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; public static WaitHandle ImplGetSyncObject(IGeneratedStore s) => FindImpl(s).SyncObject; internal static MethodInfo ImplGetSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetSyncObject)); public ReaderWriterLockSlim WriteSyncObject { get; } = new ReaderWriterLockSlim(); public static ReaderWriterLockSlim ImplGetWriteSyncObject(IGeneratedStore s) => FindImpl(s)?.WriteSyncObject; internal static MethodInfo ImplGetWriteSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetWriteSyncObject)); 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() { if (!WriteSyncObject.IsWriteLockHeld) WriteSyncObject.EnterReadLock(); } internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead)); public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead(); public void ReleaseRead() { if (!WriteSyncObject.IsWriteLockHeld) 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 ImplChangeTransactionMethod = typeof(Impl).GetMethod(nameof(ImplChangeTransaction)); public static IDisposable ImplChangeTransaction(IGeneratedStore s, IDisposable nest) => FindImpl(s).ChangeTransaction(nest); // TODO: improve trasactionals so they don't always save in every case public IDisposable ChangeTransaction(IDisposable nest, bool takeWrite = true) => GetFreeTransaction().InitWith(this, !inChangeTransaction, nest, takeWrite && !WriteSyncObject.IsWriteLockHeld); private ChangeTransactionObj GetFreeTransaction() => freeTransactionObjs.Count > 0 ? freeTransactionObjs.Pop() : new ChangeTransactionObj(); // TODO: maybe sometimes clean this? private static readonly Stack freeTransactionObjs = new Stack(); private sealed class ChangeTransactionObj : IDisposable { private struct Data { public readonly Impl impl; public readonly bool owns; public readonly bool ownsWrite; public readonly IDisposable nested; public Data(Impl impl, bool owning, bool takeWrite, IDisposable nest) { this.impl = impl; owns = owning; ownsWrite = takeWrite; nested = nest; } } private Data data; public ChangeTransactionObj InitWith(Impl impl, bool owning, IDisposable nest, bool takeWrite) { data = new Data(impl, owning, takeWrite, nest); if (data.owns) impl.inChangeTransaction = true; if (data.ownsWrite) impl.TakeWrite(); return this; } public void Dispose() => Dispose(true); private void Dispose(bool addToStore) { if (data.owns) { data.impl.inChangeTransaction = false; data.impl.InvokeChanged(); } data.nested?.Dispose(); if (data.ownsWrite) data.impl.ReleaseWrite(); if (addToStore) freeTransactionObjs.Push(this); } ~ChangeTransactionObj() => Dispose(false); } 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 ImplReadFromMethod = typeof(Impl).GetMethod(nameof(ImplReadFrom)); public static void ImplReadFrom(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).ReadFrom(provider); public void ReadFrom(ConfigProvider provider) { var values = provider.Load(); Logger.config.Debug("Generated impl ReadFrom"); Logger.config.Debug($"Read {values}"); generated.Deserialize(values); using var transaction = generated.ChangeTransaction(); generated.OnReload(); } internal static MethodInfo ImplWriteToMethod = typeof(Impl).GetMethod(nameof(ImplWriteTo)); public static void ImplWriteTo(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).WriteTo(provider); public void WriteTo(ConfigProvider 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 ConverterBase; 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 bool IsMethodInvalid(MethodInfo m, Type ret) => !m.IsVirtual || m.ReturnType != ret; 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"); #region Parse base object structure const BindingFlags overrideMemberFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var baseChanged = type.GetMethod("Changed", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); if (baseChanged != null && IsMethodInvalid(baseChanged, typeof(void))) baseChanged = null; var baseOnReload = type.GetMethod("OnReload", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); if (baseOnReload != null && IsMethodInvalid(baseOnReload, typeof(void))) baseOnReload = null; var baseCopyFrom = type.GetMethod("CopyFrom", overrideMemberFlags, null, new[] { type }, Array.Empty()); if (baseCopyFrom != null && IsMethodInvalid(baseCopyFrom, typeof(void))) baseCopyFrom = null; var baseChangeTransaction = type.GetMethod("ChangeTransaction", overrideMemberFlags, null, Type.EmptyTypes, Array.Empty()); if (baseChangeTransaction != null && IsMethodInvalid(baseChangeTransaction, typeof(IDisposable))) baseChangeTransaction = null; var isINotifyPropertyChanged = type.FindInterfaces((i, t) => i == (Type)t, typeof(INotifyPropertyChanged)).Length != 0; var hasNotifyAttribute = type.GetCustomAttribute() != null; var structure = new List(); 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; if (member.IsGenericConverter) member.ConverterBase = typeof(ValueConverter<>).MakeGenericType(targetType); else member.ConverterBase = typeof(IValueConverter); 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 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); #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); var noImplLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Brtrue, noImplLabel); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Newobj, Impl.Ctor); il.Emit(OpCodes.Stfld, implField); il.MarkLabel(noImplLabel); var GetLocal = MakeGetLocal(il); foreach (var member in structure) { EmitStore(il, member, il => { EmitLoad(il, member); // load the member EmitCorrectMember(il, member, false, true, GetLocal); // correct it }); } 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 INotifyPropertyChanged MethodBuilder notifyChanged = null; if (isINotifyPropertyChanged || hasNotifyAttribute) { var INotifyPropertyChanged_t = typeof(INotifyPropertyChanged); typeBuilder.AddInterfaceImplementation(INotifyPropertyChanged_t); var INotifyPropertyChanged_PropertyChanged = INotifyPropertyChanged_t.GetEvent(nameof(INotifyPropertyChanged.PropertyChanged)); var PropertyChangedEventHandler_t = typeof(PropertyChangedEventHandler); var PropertyChangedEventHander_Invoke = PropertyChangedEventHandler_t.GetMethod(nameof(PropertyChangedEventHandler.Invoke)); var PropertyChangedEventArgs_t = typeof(PropertyChangedEventArgs); var PropertyChangedEventArgs_ctor = PropertyChangedEventArgs_t.GetConstructor(new[] { typeof(string) }); var Delegate_t = typeof(Delegate); var Delegate_Combine = Delegate_t.GetMethod(nameof(Delegate.Combine), BindingFlags.Static | BindingFlags.Public, null, new[] { Delegate_t, Delegate_t }, Array.Empty()); var Delegate_Remove = Delegate_t.GetMethod(nameof(Delegate.Remove), BindingFlags.Static | BindingFlags.Public, null, new[] { Delegate_t, Delegate_t }, Array.Empty()); var CompareExchange = typeof(Interlocked).GetMethods() .Where(m => m.Name == nameof(Interlocked.CompareExchange)) .Where(m => m.ContainsGenericParameters) .Where(m => m.GetParameters().Length == 3).First() .MakeGenericMethod(PropertyChangedEventHandler_t); var PropertyChanged_backing = typeBuilder.DefineField("PropertyChanged", PropertyChangedEventHandler_t, FieldAttributes.Private); var add_PropertyChanged = typeBuilder.DefineMethod("PropertyChanged", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.Virtual, null, new[] { PropertyChangedEventHandler_t }); typeBuilder.DefineMethodOverride(add_PropertyChanged, INotifyPropertyChanged_PropertyChanged.GetAddMethod()); { var il = add_PropertyChanged.GetILGenerator(); var loopLabel = il.DefineLabel(); var delTemp = il.DeclareLocal(PropertyChangedEventHandler_t); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, PropertyChanged_backing); il.MarkLabel(loopLabel); il.Emit(OpCodes.Stloc, delTemp); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldflda, PropertyChanged_backing); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, Delegate_Combine); il.Emit(OpCodes.Castclass, PropertyChangedEventHandler_t); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Call, CompareExchange); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Bne_Un_S, loopLabel); il.Emit(OpCodes.Ret); } var remove_PropertyChanged = typeBuilder.DefineMethod("PropertyChanged", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.Virtual, null, new[] { PropertyChangedEventHandler_t }); typeBuilder.DefineMethodOverride(remove_PropertyChanged, INotifyPropertyChanged_PropertyChanged.GetRemoveMethod()); { var il = remove_PropertyChanged.GetILGenerator(); var loopLabel = il.DefineLabel(); var delTemp = il.DeclareLocal(PropertyChangedEventHandler_t); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, PropertyChanged_backing); il.MarkLabel(loopLabel); il.Emit(OpCodes.Stloc, delTemp); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldflda, PropertyChanged_backing); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, Delegate_Remove); il.Emit(OpCodes.Castclass, PropertyChangedEventHandler_t); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Call, CompareExchange); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldloc, delTemp); il.Emit(OpCodes.Bne_Un_S, loopLabel); il.Emit(OpCodes.Ret); } var PropertyChanged_event = typeBuilder.DefineEvent(nameof(INotifyPropertyChanged.PropertyChanged), EventAttributes.None, PropertyChangedEventHandler_t); PropertyChanged_event.SetAddOnMethod(add_PropertyChanged); PropertyChanged_event.SetRemoveOnMethod(remove_PropertyChanged); notifyChanged = typeBuilder.DefineMethod("<>NotifyChanged", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Final, null, new[] { typeof(string) }); { var il = notifyChanged.GetILGenerator(); var invokeNonNull = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, PropertyChanged_backing); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Brtrue, invokeNonNull); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ret); il.MarkLabel(invokeNonNull); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Newobj, PropertyChangedEventArgs_ctor); il.Emit(OpCodes.Call, PropertyChangedEventHander_Invoke); il.Emit(OpCodes.Ret); } } #endregion #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)); var IGeneratedStore_ChangeTransaction = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.ChangeTransaction)); #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)); var mapLocal = il.DeclareLocal(typeof(Map)); var GetLocal = MakeGetLocal(il); var valLocal = GetLocal(typeof(Value)); il.Emit(OpCodes.Call, typeof(Value).GetMethod(nameof(Value.Map))); il.Emit(OpCodes.Stloc, mapLocal); foreach (var member in structure) { EmitSerializeMember(il, member, GetLocal); il.Emit(OpCodes.Stloc, valLocal); il.Emit(OpCodes.Ldloc, mapLocal); il.Emit(OpCodes.Ldstr, member.Name); il.Emit(OpCodes.Ldloc, valLocal); il.Emit(OpCodes.Call, Map_Add); } il.Emit(OpCodes.Ldloc, mapLocal); 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 mapLocal = il.DeclareLocal(typeof(Map)); 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 il.Emit(OpCodes.Stloc, mapLocal); var notMapError = il.DefineLabel(); il.Emit(OpCodes.Brtrue, notMapError); // handle error 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.Ldloc, mapLocal); 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); if (notifyChanged != null) { foreach (var member in structure) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, member.Name); il.Emit(OpCodes.Call, notifyChanged); } } 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.Tailcall); il.Emit(OpCodes.Call, Impl.ImplGetSyncObjectMethod); 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.Tailcall); il.Emit(OpCodes.Call, Impl.ImplGetWriteSyncObjectMethod); il.Emit(OpCodes.Ret); } #endregion #region IConfigStore.WriteTo var writeTo = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.WriteTo)}", virtualMemberMethod, null, new[] { typeof(ConfigProvider) }); typeBuilder.DefineMethodOverride(writeTo, IConfigStore_WriteTo); { var il = writeTo.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.ImplWriteToMethod); il.Emit(OpCodes.Ret); } #endregion #region IConfigStore.ReadFrom var readFrom = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.ReadFrom)}", virtualMemberMethod, null, new[] { typeof(ConfigProvider) }); typeBuilder.DefineMethodOverride(readFrom, IConfigStore_ReadFrom); { var il = readFrom.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.ImplReadFromMethod); il.Emit(OpCodes.Ret); } #endregion #endregion #region Changed var coreChanged = typeBuilder.DefineMethod( "<>Changed", virtualMemberMethod, null, Type.EmptyTypes); typeBuilder.DefineMethodOverride(coreChanged, IGeneratedStore_Changed); if (baseChanged != null) typeBuilder.DefineMethodOverride(coreChanged, baseChanged); { var il = coreChanged.GetILGenerator(); if (baseChanged != null) { 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, Impl.ImplSignalChangedMethod); il.Emit(OpCodes.Ret); // simply call our impl's SignalChanged method and return } #endregion #region ChangeTransaction var coreChangeTransaction = typeBuilder.DefineMethod( "<>ChangeTransaction", virtualMemberMethod, typeof(IDisposable), Type.EmptyTypes); typeBuilder.DefineMethodOverride(coreChangeTransaction, IGeneratedStore_ChangeTransaction); if (baseChangeTransaction != null) typeBuilder.DefineMethodOverride(coreChangeTransaction, baseChangeTransaction); { var il = coreChangeTransaction.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); if (baseChangeTransaction != null) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, baseChangeTransaction); } else il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Call, Impl.ImplChangeTransactionMethod); il.Emit(OpCodes.Ret); } #endregion #region IGeneratedStore var IGeneratedStore_T_t = typeof(IGeneratedStore<>).MakeGenericType(type); typeBuilder.AddInterfaceImplementation(IGeneratedStore_T_t); var IGeneratedStore_T_CopyFrom = IGeneratedStore_T_t.GetMethod(nameof(IGeneratedStore.CopyFrom)); #region IGeneratedStore.CopyFrom var copyFrom = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.CopyFrom)}", virtualMemberMethod, null, new[] { type, typeof(bool) }); typeBuilder.DefineMethodOverride(copyFrom, IGeneratedStore_T_CopyFrom); { var il = copyFrom.GetILGenerator(); var transactionLocal = il.DeclareLocal(IDisposable_t); var startLock = il.DefineLabel(); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Brfalse, startLock); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, coreChangeTransaction); // take the write lock il.Emit(OpCodes.Stloc, transactionLocal); il.MarkLabel(startLock); var GetLocal = MakeGetLocal(il); foreach (var member in structure) { il.BeginExceptionBlock(); EmitStore(il, member, il => { EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_1)); EmitCorrectMember(il, member, false, false, GetLocal); }); il.BeginCatchBlock(typeof(Exception)); EmitWarnException(il, $"Error while copying from member {member.Name}"); il.EndExceptionBlock(); } if (notifyChanged != null) { foreach (var member in structure) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, member.Name); il.Emit(OpCodes.Call, notifyChanged); } } var endLock = il.DefineLabel(); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Brfalse, endLock); il.Emit(OpCodes.Ldloc, transactionLocal); il.Emit(OpCodes.Callvirt, IDisposable_Dispose); il.MarkLabel(endLock); il.Emit(OpCodes.Ret); } #endregion #endregion #region base.CopyFrom if (baseCopyFrom != null) { var pubCopyFrom = typeBuilder.DefineMethod( baseCopyFrom.Name, virtualMemberMethod, null, new[] { type }); typeBuilder.DefineMethodOverride(pubCopyFrom, baseCopyFrom); { var il = pubCopyFrom.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, coreChangeTransaction); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Call, copyFrom); // call internal il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, baseCopyFrom); // call base il.Emit(OpCodes.Tailcall); il.Emit(OpCodes.Callvirt, IDisposable_Dispose); // dispose transaction (which calls changed) il.Emit(OpCodes.Ret); } } #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(); var transactionLocal = il.DeclareLocal(IDisposable_t); var GetLocal = MakeGetLocal(il); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, coreChangeTransaction); // take the write lock il.Emit(OpCodes.Stloc, transactionLocal); il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); EmitCorrectMember(il, member, false, false, GetLocal); il.Emit(OpCodes.Call, set); il.BeginFinallyBlock(); il.Emit(OpCodes.Ldloc, transactionLocal); il.Emit(OpCodes.Callvirt, IDisposable_Dispose); il.EndExceptionBlock(); if (notifyChanged != null) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldstr, member.Name); il.Emit(OpCodes.Call, notifyChanged); } 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); } #region Logs 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 readonly MethodInfo LogWarningMethod = typeof(GeneratedStore).GetMethod(nameof(LogWarning), BindingFlags.NonPublic | BindingFlags.Static); internal static void LogWarning(string message) { Logger.config.Warn(message); } private static readonly MethodInfo LogWarningExceptionMethod = typeof(GeneratedStore).GetMethod(nameof(LogWarningException), BindingFlags.NonPublic | BindingFlags.Static); internal static void LogWarningException(Exception exception) { Logger.config.Warn(exception); } #endregion #region Correction private static bool NeedsCorrection(SerializedMemberInfo member) { var expectType = GetExpectedValueTypeForType(member.IsNullable ? member.NullableWrappedType : member.Type); if (expectType == typeof(Map)) // TODO: make this slightly saner return true; return false; } // expects start value on stack, exits with final value on stack private static void EmitCorrectMember(ILGenerator il, SerializedMemberInfo member, bool shouldLock, bool alwaysNew, GetLocal GetLocal) { if (!NeedsCorrection(member)) return; var endLabel = il.DefineLabel(); if (member.IsNullable) { il.Emit(OpCodes.Dup); il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod()); il.Emit(OpCodes.Brfalse, endLabel); il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); } // TODO: impl the rest of this // currently the only thing for this is where expect == Map, so do generate shit var copyFrom = typeof(IGeneratedStore<>).MakeGenericType(member.Type).GetMethod(nameof(IGeneratedStore.CopyFrom)); var noCreate = il.DefineLabel(); var valLocal = GetLocal(member.Type); if (!alwaysNew) { il.Emit(OpCodes.Dup); il.Emit(OpCodes.Isinst, typeof(IGeneratedStore)); il.Emit(OpCodes.Brtrue_S, endLabel); // our input is already something we like } il.Emit(OpCodes.Stloc, valLocal); if (!alwaysNew) { EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_0)); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Isinst, typeof(IGeneratedStore)); il.Emit(OpCodes.Brtrue_S, noCreate); il.Emit(OpCodes.Pop); } EmitCreateChildGenerated(il, member.Type); il.MarkLabel(noCreate); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldloc, valLocal); il.Emit(shouldLock ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); il.Emit(OpCodes.Callvirt, copyFrom); // TODO: impl the rest of this if (member.IsNullable) il.Emit(OpCodes.Newobj, member.Nullable_Construct); il.MarkLabel(endLabel); } #endregion #region Utility 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) { 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 void EmitLoad(ILGenerator il, SerializedMemberInfo member, Action thisarg = null) { if (thisarg == null) thisarg = il => il.Emit(OpCodes.Ldarg_0); thisarg(il); // 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 EmitWarnException(ILGenerator il, string v) { il.Emit(OpCodes.Ldstr, v); il.Emit(OpCodes.Call, LogWarningMethod); il.Emit(OpCodes.Call, LogWarningExceptionMethod); } 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 IDisposable_t = typeof(IDisposable); private static MethodInfo IDisposable_Dispose = IDisposable_t.GetMethod(nameof(IDisposable.Dispose)); 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 (member.HasConverter) { var stlocal = GetLocal(member.Type); var valLocal = GetLocal(typeof(Value)); il.Emit(OpCodes.Stloc, stlocal); il.BeginExceptionBlock(); il.Emit(OpCodes.Ldsfld, member.ConverterField); il.Emit(OpCodes.Ldloc, stlocal); if (member.IsGenericConverter) { var toValueBase = member.ConverterBase.GetMethod(nameof(ValueConverter.ToValue), new[] { member.ConverterTarget, typeof(object) }); var toValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(m => m.GetBaseDefinition() == toValueBase) ?? toValueBase; il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, toValue); } else { var toValueBase = typeof(IValueConverter).GetMethod(nameof(IValueConverter.ToValue), new[] { typeof(object), typeof(object) }); var toValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(m => m.GetBaseDefinition() == toValueBase) ?? toValueBase; il.Emit(OpCodes.Box); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, toValue); } il.Emit(OpCodes.Stloc, valLocal); il.BeginCatchBlock(typeof(Exception)); EmitWarnException(il, "Error serializing member using converter"); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Stloc, valLocal); il.EndExceptionBlock(); il.Emit(OpCodes.Ldloc, valLocal); } else 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); } #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 return typeof(Map); // default for various objects } private static void EmitDeserializeGeneratedValue(ILGenerator il, SerializedMemberInfo member, Type srcType, GetLocal GetLocal) { var IGeneratedStore_Deserialize = typeof(IGeneratedStore).GetMethod(nameof(IGeneratedStore.Deserialize)); var valuel = GetLocal(srcType, 0); var noCreate = il.DefineLabel(); il.Emit(OpCodes.Stloc, valuel); EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_0)); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Isinst, typeof(IGeneratedStore)); il.Emit(OpCodes.Brtrue_S, noCreate); il.Emit(OpCodes.Pop); EmitCreateChildGenerated(il, member.Type); il.MarkLabel(noCreate); 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, 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, SerializedMemberInfo member, 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, member, expected, GetLocal); } else { il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); } } private static void EmitDeserializeConverter(ILGenerator il, SerializedMemberInfo member, Label nextLabel, GetLocal GetLocal) { var stlocal = GetLocal(typeof(Value)); var valLocal = GetLocal(member.Type); il.Emit(OpCodes.Stloc, stlocal); il.BeginExceptionBlock(); il.Emit(OpCodes.Ldsfld, member.ConverterField); il.Emit(OpCodes.Ldloc, stlocal); il.Emit(OpCodes.Ldarg_0); if (member.IsGenericConverter) { var fromValueBase = member.ConverterBase.GetMethod(nameof(ValueConverter.FromValue), new[] { typeof(Value), typeof(object) }); var fromValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(m => m.GetBaseDefinition() == fromValueBase) ?? fromValueBase; il.Emit(OpCodes.Call, fromValue); } else { var fromValueBase = typeof(IValueConverter).GetMethod(nameof(IValueConverter.FromValue), new[] { typeof(Value), typeof(object) }); var fromValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(m => m.GetBaseDefinition() == fromValueBase) ?? fromValueBase; il.Emit(OpCodes.Call, fromValue); if (member.Type.IsValueType) il.Emit(OpCodes.Unbox); } il.Emit(OpCodes.Stloc, valLocal); il.BeginCatchBlock(typeof(Exception)); EmitWarnException(il, "Error occurred while deserializing"); il.Emit(OpCodes.Leave, nextLabel); il.EndExceptionBlock(); il.Emit(OpCodes.Ldloc, valLocal); } // 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); } if (!member.HasConverter) { 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 (member.HasConverter) il.MarkLabel(implLabel); else 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); } if (!member.HasConverter) { 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.HasConverter) EmitDeserializeConverter(il, member, nextLabel, GetLocal); else if (member.IsNullable) EmitDeserializeNullable(il, member, expectType, GetLocal); else EmitDeserializeValue(il, member, member.Type, expectType, GetLocal); il.Emit(OpCodes.Stloc, local); EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, local)); } #endregion } }