From fa1598f1515e65c5bbe9e680d61952ac49a3b992 Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Tue, 3 Dec 2024 17:28:44 +0300 Subject: [PATCH] Allow dynamic code in supported environments Fixes #16 --- src/HotAvalonia/Assets/DynamicAsset.cs | 6 +- src/HotAvalonia/Assets/DynamicAssetLoader.cs | 4 +- .../Assets/DynamicAssetTypeConverter.cs | 2 +- src/HotAvalonia/Helpers/AssemblyHelper.cs | 64 ++++++++++++++++++- src/HotAvalonia/Helpers/MethodHelper.cs | 20 ++++++ .../Reflection/Inject/CallbackInjector.cs | 2 +- .../Reflection/Inject/MethodInjector.cs | 8 +++ 7 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/HotAvalonia/Assets/DynamicAsset.cs b/src/HotAvalonia/Assets/DynamicAsset.cs index 29ca7df..682f8ec 100644 --- a/src/HotAvalonia/Assets/DynamicAsset.cs +++ b/src/HotAvalonia/Assets/DynamicAsset.cs @@ -296,7 +296,7 @@ public static Delegate CreateAssetFactory(Type delegateType, Type assetType) // public static TAsset Create{TAsset}(...args) // => new(...args); - DynamicMethod factory = new($"Create{assetType.Name}", assetType, parameterTypes, true); + using IDisposable context = MethodHelper.DefineDynamicMethod($"Create{assetType.Name}", assetType, parameterTypes, out DynamicMethod factory); ILGenerator il = factory.GetILGenerator(); for (int i = 0; i < parameterTypes.Length; i++) @@ -323,7 +323,7 @@ public static Delegate CreateAssetCopier(Type assetType) // public static void Copy{TAsset}(TAsset from, TAsset to) // { - DynamicMethod copier = new($"Copy{assetType.Name}", typeof(void), [assetType, assetType], true); + using IDisposable context = MethodHelper.DefineDynamicMethod($"Copy{assetType.Name}", typeof(void), [assetType, assetType], out DynamicMethod copier); ILGenerator il = copier.GetILGenerator(); // if (to.fieldN is IDisposable) @@ -365,7 +365,7 @@ public static Delegate CreateAssetCopier(Type assetType) private static ModuleBuilder CreateModuleBuilder() { string assemblyName = $"{nameof(HotAvalonia)}.{nameof(Assets)}.Dynamic"; - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + _ = AssemblyHelper.DefineDynamicAssembly(assemblyName, out AssemblyBuilder assemblyBuilder); assemblyBuilder.AllowAccessTo(typeof(DynamicAsset<>)); return assemblyBuilder.DefineDynamicModule(assemblyName); diff --git a/src/HotAvalonia/Assets/DynamicAssetLoader.cs b/src/HotAvalonia/Assets/DynamicAssetLoader.cs index 420e4a2..6dca378 100644 --- a/src/HotAvalonia/Assets/DynamicAssetLoader.cs +++ b/src/HotAvalonia/Assets/DynamicAssetLoader.cs @@ -157,7 +157,7 @@ public static Func CreateDynamicAssetLoaderFactory() // public static DynamicAssetLoaderImpl CreateDynamicAssetLoaderImpl(IAssetLoader loader) // => new(loader); - DynamicMethod factory = new($"Create{loaderType.Name}", loaderType, [typeof(IAssetLoader)], true); + using IDisposable context = MethodHelper.DefineDynamicMethod($"Create{loaderType.Name}", loaderType, [typeof(IAssetLoader)], out DynamicMethod factory); ILGenerator il = factory.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Newobj, ctor); @@ -245,7 +245,7 @@ public static Type CreateDynamicAssetLoaderType() private static ModuleBuilder CreateModuleBuilder() { string assemblyName = $"{nameof(HotAvalonia)}.{nameof(Assets)}.Dynamic"; - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + _ = AssemblyHelper.DefineDynamicAssembly(assemblyName, out AssemblyBuilder assemblyBuilder); assemblyBuilder.AllowAccessTo(typeof(DynamicAssetLoader)); return assemblyBuilder.DefineDynamicModule(assemblyName); diff --git a/src/HotAvalonia/Assets/DynamicAssetTypeConverter.cs b/src/HotAvalonia/Assets/DynamicAssetTypeConverter.cs index 297f35d..f2fbeb7 100644 --- a/src/HotAvalonia/Assets/DynamicAssetTypeConverter.cs +++ b/src/HotAvalonia/Assets/DynamicAssetTypeConverter.cs @@ -180,7 +180,7 @@ public static Type CreateDynamicAssetConverterType(Type assetType, Type assetCon private static ModuleBuilder CreateModuleBuilder() { string assemblyName = $"{nameof(HotAvalonia)}.{nameof(Assets)}.Dynamic"; - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + _ = AssemblyHelper.DefineDynamicAssembly(assemblyName, out AssemblyBuilder assemblyBuilder); assemblyBuilder.AllowAccessTo(typeof(DynamicAssetTypeConverter<>)); return assemblyBuilder.DefineDynamicModule(assemblyName); diff --git a/src/HotAvalonia/Helpers/AssemblyHelper.cs b/src/HotAvalonia/Helpers/AssemblyHelper.cs index 008b49c..3ce8c4d 100644 --- a/src/HotAvalonia/Helpers/AssemblyHelper.cs +++ b/src/HotAvalonia/Helpers/AssemblyHelper.cs @@ -34,6 +34,12 @@ internal static class AssemblyHelper isThreadSafe: true ); + /// + /// A delegate function used to temporarily allow dynamic code generation + /// even when RuntimeFeature.IsDynamicCodeSupported is false. + /// + private static readonly Func s_forceAllowDynamicCode = CreateForceAllowDynamicCodeDelegate(); + /// /// Retrieves all loadable types from a given assembly. /// @@ -132,6 +138,62 @@ public static void AllowAccessTo(this AssemblyBuilder sourceAssembly, MethodBase sourceAssembly.AllowAccessTo(assembly.GetName()); } + /// + /// Defines a new dynamic assembly with the specified name and temporarily allows dynamic code generation. + /// + /// The name of the dynamic assembly to define. + /// The resulting instance. + /// + /// An object that, when disposed, will revert the environment + /// to its previous state regarding support for dynamic code generation. + /// + public static IDisposable DefineDynamicAssembly(string assemblyName, out AssemblyBuilder assembly) + { + IDisposable dynamicCodeContext = ForceAllowDynamicCode(); + assembly = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + return dynamicCodeContext; + } + + /// + /// Temporarily allows dynamic code generation even when RuntimeFeature.IsDynamicCodeSupported is false. + /// + /// + /// An object that, when disposed, will revert the environment + /// to its previous state regarding support for dynamic code generation. + /// + /// + /// This is particularly useful in scenarios where the runtime can support emitting dynamic code, + /// but a feature switch or configuration has disabled it (e.g., PublishAot=true during debugging). + /// + public static IDisposable ForceAllowDynamicCode() + => s_forceAllowDynamicCode(); + + /// + /// Creates a delegate to the internal ForceAllowDynamicCode method, + /// enabling the temporary allowance of dynamic code generation. + /// + /// + /// A delegate that can be invoked to allow dynamic code generation. + /// + private static Func CreateForceAllowDynamicCodeDelegate() + { + MethodInfo? forceAllowDynamicCode = typeof(AssemblyBuilder).GetMethod( + nameof(ForceAllowDynamicCode), + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, + null, + Type.EmptyTypes, + null + ); + + if (forceAllowDynamicCode is not null && typeof(IDisposable).IsAssignableFrom(forceAllowDynamicCode.ReturnType)) + return (Func)forceAllowDynamicCode.CreateDelegate(typeof(Func)); + + // I'm too lazy to create a new type for the stub, so just use an empty + // `MemoryStream` that can be disposed of an infinite number of times. + IDisposable disposableInstance = new MemoryStream(Array.Empty()); + return () => disposableInstance; + } + /// /// Creates a dynamic type that represents the System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute. /// @@ -149,7 +211,7 @@ private static Type CreateIgnoresAccessChecksToAttributeType() const string moduleName = "IgnoresAccessChecksToAttributeDefinition"; string assemblyName = $"{moduleName}+{Guid.NewGuid():N}"; - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + using IDisposable context = DefineDynamicAssembly(assemblyName, out AssemblyBuilder assemblyBuilder); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName); TypeBuilder attributeBuilder = moduleBuilder.DefineType(attributeName, TypeAttributes.Class | TypeAttributes.Public, typeof(Attribute)); diff --git a/src/HotAvalonia/Helpers/MethodHelper.cs b/src/HotAvalonia/Helpers/MethodHelper.cs index a152e6c..53eadd4 100644 --- a/src/HotAvalonia/Helpers/MethodHelper.cs +++ b/src/HotAvalonia/Helpers/MethodHelper.cs @@ -1,5 +1,6 @@ using System.Linq.Expressions; using System.Reflection; +using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Mono.Cecil; @@ -41,6 +42,25 @@ public static Delegate CreateUnsafeDelegate(this MethodBase method, Type delegat return (Delegate)Activator.CreateInstance(delegateType, target, ptr)!; } + /// + /// Defines a new dynamic method with the specified name, return type, and parameter types, + /// and temporarily allows dynamic code generation. + /// + /// The name of the dynamic method to define. + /// The return type of the dynamic method. + /// An array of representing the parameter types of the dynamic method. + /// The resulting instance. + /// + /// An object that, when disposed, will revert the environment + /// to its previous state regarding support for dynamic code generation. + /// + public static IDisposable DefineDynamicMethod(string name, Type returnType, Type[] parameterTypes, out DynamicMethod method) + { + IDisposable dynamicCodeContext = AssemblyHelper.ForceAllowDynamicCode(); + method = new(name, returnType, parameterTypes, true); + return dynamicCodeContext; + } + /// /// Gets the type of the instance for instance methods or null for static methods. /// diff --git a/src/HotAvalonia/Reflection/Inject/CallbackInjector.cs b/src/HotAvalonia/Reflection/Inject/CallbackInjector.cs index bed0267..1786e4a 100644 --- a/src/HotAvalonia/Reflection/Inject/CallbackInjector.cs +++ b/src/HotAvalonia/Reflection/Inject/CallbackInjector.cs @@ -309,7 +309,7 @@ private static bool NeedsCallerMember(MethodBase method) private static ModuleBuilder CreateModuleBuilder() { string assemblyName = $"__Reflection.Inject.Dynamic+{Guid.NewGuid():N}"; - AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect); + _ = AssemblyHelper.DefineDynamicAssembly(assemblyName, out AssemblyBuilder assemblyBuilder); assemblyBuilder.AllowAccessTo(typeof(IInjection)); return assemblyBuilder.DefineDynamicModule(assemblyName); diff --git a/src/HotAvalonia/Reflection/Inject/MethodInjector.cs b/src/HotAvalonia/Reflection/Inject/MethodInjector.cs index 214382b..989c328 100644 --- a/src/HotAvalonia/Reflection/Inject/MethodInjector.cs +++ b/src/HotAvalonia/Reflection/Inject/MethodInjector.cs @@ -85,6 +85,9 @@ private static InjectionType DetectSupportedInjectionType() { try { + // Enable dynamic code generation, which is required for MonoMod to function. + _ = AssemblyHelper.ForceAllowDynamicCode(); + // `PlatformTriple.Current` may throw exceptions such as: // - NotImplementedException // - PlatformNotSupportedException @@ -123,6 +126,11 @@ private static InjectionType DetectSupportedInjectionType() /// The replacement method implementation. public NativeInjection(MethodBase source, MethodInfo replacement) { + // Enable dynamic code generation, which is required for MonoMod to function. + // Note that we cannot enable it forcefully just once and call it a day, + // because this only affects the current thread. + _ = AssemblyHelper.ForceAllowDynamicCode(); + _hook = new(source, replacement, applyByDefault: true); }