From d2fae906418b604ec8c2f032cc127c379c3e5f54 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 11 Jul 2023 11:55:13 -0500 Subject: [PATCH] Respect IsDynamicCodeSupported in more places in Linq.Expressions (#88539) * Respect IsDynamicCodeSupported in more places in Linq.Expressions * CanEmitObjectArrayDelegate * CanCreateArbitraryDelegates These properties are all set to `false` when running on NativeAOT, so have them respect RuntimeFeature.IsDynamicCodeSupported. However, CanEmitObjectArrayDelegate needs a work around because DynamicDelegateAugments.CreateObjectArrayDelegate does not exist in CoreClr's System.Private.CoreLib. Allow System.Linq.Expressions to create an object[] delegate using Ref.Emit even though RuntimeFeature.IsDynamicCodeSupported is set to false (ex. using a feature switch). To enable this, add an internal method in CoreLib that temporarily allows the current thread to skip the RuntimeFeature check and allows DynamicMethod instances to be created. When System.Linq.Expressions needs to generate one of these delegates, it calls the internal method through Reflection and continues to use Ref.Emit to generate the delegate. Fix #81803 --- .../src/System/Dynamic/Utils/DelegateHelpers.cs | 37 ++++++++++++--- .../Expressions/Interpreter/CallInstruction.cs | 2 +- .../System.Linq.Expressions/tests/CompilerTests.cs | 52 ++++++++++++++++++---- .../src/ILLink/ILLink.Descriptors.LibraryBuild.xml | 4 ++ .../src/System/Reflection/Emit/AssemblyBuilder.cs | 33 +++++++++++++- 5 files changed, 113 insertions(+), 15 deletions(-) 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 b46fa67..a56b5f8 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 @@ namespace System.Dynamic.Utils 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 @@ namespace System.Dynamic.Utils 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 @@ namespace System.Dynamic.Utils 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 951f44a..5ab0cea 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 @@ namespace System.Linq.Expressions.Interpreter /// 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 3c2f4bc..3521789 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 @@ namespace System.Linq.Expressions.Tests 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 2cafb97..9ea6ada 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 4989a83..d7ee536 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 @@ namespace System.Reflection.Emit 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); } -- 2.7.4