diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index 55c78fa0..77385a0b 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -22,6 +22,11 @@ namespace IPA.Config Instance = LoaderConfig.Generated(); } + protected internal virtual void OnReload() + { + StandardLogger.Configure(this); + } + public static void ReadCommandLine(string[] args) { foreach (var arg in args) diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs index 5f7e17f2..575681b0 100644 --- a/IPA.Loader/Config/Stores/GeneratedStore.cs +++ b/IPA.Loader/Config/Stores/GeneratedStore.cs @@ -26,8 +26,13 @@ namespace IPA.Config.Stores /// A class providing an extension for to make it easy to use generated /// config stores. /// - public static class GeneratedStoreExtensions + 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 @@ -35,7 +40,25 @@ namespace IPA.Config.Stores /// /// /// - /// must be a non- . + /// must be a public non- . + /// 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)] + /// + /// + /// + /// If the declares a or , + /// 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. + /// + /// + /// Similarly, can declare a or , + /// 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 @@ -69,6 +92,7 @@ namespace IPA.Config.Stores Type Type { get; } IGeneratedStore Parent { get; } Impl Impl { get; } + void OnReload(); } internal class Impl : IConfigStore @@ -95,7 +119,7 @@ namespace IPA.Config.Stores internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead)); public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead(); - public void ReleaseRead() => WriteSyncObject.ExitWriteLock(); + public void ReleaseRead() => WriteSyncObject.ExitReadLock(); internal static MethodInfo ImplTakeWriteMethod = typeof(Impl).GetMethod(nameof(ImplTakeWrite)); public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s).TakeWrite(); @@ -121,6 +145,10 @@ namespace IPA.Config.Stores Logger.config.Debug("Generated impl ReadFrom"); Logger.config.Debug($"Read {values}"); generated.Values = 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)); @@ -140,6 +168,11 @@ namespace IPA.Config.Stores 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) { if (generatedCreators.TryGetValue(type, out var creator)) @@ -201,7 +234,7 @@ namespace IPA.Config.Stores if (baseCtor == null) throw new ArgumentException("Config type does not have a public parameterless constructor"); - var typeBuilder = Module.DefineType($"{type.FullName}.Generated", + var typeBuilder = Module.DefineType($"{type.FullName}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, type); var typeField = typeBuilder.DefineField("<>_type", typeof(Type), FieldAttributes.Private | FieldAttributes.InitOnly); @@ -212,8 +245,10 @@ namespace IPA.Config.Stores // 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.Instance, null, Type.EmptyTypes, Array.Empty()); + 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(); @@ -298,7 +333,25 @@ namespace IPA.Config.Stores var IGeneratedStore_GetParent = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Parent)).GetGetMethod(); var IGeneratedStore_GetValues = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Values)).GetGetMethod(); var IGeneratedStore_SetValues = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Values)).GetSetMethod(); + var IGeneratedStore_OnReload = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.OnReload)); + + #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); @@ -619,6 +672,13 @@ namespace IPA.Config.Stores 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 @@ -670,9 +730,21 @@ namespace IPA.Config.Stores return typeof(Map); // default for various objects } + private static void EmitDeserializeGeneratedValue(ILGenerator il, Type targetType, Type srcType, Func GetLocal) + { + var IGeneratedStore_ValueSet = typeof(IGeneratedStore).GetProperty(nameof(IGeneratedStore.Values)).GetSetMethod(); + + 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_ValueSet); + } + // 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, Label nextLabel) + private static void EmitDeserializeValue(ILGenerator il, Type targetType, Label nextLabel, Func GetLocal) { if (typeof(Value).IsAssignableFrom(targetType)) return; // do nothing @@ -699,6 +771,10 @@ namespace IPA.Config.Stores 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); @@ -715,9 +791,6 @@ namespace IPA.Config.Stores var passedTypeCheck = il.DefineLabel(); var expectType = GetExpectedValueTypeForType(member.Type); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brtrue_S, implLabel); // null check - void EmitStore(Action value) { il.Emit(OpCodes.Ldarg_0); // load this @@ -746,6 +819,9 @@ namespace IPA.Config.Stores return builder; } + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue_S, implLabel); // null check + if (member.Type.IsValueType) { il.Emit(OpCodes.Pop); @@ -766,6 +842,39 @@ namespace IPA.Config.Stores 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 => @@ -778,7 +887,7 @@ namespace IPA.Config.Stores il.MarkLabel(passedTypeCheck); var local = GetLocal(member.Type); - EmitDeserializeValue(il, member.Type, nextLabel); + EmitDeserializeValue(il, member.Type, nextLabel, GetLocal); il.Emit(OpCodes.Stloc, local); EmitStore(il => il.Emit(OpCodes.Ldloc, local)); }