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 struct 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 IsGenericConverter; // used so we can call directly to the generic version if it is
public Type Converter;
public Type ConverterTarget;
// 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)
{
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
static 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;
var converterAttr = attrs.Select(o => o as UseConverterAttribute).NonNull().FirstOrDefault();
if (converterAttr != null)
{
member.Converter = converterAttr.ConverterType;
member.ConverterTarget = converterAttr.ConverterTargetType;
member.IsGenericConverter = member.ConverterTarget != null;
}
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 Constructor
var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGeneratedStore) });
{ // because this is a constructor, it has to be raw IL
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
// TODO: generate overrides for all the virtual properties
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);
}
}
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(ulong)) return typeof(Integer);
if (valT == typeof(float)
|| valT == typeof(double)
|| valT == typeof(decimal)) 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
}
}