diff --git a/src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs b/src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs index b46fa67682891..a56b5f813adec 100644 --- a/src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs +++ b/src/libraries/System.Linq.Expressions/src/System/Dynamic/Utils/DelegateHelpers.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -11,7 +12,7 @@ namespace System.Dynamic.Utils { internal static class DelegateHelpers { - // This can be flipped to true using feature switches at publishing time + // This can be flipped to false using feature switches at publishing time internal static bool CanEmitObjectArrayDelegate => true; // Separate class so that the it can be trimmed away and doesn't get conflated @@ -21,14 +22,23 @@ private static class DynamicDelegateLightup public static Func, Delegate> CreateObjectArrayDelegate { get; } = CreateObjectArrayDelegateInternal(); - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", - Justification = "Works around https://github.com/dotnet/linker/issues/2392")] private static Func, Delegate> CreateObjectArrayDelegateInternal() => Type.GetType("Internal.Runtime.Augments.DynamicDelegateAugments")! .GetMethod("CreateObjectArrayDelegate")! .CreateDelegate, Delegate>>(); } + private static class ForceAllowDynamicCodeLightup + { + public static Func? ForceAllowDynamicCodeDelegate { get; } + = ForceAllowDynamicCodeDelegateInternal(); + + private static Func? ForceAllowDynamicCodeDelegateInternal() + => typeof(AssemblyBuilder) + .GetMethod("ForceAllowDynamicCode", BindingFlags.NonPublic | BindingFlags.Static) + ?.CreateDelegate>(); + } + internal static Delegate CreateObjectArrayDelegate(Type delegateType, Func handler) { if (CanEmitObjectArrayDelegate) @@ -186,6 +196,23 @@ private static Delegate CreateObjectArrayDelegateRefEmit(Type delegateType, Func if (thunkMethod == null) { + static IDisposable? CreateForceAllowDynamicCodeScope() + { + if (!RuntimeFeature.IsDynamicCodeSupported) + { + // Force 'new DynamicMethod' to not throw even though RuntimeFeature.IsDynamicCodeSupported is false. + // If we are running on a runtime that supports dynamic code, even though the feature switch is off, + // for example when running on CoreClr with PublishAot=true, this will allow IL to be emitted. + // If we are running on a runtime that really doesn't support dynamic code, like NativeAOT, + // CanEmitObjectArrayDelegate will be flipped to 'false', and this method won't be invoked. + return ForceAllowDynamicCodeLightup.ForceAllowDynamicCodeDelegate?.Invoke(); + } + + return null; + } + + using IDisposable? forceAllowDynamicCodeScope = CreateForceAllowDynamicCodeScope(); + int thunkIndex = Interlocked.Increment(ref s_ThunksCreated); Type[] paramTypes = new Type[parameters.Length + 1]; paramTypes[0] = typeof(Func); @@ -270,8 +297,8 @@ private static Delegate CreateObjectArrayDelegateRefEmit(Type delegateType, Func ilgen.BeginFinallyBlock(); for (int i = 0; i < parameters.Length; i++) { - if (parameters[i].ParameterType.IsByRef) - { + if (parameters[i].ParameterType.IsByRef) + { Type byrefToType = parameters[i].ParameterType.GetElementType()!; // update parameter diff --git a/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs b/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs index 951f44a6c50f5..5ab0cea57217b 100644 --- a/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs +++ b/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs @@ -16,7 +16,7 @@ internal abstract partial class CallInstruction : Instruction /// public abstract int ArgumentCount { get; } - private static bool CanCreateArbitraryDelegates => true; + private static bool CanCreateArbitraryDelegates => RuntimeFeature.IsDynamicCodeSupported; #region Construction diff --git a/src/libraries/System.Linq.Expressions/tests/CompilerTests.cs b/src/libraries/System.Linq.Expressions/tests/CompilerTests.cs index 3c2f4bc6b5698..3521789b3eefa 100644 --- a/src/libraries/System.Linq.Expressions/tests/CompilerTests.cs +++ b/src/libraries/System.Linq.Expressions/tests/CompilerTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.DotNet.RemoteExecutor; @@ -442,16 +443,51 @@ public static void CompileWorksWhenDynamicCodeNotSupported() using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(static () => { - ParameterExpression param = Expression.Parameter(typeof(int)); + CompileWorksWhenDynamicCodeNotSupportedInner(); + }, options); + } - Func typedDel = - Expression.Lambda>(Expression.Add(param, Expression.Constant(4)), param).Compile(); - Assert.Equal(304, typedDel(300)); + // run the same test code as the above CompileWorksWhenDynamicCodeNotSupported test + // to ensure this test works correctly on all platforms - even if RemoteExecutor is not supported + [Fact] + public static void CompileWorksWhenDynamicCodeNotSupportedInner() + { + ParameterExpression param = Expression.Parameter(typeof(int)); - Delegate del = - Expression.Lambda(Expression.Add(param, Expression.Constant(5)), param).Compile(); - Assert.Equal(305, del.DynamicInvoke(300)); - }, options); + Func typedDel = + Expression.Lambda>(Expression.Add(param, Expression.Constant(4)), param).Compile(); + Assert.Equal(304, typedDel(300)); + + Delegate del = + Expression.Lambda(Expression.Add(param, Expression.Constant(5)), param).Compile(); + Assert.Equal(305, del.DynamicInvoke(300)); + + // testing more than 2 parameters is important because because it follows a different code path in Compile. + Expression> fiveParameterExpression = (a, b, c, d, e) => a + b + c + d + e; + Func fiveParameterFunc = fiveParameterExpression.Compile(); + Assert.Equal(6, fiveParameterFunc(2, 2, 1, 1, 0)); + + Expression> callExpression = (a, b) => Add(a, b); + Func callFunc = callExpression.Compile(); + Assert.Equal(29, callFunc(20, 9)); + + MethodCallExpression methodCallExpression = Expression.Call( + instance: null, + typeof(CompilerTests).GetMethod("Add4", BindingFlags.NonPublic | BindingFlags.Static), + Expression.Constant(5), Expression.Constant(6), Expression.Constant(7), Expression.Constant(8)); + + Func methodCallDelegate = Expression.Lambda>(methodCallExpression).Compile(); + Assert.Equal(26, methodCallDelegate()); + } + + private static int Add(int a, int b) + { + return a + b; + } + + private static int Add4(int a, int b, int c, int d) + { + return a + b + c + d; } } diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.xml index 2cafb97358f18..9ea6ada68e8cf 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.LibraryBuild.xml @@ -8,6 +8,10 @@ + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.cs index 4989a83cc4912..d7ee53669cc91 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/AssemblyBuilder.cs @@ -9,6 +9,9 @@ namespace System.Reflection.Emit { public abstract partial class AssemblyBuilder : Assembly { + [ThreadStatic] + private static bool t_allowDynamicCode; + protected AssemblyBuilder() { } @@ -81,12 +84,40 @@ public override string[] GetManifestResourceNames() => internal static void EnsureDynamicCodeSupported() { - if (!RuntimeFeature.IsDynamicCodeSupported) + if (!RuntimeFeature.IsDynamicCodeSupported && !t_allowDynamicCode) { ThrowDynamicCodeNotSupported(); } } + /// + /// Allows dynamic code even though RuntimeFeature.IsDynamicCodeSupported is false. + /// + /// An object that, when disposed, will revert allowing dynamic code back to its initial state. + /// + /// This is useful for cases where RuntimeFeature.IsDynamicCodeSupported returns false, but + /// the runtime is still capable of emitting dynamic code. For example, when generating delegates + /// in System.Linq.Expressions while PublishAot=true is set in the project. At debug time, the app + /// uses the non-AOT runtime with the IsDynamicCodeSupported feature switch set to false. + /// + internal static IDisposable ForceAllowDynamicCode() => new ForceAllowDynamicCodeScope(); + + private sealed class ForceAllowDynamicCodeScope : IDisposable + { + private readonly bool _previous; + + public ForceAllowDynamicCodeScope() + { + _previous = t_allowDynamicCode; + t_allowDynamicCode = true; + } + + public void Dispose() + { + t_allowDynamicCode = _previous; + } + } + private static void ThrowDynamicCodeNotSupported() => throw new PlatformNotSupportedException(SR.PlatformNotSupported_ReflectionEmit); }