Skip to content

Commit

Permalink
Allow dynamic code in supported environments
Browse files Browse the repository at this point in the history
Fixes #16
  • Loading branch information
Kir-Antipov committed Dec 3, 2024
1 parent c62337b commit fa1598f
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 8 deletions.
6 changes: 3 additions & 3 deletions src/HotAvalonia/Assets/DynamicAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/HotAvalonia/Assets/DynamicAssetLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public static Func<IAssetLoader, IAssetLoader> 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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/HotAvalonia/Assets/DynamicAssetTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
64 changes: 63 additions & 1 deletion src/HotAvalonia/Helpers/AssemblyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ internal static class AssemblyHelper
isThreadSafe: true
);

/// <summary>
/// A delegate function used to temporarily allow dynamic code generation
/// even when <c>RuntimeFeature.IsDynamicCodeSupported</c> is <c>false</c>.
/// </summary>
private static readonly Func<IDisposable> s_forceAllowDynamicCode = CreateForceAllowDynamicCodeDelegate();

/// <summary>
/// Retrieves all loadable types from a given assembly.
/// </summary>
Expand Down Expand Up @@ -132,6 +138,62 @@ public static void AllowAccessTo(this AssemblyBuilder sourceAssembly, MethodBase
sourceAssembly.AllowAccessTo(assembly.GetName());
}

/// <summary>
/// Defines a new dynamic assembly with the specified name and temporarily allows dynamic code generation.
/// </summary>
/// <param name="assemblyName">The name of the dynamic assembly to define.</param>
/// <param name="assembly">The resulting <see cref="AssemblyBuilder"/> instance.</param>
/// <returns>
/// An <see cref="IDisposable"/> object that, when disposed, will revert the environment
/// to its previous state regarding support for dynamic code generation.
/// </returns>
public static IDisposable DefineDynamicAssembly(string assemblyName, out AssemblyBuilder assembly)
{
IDisposable dynamicCodeContext = ForceAllowDynamicCode();
assembly = AssemblyBuilder.DefineDynamicAssembly(new(assemblyName), AssemblyBuilderAccess.RunAndCollect);
return dynamicCodeContext;
}

/// <summary>
/// Temporarily allows dynamic code generation even when <c>RuntimeFeature.IsDynamicCodeSupported</c> is <c>false</c>.
/// </summary>
/// <returns>
/// An <see cref="IDisposable"/> object that, when disposed, will revert the environment
/// to its previous state regarding support for dynamic code generation.
/// </returns>
/// <remarks>
/// 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., <c>PublishAot=true</c> during debugging).
/// </remarks>
public static IDisposable ForceAllowDynamicCode()
=> s_forceAllowDynamicCode();

/// <summary>
/// Creates a delegate to the internal <c>ForceAllowDynamicCode</c> method,
/// enabling the temporary allowance of dynamic code generation.
/// </summary>
/// <returns>
/// A delegate that can be invoked to allow dynamic code generation.
/// </returns>
private static Func<IDisposable> 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<IDisposable>)forceAllowDynamicCode.CreateDelegate(typeof(Func<IDisposable>));

// 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<byte>());
return () => disposableInstance;
}

/// <summary>
/// Creates a dynamic type that represents the <c>System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute</c>.
/// </summary>
Expand All @@ -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));
Expand Down
20 changes: 20 additions & 0 deletions src/HotAvalonia/Helpers/MethodHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,6 +42,25 @@ public static Delegate CreateUnsafeDelegate(this MethodBase method, Type delegat
return (Delegate)Activator.CreateInstance(delegateType, target, ptr)!;
}

/// <summary>
/// Defines a new dynamic method with the specified name, return type, and parameter types,
/// and temporarily allows dynamic code generation.
/// </summary>
/// <param name="name">The name of the dynamic method to define.</param>
/// <param name="returnType">The return type of the dynamic method.</param>
/// <param name="parameterTypes">An array of <see cref="Type"/> representing the parameter types of the dynamic method.</param>
/// <param name="method">The resulting <see cref="DynamicMethod"/> instance.</param>
/// <returns>
/// An <see cref="IDisposable"/> object that, when disposed, will revert the environment
/// to its previous state regarding support for dynamic code generation.
/// </returns>
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;
}

/// <summary>
/// Gets the type of the instance for instance methods or <c>null</c> for static methods.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/HotAvalonia/Reflection/Inject/CallbackInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/HotAvalonia/Reflection/Inject/MethodInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +126,11 @@ private static InjectionType DetectSupportedInjectionType()
/// <param name="replacement">The replacement method implementation.</param>
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);
}

Expand Down

0 comments on commit fa1598f

Please sign in to comment.